Skip to main content
Sigvex

Remediating Constructor State in Upgradeable Contracts

How to move state initialization from constructors to initializer functions in proxy-based upgradeable contracts.

Remediating Constructor State in Upgradeable Contracts

Overview

Related Detector: Constructor State

When an upgradeable contract sets state in its constructor, that state exists only in the implementation contract’s storage. The proxy’s storage – where all user interactions occur via delegatecall – never receives these values. The fix is to move all state initialization to an initialize() function and call _disableInitializers() in the constructor.

Move State to an Initializer Function

// BEFORE: State set in constructor -- invisible to proxy
contract TokenV1 {
    address public owner;
    string public name;
    uint256 public totalSupply;

    constructor(string memory _name, uint256 _supply) {
        owner = msg.sender;
        name = _name;
        totalSupply = _supply;
    }
}

// AFTER: State set in initializer -- written to proxy storage
import "@audited/proxy-utils/proxy/utils/Initializable.sol";

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

    constructor() { _disableInitializers(); }

    function initialize(string memory _name, uint256 _supply) external initializer {
        owner = msg.sender;
        name = _name;
        totalSupply = _supply;
    }
}

Alternative Mitigations

Use OpenZeppelin’s Upgradeable Variants

Replace standard OpenZeppelin imports with their upgradeable equivalents:

// BEFORE: standard OZ (uses constructors internally)
import "@audited/token/ERC20/ERC20.sol";

// AFTER: upgradeable OZ (uses initializers internally)
import "@audited/proxy-utils/token/ERC20/ERC20Upgradeable.sol";

contract TokenV1 is ERC20Upgradeable {
    constructor() { _disableInitializers(); }

    function initialize(string memory name, string memory symbol, uint256 supply)
        external initializer
    {
        __ERC20_init(name, symbol);
        _mint(msg.sender, supply);
    }
}

Immutable Variables for Constants

Values that never change can use Solidity immutable variables, which are stored in the bytecode (not storage) and are visible to the proxy:

contract VaultV1 is Initializable {
    // Immutable -- stored in bytecode, visible to proxy
    IERC20 public immutable asset;

    constructor(IERC20 _asset) {
        asset = _asset;  // OK: immutable is in bytecode
        _disableInitializers();
    }

    function initialize() external initializer {
        // Mutable state goes here
    }
}

Common Mistakes

Mistake: Inheriting Non-Upgradeable Base Contracts

// WRONG: Ownable uses a constructor internally
import "@audited/access/Ownable.sol";

contract VaultV1 is Ownable, Initializable {
    constructor() Ownable(msg.sender) {  // Owner set in implementation storage
        _disableInitializers();
    }
}

Use OwnableUpgradeable and call __Ownable_init() inside initialize().

Mistake: Forgetting to Disable Initializers

contract VaultV1 is Initializable {
    // Missing: constructor() { _disableInitializers(); }
    function initialize() external initializer { /* ... */ }
}
// Anyone can call initialize() directly on the implementation contract

Always call _disableInitializers() in the constructor.

Mistake: Using constructor Parameters for Proxy State

constructor(address _oracle) {
    oracle = _oracle;  // Only in implementation storage
}

Constructor parameters for mutable state must be moved to initialize() parameters.

References