Hawk High

First Flight #39
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Loss of Further UUPS Upgradeability in LevelTwo Contract

Summary

This report identifies a critical design flaw in the LevelTwo.sol contract. While the LevelOne.sol contract is correctly configured for UUPS (Universal Upgradeable Proxy Standard) upgrades, the LevelTwo.sol contract, intended as its upgrade target, does not implement the necessary UUPS mechanisms. This oversight means that once the system is upgraded to LevelTwo.sol, no further UUPS-based upgrades to subsequent versions (e.g., a hypothetical LevelThree) will be possible through the standard UUPS proxy upgradeToAndCall mechanism.

Vulnerability Details

The UUPS upgrade pattern requires the upgrade authorization logic (typically an _authorizeUpgrade function) to reside within the implementation contract itself. The proxy contract delegates the authorization check to the current implementation before switching to a new one.

  • LevelOne.sol Implementation:
    This contract correctly implements UUPS upgradeability by:

    1. Inheriting from UUPSUpgradeable.

    2. Overriding the _authorizeUpgrade(address newImplementation) internal override onlyPrincipal {} function, restricting upgrade authorization to the principal.

  • LevelTwo.sol Implementation:
    This contract, as provided, fails to maintain UUPS compliance:

    1. It only inherits from Initializable and does not inherit from UUPSUpgradeable.

      // From src/LevelTwo.sol
      contract LevelTwo is Initializable {
      // ...
      }
    2. Consequently, it does not implement or override the _authorizeUpgrade function.

This omission breaks the chain of UUPS upgradeability.

Proof Of Concept

  1. Initial State: The proxy contract points to LevelOne.sol as its implementation.

  2. Upgrade to LevelTwo: The principal calls graduateAndUpgrade(address _levelTwo, bytes memory) on LevelOne.sol.

    • Inside graduateAndUpgrade, LevelOne.sol's _authorizeUpgrade function is called, which succeeds because msg.sender is the principal.

    • The proxy's upgradeToAndCall function (called externally or by the principal after _authorizeUpgrade sets the stage) successfully changes the implementation pointer to LevelTwo.sol.

  3. Attempted Upgrade from LevelTwo to LevelThree (Hypothetical):

    • Sometime later, a LevelThree.sol contract is developed.

    • The principal attempts to upgrade the proxy from LevelTwo.sol to LevelThree.sol by calling the proxy's upgradeToAndCall function.

    • The proxy will attempt to call the _authorizeUpgrade function on its current implementation, which is now LevelTwo.sol.

    • Since LevelTwo.sol does not define an _authorizeUpgrade function (nor does it inherit UUPSUpgradeable which provides a base for it), the call will fail (e.g., function not found, or revert if a non-matching selector is somehow hit).

    • The upgrade to LevelThree.sol is thus prevented.

Impact

The impact of this vulnerability is High:

  • Loss of Future Upgradeability: The primary purpose of using an upgradeable contract system is to allow for future modifications and bug fixes. This vulnerability negates that benefit beyond LevelTwo.sol.

  • Contradicts Project Intent: The project documentation states, "At the end of the school session (4 weeks), the system is upgraded to a new one," implying a potentially ongoing cycle of upgrades. This vulnerability breaks that cycle.

  • Increased Migration Costs: If future changes are needed after upgrading to LevelTwo.sol, a full data migration to a new proxy system would be required, which is significantly more complex, costly, and risky than a standard UUPS upgrade.

Tools Used

  • Manual Code Review

  • Analysis of OpenZeppelin UUPSUpgradeable contract patterns

Recommendations

To ensure continued upgradeability, the LevelTwo.sol contract must be made UUPS-compliant.

  1. Inherit UUPSUpgradeable: Modify LevelTwo.sol to inherit from UUPSUpgradeable in addition to Initializable.

  2. Implement _authorizeUpgrade: Add an _authorizeUpgrade function to LevelTwo.sol. This function should contain the logic to authorize subsequent upgrades, typically restricting it to the principal (or another designated upgrade admin role).

  3. Call __UUPSUpgradeable_init: Ensure that the reinitializer function (or a dedicated initializer for LevelTwo if it were the first deployment) calls __UUPSUpgradeable_init() if it's not already implicitly handled by the Initializable chain and UUPS versioning. However, since graduate() is a reinitializer(2), the UUPS state is already initialized by LevelOne.sol. The key is the presence of _authorizeUpgrade in LevelTwo.sol.

Proposed Code Change for LevelTwo.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
+import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
-
-contract LevelTwo is Initializable {
+contract LevelTwo is Initializable, UUPSUpgradeable {
using SafeERC20 for IERC20;
address principal;
@@ -23,7 +24,23 @@
IERC20 usdc;
- function graduate() public reinitializer(2) {}
+ // It's good practice to define custom errors if they are to be used.
+ error HH__NotPrincipal(); // Assuming this error is defined or imported if used by onlyPrincipal
+
+ modifier onlyPrincipal() {
+ // This assumes 'principal' is correctly set and inherited from LevelOne's storage.
+ // If LevelTwo were to change how the principal is determined, this logic would need adjustment.
+ if (msg.sender != principal) {
+ revert HH__NotPrincipal();
+ }
+ _;
+ }
+
+ function graduate() public reinitializer(2) {
+ // If LevelTwo is the first version, __UUPSUpgradeable_init() would be called here.
+ // As a reinitializer for version 2, UUPS is already initialized by LevelOne.
+ // No specific UUPS re-initialization call is needed here for the UUPS mechanism itself.
+ }
function getPrincipal() external view returns (address) {
return principal;
@@ -49,4 +66,10 @@
function getListOfTeachers() external view returns (address[] memory) {
return listOfTeachers;
}
+
+ /**
+ * @dev Authorizes an upgrade to a new implementation.
+ * Only the principal can authorize an upgrade.
+ */
+ function _authorizeUpgrade(address newImplementation) internal override onlyPrincipal {}
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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