Reinitializer() works even with _disableInitializer() in constructor?

I have deployed an upgradable contract shown below.
I recently realized that I had missed adding the __ReentrancyGuard_init(); statement to the initialize function.

I had also added _disableInitializers() to the constructor.

So to initialize __ReentrancyGuard_init();, I added a new function initializeV2() with a reinitialize modifier and tried to deploy a new implementation, but it wasn't enough. Thus added __ReentrancyGuard_init(); to the old initialize function and tried to upgrade.

Surprisingly the upgrade worked and I called the initializeV2() function as well.

As per my understanding, initializeV2() should not have been executed as I have added _disableInitializers() to the constructor in the initial code.

Can someone, help me understand how this was possible?

Check the logs here, the "Initialized" event has emitted twice.

Link To Contract Event Logs

Initial Code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract LpToken is
    ERC4626Upgradeable,
    UUPSUpgradeable,
    ReentrancyGuardUpgradeable
{

    // ========================== Functions ========================== //
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() payable {
        _disableInitializers();
    }

    function initialize(address usdc, string memory _name, string memory _symbol) public initializer {
        __UUPSUpgradeable_init();
        __ERC4626_init(IERC20(usdc)); 
        __ERC20_init(_name, _symbol);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }

}

Upgraded Code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.27;

import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract LpToken is
    ERC4626Upgradeable,
    UUPSUpgradeable,
    ReentrancyGuardUpgradeable
{

    // ========================== Functions ========================== //
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() payable {
        _disableInitializers();
    }

    function initialize(address usdc, string memory _name, string memory _symbol) public initializer {
        __UUPSUpgradeable_init();
        __ReentrancyGuard_init(); // $$$$$$$$ THIS LINE $$$$$$$$ //
        __ERC4626_init(IERC20(usdc));
        __ERC20_init(_name, _symbol);
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }

    // $$$$$$$$ THIS NEW FUNCTION $$$$$$$$ //
    /// @notice Second initializer to add ReentrancyGuard
    /// @custom:oz-upgrades-unsafe-allow constructor
    function initializeV2() public reinitializer(2) {
        __ReentrancyGuard_init();
    }

}

Hi, welcome to the community! :wave:

Do you mean you can call the function initializeV2() in the new implementation contract?

Yes. I was able to execute initializeV2() in the new implementation.

Yes. I was able to execute initializeV2() in the new implementation.

Any idea why this might happen?

The function _disableInitializers is used in the constructor of a contract to prevent that contract from being initialized or reinitialized to any version, in your case, I am not sure, is it possible to share the full project or the scripts at here? so I can have a check what is wrong.