Skip to main content
Sigvex

Initializer Reentrancy

Detects proxy initializer functions that make external calls before setting the initialized flag, enabling re-initialization attacks.

Initializer Reentrancy

Overview

The initializer reentrancy detector identifies initialization functions in upgradeable contracts that make external calls before setting the _initialized flag. In proxy patterns, initialize() replaces the constructor. If this function calls an external contract before marking itself as initialized, the external contract can call back into initialize() and execute the initialization logic a second time.

This vulnerability class affected Cream Finance and multiple proxy implementations where unguarded external calls in initializers allowed double initialization.

Why This Is an Issue

Upgradeable contracts rely on a storage flag (_initialized) to prevent re-initialization. The initialization function typically sets critical state: the owner address, token parameters, protocol fees, and access control roles. If an attacker can trigger initialization twice, they can overwrite the owner, reset fees to zero, or corrupt the contract into an exploitable state.

The window of vulnerability exists from the moment the proxy is deployed until initialize() completes. If the initializer makes an external call before writing the flag, the callback can re-enter and execute the entire initializer again.

How to Resolve

// Before: Vulnerable -- external call before flag
function initialize(address token) public {
    uint256 balance = IERC20(token).balanceOf(address(this));  // Callback risk
    _initialized = true;  // Too late
    owner = msg.sender;
}

// After: Fixed -- flag set first
function initialize(address token) public initializer {
    __Ownable_init(msg.sender);
    // External calls are safe after the initializer modifier sets the flag
    uint256 balance = IERC20(token).balanceOf(address(this));
}

Examples

Vulnerable

contract VaultV1 {
    bool private _initialized;
    address public owner;
    address public rewardToken;

    function initialize(address _reward) external {
        // External call BEFORE setting initialized flag
        IERC20(_reward).approve(address(this), type(uint256).max);
        _initialized = true;
        owner = msg.sender;
        rewardToken = _reward;
    }
}

Fixed

import "@audited/proxy/utils/Initializable.sol";

contract VaultV1 is Initializable {
    address public owner;
    address public rewardToken;

    function initialize(address _reward) external initializer {
        owner = msg.sender;
        rewardToken = _reward;
        // External call AFTER initializer modifier sets the flag
        IERC20(_reward).approve(address(this), type(uint256).max);
    }
}

Sample Sigvex Output

[HIGH] initializer-reentrancy
  Initializer function 'initialize' makes external call at block 0:1
  BEFORE setting initialization flag at block 0:3
  Location: initialize @ block 0, instruction 1
  Confidence: 0.85

Detection Methodology

  1. Initializer identification: Functions with names containing init, setup, or configure are analyzed.
  2. External call tracking: Locates CALL, DELEGATECALL, and STATICCALL instructions within initializer functions.
  3. Flag write detection: Finds SSTORE operations to low-numbered storage slots or OpenZeppelin Initializable slot that represent the initialization guard.
  4. Ordering analysis: Compares the position of the first external call against the first flag write. If the call precedes the flag, the finding is reported.
  5. Context suppression: Contracts using OpenZeppelin’s Initializable modifier, UUPS patterns, or reentrancy guards receive reduced severity.

Limitations

False positives: STATICCALL (read-only) triggers detection even though it cannot modify state during the callback. Contracts using OpenZeppelin’s initializer modifier are automatically suppressed. False negatives: Initializers that delegate to internal helper functions making external calls are not traced cross-function.

References