Hawk High

First Flight #39
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Med) Publicly Callable Reinitializer Function in Upgraded Contract - Allows any user to advance the contract's initialization state after an upgrade, potentially blocking subsequent intended initialization steps by the principal.

Summary

(Med) Publicly Callable Reinitializer Function in Upgraded Contract -
Allows any user to advance the contract's initialization state after an upgrade, potentially blocking subsequent intended initialization steps by the principal.

Vulnerability Details

Affected Assets

https://github.com/CodeHawks-Contests/2025-05-hawk-high/blob/main/src/LevelTwo.sol#L28

function graduate() public reinitializer(2) {}
  1. 2025-05-hawk-high/src/LevelOne.sol::graduateAndUpgrade() (signature provided, allows principal to upgrade)

Impact

The LevelOne contract is UUPS upgradeable, meaning its implementation can be changed by the principal. When LevelOne is upgraded to LevelTwo, the underlying proxy contract will use LevelTwo's code while maintaining its existing storage.

The LevelTwo contract has a public function graduate() which is also a reinitializer(2). The Initializable contract (from OpenZeppelin) uses a state variable (typically _initialized) to track the initialization version.

  • LevelOne.initialize() (using initializer) sets _initialized to 1.

  • After upgrading the proxy to LevelTwo, the _initialized variable in storage remains 1.

  • The reinitializer(2) modifier on LevelTwo.graduate() allows this function to be called if _initialized < 2. Upon successful execution, it sets _initialized to 2.

  • Because LevelTwo.graduate() is public, anyone can call it on the proxy after the upgrade to LevelTwo has occurred.

If an attacker calls proxy.graduate():

  1. The reinitializer(2) modifier checks !_initializing && _initialized < 2. If _initialized is 1 (from LevelOne), this check passes.

  2. _initialized is set to 2.

If the principal intended to call a different function on LevelTwo that also uses reinitializer(2) (e.g., an initializeLevelTwoSpecifics() reinitializer(2) function) as a separate step after the upgrade, the attacker's prior call to graduate() would have already set _initialized to 2. The principal's subsequent call to initializeLevelTwoSpecifics() would then fail because the reinitializer(2) check _initialized < 2 (i.e., 2 < 2) would be false.

This effectively allows an attacker to perform a limited Denial of Service (DoS) against the principal's intended post-upgrade initialization sequence for version 2.

  • Likelihood of Exploitation:

    • Medium. The vulnerability requires the principal to upgrade to LevelTwo without immediately calling a reinitializer(2) function within the data payload of the upgrade call. Attackers (e.g., MEV bots) can monitor for upgrade transactions and front-run or quickly follow up with a call to the public graduate() function. The public nature of the function and the clear pattern of UUPS upgrades make it discoverable.

Tools Used

  1. Manual Review

  2. AI Assistance for impact analysis

Recommendations

Access Control for LevelTwo.graduate():
The simplest fix is to restrict who can call LevelTwo.graduate(). If it's intended to be part of the principal's upgrade and initialization flow, it should be protected, for example, with an onlyPrincipal modifier (assuming principal state is correctly set up or passed into LevelTwo's own initializer).

Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

failed upgrade

The system doesn't implement UUPS properly.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.