Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Noisy Quartz Bird - Storage Manipulation Attack using _version in initialization process #59

Open
sherlock-admin2 opened this issue Jan 31, 2025 · 0 comments
Labels
Sponsor Disputed The sponsor disputed this issue's validity

Comments

@sherlock-admin2
Copy link
Contributor

Noisy Quartz Bird

High

Storage Manipulation Attack using _version in initialization process

Summary

https://github.com/sherlock-audit/2025-01-perennial-v2-4-update/blob/main/perennial-v2/packages/core/contracts/Market.sol#L120-L128

/// @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

PoC

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.

Recommendation

How to Fix This?

  1. 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.

  1. Verify _version Before Upgrading
require(proxyAdmin.getVersion() == latestVersion, "Cannot upgrade from old version");
@sherlock-admin3 sherlock-admin3 added the Sponsor Disputed The sponsor disputed this issue's validity label Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Sponsor Disputed The sponsor disputed this issue's validity
Projects
None yet
Development

No branches or pull requests

2 participants