Hawk High

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

Incorrect UUPS Upgrade Mechanism Leading to Failed Upgrades

Summary

The graduateAndUpgrade function in the LevelOne contract misuses the UUPS pattern by calling _authorizeUpgrade without executing upgradeTo or upgradeToAndCall. This prevents the contract from upgrading to LevelTwo, leaving it on outdated logic and risking fund mismanagement or loss of functionality.

Vulnerability Details

The LevelOne contract uses OpenZeppelin’s UUPSUpgradeable for upgradeability. The graduateAndUpgrade function, restricted to the principal, authorizes an upgrade to _levelTwo via _authorizeUpgrade and distributes bursary funds but fails to call upgradeTo or upgradeToAndCall. Per OpenZeppelin’s documentation, _authorizeUpgrade only checks permissions, and the upgrade requires a separate upgradeTo call on the proxy. This omission means the proxy remains on LevelOne logic, and LevelTwo’s reinitializers (e.g., graduate) are never executed. The function also lacks the onlyProxy modifier and does not emit a Graduated event for transparency.

https://github.com/CodeHawks-Contests/2025-05-hawk-high/blob/3a7251910c31739505a8699c7a0fc1b7de2c30b5/src/LevelOne.sol#L305

Add this function to LevelOneAndGraduateTest:

function test_graduateAndUpgradeFailsToUpgrade() public schoolInSession {
address proxyAddr = address(levelOneProxy);
// Get current implementation via storage slot
bytes32 slot = bytes32(uint256(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc));
address initialImplementation = address(uint160(uint256(vm.load(proxyAddr, slot))));
// Deploy LevelTwo
LevelTwo levelTwo = new LevelTwo();
address levelTwoAddress = address(levelTwo);
// Call graduateAndUpgrade as principal
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoAddress, "");
// Check implementation remains unchanged
address finalImplementation = address(uint160(uint256(vm.load(proxyAddr, slot))));
assertEq(
finalImplementation,
initialImplementation,
"Proxy implementation should remain unchanged (vulnerability confirmed)"
);
}

run: forge test --match-test test_graduateAndUpgradeFailsToUpgrade -vvv

Impact

High impact due to:

  • Persistent old logic, potentially with vulnerabilities or missing features.

  • Possible fund mismanagement if LevelTwo is needed for proper fund handling.

  • Undetectable failure due to no events or state changes, risking prolonged issues.

Tools Used

Recommendations

Add Upgrade Call: Modify graduateAndUpgrade to call upgradeTo(_levelTwo) on the proxy after _authorizeUpgrade.

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
...
_authorizeUpgrade(_levelTwo);
// Call upgradeTo(address)
(bool success, ) = address(this).call(abi.encodeWithSignature("upgradeTo(address)", _levelTwo));
require(success, "Upgrade failed");
// Distribute funds
...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 17 days 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.