Noisy Quartz Bird
High
/// @dev The initialized flag
Uint256Storage private constant _version = Uint256Storage.wrap(keccak256("equilibria.root.Initializable.version"));
/// @dev The initializing flag
BoolStorage private constant _initializing = BoolStorage.wrap(keccak256("equilibria.root.Initializable.initializing"));
/// @dev Can only be called once per version, `version` is 1-indexed
modifier initializer(uint256 version) {
if (version == 0) revert InitializableZeroVersionError();
if (_version.read() >= version) revert InitializableAlreadyInitializedError(version);
_version.store(version);
_initializing.store(true);
_;
_initializing.store(false);
emit Initialized(version);
}
_version
is critical to upgrade security.
If it is reset, manipulated, or bypassed, an attacker can take over the contract via re-initialization or forced downgrades.
If _version
is stored in an upgradeable proxy’s storage, an attacker could overwrite it and re-trigger initialize()
, bypassing security.
This attack is a serious risk in upgradeable contracts.
Many real-world exploits involve storage overwrites, making _version
a critical weakness.
https://solodit.cyfrin.io/issues/changing-_initializableslot-can-cause-_disableinitializers-to-actually-enable-initializers-spearbit-none-coinbase-solady-pdf
Since _version
is used to track the initialization state, if an attacker finds a way to corrupt or reset _version, they can re-trigger initialize().
Attack Steps
Find a Storage Collision or Overwrite _version
If _version
is stored in an upgradeable proxy contract, an attacker could override this slot by deploying a malicious contract that writes over the same storage index.
Example Attack Contract:
contract StorageCorruptor {
function overwriteVersion(address target) public {
bytes32 slot = keccak256("equilibria.market.version"); // Hypothetical storage slot
assembly {
sstore(slot, 0) // Force `_version` back to 0
}
IMarket(target).initialize(IMarket.MarketDefinition(...)); // Reinitialize!
}
}
This forces _version = 0
, bypassing the initializer(1) check.
Call initialize()
Again
After corrupting _version
, the attacker calls initialize() again to replace the token and oracle with malicious versions.
Outcome
The attacker resets initialization.
They replace the token with a malicious ERC-20 that allows infinite minting.
They set oracle to a fake price feed, allowing market manipulation.
They drain all user funds by exploiting liquidity rules.
How to Fix This?
- Store
_version
in an Immutable Contract If_version
is only stored in a proxy contract, it becomes easier to overwrite. Moving_version
into an immutable logic contract removes this attack vector. Double-Lock Initialization with a Boolean Flag
bool private _isInitialized;
modifier initializer(uint256 version) {
require(!_isInitialized, "Already initialized");
_isInitialized = true;
_;
}
This prevents any form of re-initialization. Use Explicit Storage Slots for Upgradeable Contracts
Instead of keccak256("some string"), explicitly define storage slots to prevent overwrites. 2. Use a Hard Upgrade Lock
require(_version == expectedVersion, "Version mismatch!");
Blocks reverting to older versions.
- Verify
_version
Before Upgrading
require(proxyAdmin.getVersion() == latestVersion, "Cannot upgrade from old version");