(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.
Affected Assets
https://github.com/CodeHawks-Contests/2025-05-hawk-high/blob/main/src/LevelTwo.sol#L28
2025-05-hawk-high/src/LevelOne.sol::graduateAndUpgrade()
(signature provided, allows principal to upgrade)
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()
:
The reinitializer(2)
modifier checks !_initializing && _initialized < 2
. If _initialized
is 1 (from LevelOne
), this check passes.
_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.
Manual Review
AI Assistance for impact analysis
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).
The system doesn't implement UUPS properly.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.