Skip to main content
Sigvex

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.

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's Initializable
import "@audited/proxy-utils/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 OpenZeppelin’s library is unavailable, 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.

References