Remediating Initializer Reentrancy
How to prevent reentrancy in proxy initializer functions by using OpenZeppelin's Initializable modifier and setting the initialization flag before external calls.
Remediating Initializer Reentrancy
Overview
Related Detector: Initializer Reentrancy
Initializer reentrancy occurs when an initialize() function makes an external call before setting the _initialized flag. The external contract can call back into initialize() and execute the initialization logic again. The fix is to use OpenZeppelin’s Initializable contract, which sets the flag before the function body runs, or to manually set the flag as the first operation.
Recommended Fix
Use OpenZeppelin’s initializer Modifier
// BEFORE: Manual initialization without protection
function initialize(address _token) external {
IERC20(_token).balanceOf(address(this)); // External call first
initialized = true; // Too late
owner = msg.sender;
}
// AFTER: OpenZeppelin Initializable
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract Vault is Initializable {
function initialize(address _token) external initializer {
// The initializer modifier sets the flag BEFORE this body runs
owner = msg.sender;
IERC20(_token).balanceOf(address(this)); // Safe now
}
}
Alternative Mitigations
Manual Flag-First Pattern
If not using OpenZeppelin, set the flag as the very first operation:
contract Vault {
bool private _initialized;
address public owner;
function initialize(address _token) external {
require(!_initialized, "Already initialized");
_initialized = true; // Set flag FIRST
owner = msg.sender;
IERC20(_token).approve(address(this), type(uint256).max);
}
}
Disable Initializers in Constructor
Prevent the implementation contract from being initialized directly:
contract VaultV1 is Initializable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // Prevents direct initialization
}
function initialize() external initializer {
__Ownable_init(msg.sender);
}
}
Common Mistakes
Mistake: Setting Flag After External Calls
function initialize(address oracle) external {
require(!_initialized, "Already initialized");
// External calls BEFORE flag is set
price = IOracle(oracle).getPrice();
_initialized = true; // Window of reentrancy exists
}
Move _initialized = true above the external call.
Mistake: Using initializer But Not disableInitializers
contract VaultV1 is Initializable {
// Missing: constructor() { _disableInitializers(); }
function initialize() external initializer {
owner = msg.sender;
}
}
// The implementation contract itself can be initialized by anyone
// calling initialize() directly on the implementation address
Always call _disableInitializers() in the constructor to protect the implementation.
Mistake: Multiple Initializer Functions Without Coordination
function initialize() external initializer {
owner = msg.sender;
}
function initializeV2(address newOracle) external reinitializer(2) {
oracle = newOracle;
// If this makes an external call, it can be re-entered before
// the reinitializer version is incremented
}
Apply the same CEI pattern to reinitializer functions.