Hawk High

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

Upgrade Bypass via Direct `upgradeTo()` call

Summary

The contract allows the principal to upgrade the implementation through the UUPS proxy. While the graduateAndUpgrade() function includes checks and logic for safe upgrades, the current setup does not require upgrades to go through it. This creates a gap that lets the principal upgrade the contract without triggering important steps like fund distribution.

Vulnerability Details

The contract uses OpenZeppelin’s UUPS upgrade pattern. It restricts upgrades with the _authorizeUpgrade() function, which checks if the caller is the principal:

function _authorizeUpgrade(address newImplementation) internal override onlyPrincipal {}

This function protects the upgrade path but doesn’t control how or when the upgrade happens. As a result, the principal can call upgradeTo() directly on the proxy. This bypasses graduateAndUpgrade(), which skipping actions like sending payments and running setup logic. However, The upgrade would still go through, but the expected outcomes would not happen.

Impact

This gap lets the principal skip important parts of the upgrade process. If the principal or an attacker with access calls upgradeTo() directly:

  • Teachers and the principal won’t get paid

  • Setup functions in the new contract won’t run

  • Events that mark the upgrade won’t be emitted

  • Users may see broken or missing data after the upgrade

Tools Used

  • Manual review of the contract

  • Understanding of the UUPS and ERC1967 proxy patterns

Recommendations

Limit upgrades to the graduateAndUpgrade() function by tracking approved upgrades. We can do this by storing the allowed address and checking it in _authorizeUpgrade():

address private _pendingUpgrade;
function graduateAndUpgrade(address _levelTwo, bytes memory data) public onlyPrincipal {
if (_levelTwo == address(0)) revert HH__ZeroAddress();
_pendingUpgrade = _levelTwo;
_upgradeToAndCall(_levelTwo, data);
// include payout logic here
}
function _authorizeUpgrade(address newImplementation) internal override onlyPrincipal {
require(newImplementation == _pendingUpgrade, "Unauthorized upgrade");
_pendingUpgrade = address(0);
}
Updates

Lead Judging Commences

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