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 "@openzeppelin/contracts-upgradeable/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
- Initializer identification: Functions with names containing
init,setup, orconfigureare analyzed. - External call tracking: Locates
CALL,DELEGATECALL, andSTATICCALLinstructions within initializer functions. - Flag write detection: Finds
SSTOREoperations to low-numbered storage slots or the OpenZeppelinInitializableslot that represent the initialization guard. - 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.
- Context suppression: Contracts using OpenZeppelin’s
Initializablemodifier, 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.
Related Detectors
- Reentrancy — detects general reentrancy patterns
- Constructor State — detects improper state initialization in constructors
- UUPS Initialization — detects uninitialized UUPS proxies