Hawk High

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

[M-2] Missing _disableInitializers() in Both Contracts Exposes Implementation to Initialization Attacks

Severity

Medium

Impact

Both LevelOne and LevelTwo contracts lack the critical _disableInitializers() protection mechanism in their constructors. This omission exposes the implementation contracts to potential initialization attacks where an attacker could:

  1. Directly initialize the implementation contract (not the proxy)

  2. Potentially interfere with the upgrade process

  3. Manipulate the implementation contract's state in ways that could impact the proxy's behavior

  4. In extreme cases, execute a destructive operation like selfdestruct on the implementation

This vulnerability affects both implementation contracts in the UUPS upgrade chain and could lead to system disruption or fund loss.

Additionally, the initialize() function accepts an arbitrary USDC address parameter with minimal validation (only checking for non-zero address). An attacker who initializes the implementation could specify a malicious ERC20 contract address instead of the real USDC. This would create a denial-of-service condition for the entire school system:

  1. Students attempting to enroll with real USDC would have their transactions revert, as their approvals would be for the real USDC contract while the system is trying to use the malicious one

  2. The school system would appear properly deployed but would be completely unusable

  3. When the system is upgraded, teachers and the principal would receive worthless tokens as payment instead of real USDC

  4. The entire economic model of the school would be compromised

Description

Upgradeable contracts using the UUPS pattern have two components:

  1. A proxy contract that stores state

  2. An implementation contract that contains logic

Without proper protection, an attacker can call initialization functions directly on the implementation contract rather than through the proxy. In the Hawk High system:

  • LevelOne.sol has the initialize() function that lacks implementation-level protection

  • LevelTwo.sol has the graduate() function with reinitializer(2) that also lacks protection

OpenZeppelin's best practice is to include a constructor with _disableInitializers() in all upgradeable implementation contracts to prevent direct initialization. Both contracts are missing this critical security mechanism:

// Missing from both contracts
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

The lack of proper initialization protection combined with insufficient token validation creates a particularly concerning scenario:

function initialize(address _principal, uint256 _schoolFees, address _usdcAddress) public initializer {
// Only checks for zero address, not valid USDC
if (_usdcAddress == address(0)) {
revert HH__ZeroAddress();
}
// ...
usdc = IERC20(_usdcAddress);
// ...
}

An attacker could:

  1. Deploy their own malicious ERC20 token that implements the IERC20 interface

  2. Directly initialize the implementation contract with their malicious token address

  3. The contract would accept this token as it passes the minimal validation checks

  4. All genuine USDC operations attempted through the contract would fail

  5. The contract would be unusable for legitimate participants

Since the school system handles funds (USDC), this vulnerability could potentially lead to financial loss or complete system failure if an attacker disrupts the enrollment process or manipulates the implementation contract's state.

Proof of Concept

An attacker could execute:

  1. Deploy a malicious ERC20 token:

contract MaliciousUSDC is IERC20 {
// Implement IERC20 interface but with malicious behavior
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
// Always revert or return false when transferring from anyone but the attacker
if (from != attacker) revert("Transfer failed");
return true;
}
// Other malicious implementations...
}
  1. Directly initialize the implementation:

address maliciousToken = address(new MaliciousUSDC());
LevelOne implementation = LevelOne(implementationAddress);
implementation.initialize(attacker, 100, maliciousToken);
  1. Now the implementation has been initialized with a malicious token. Any upgrade processes that reference the implementation will be affected, and the malicious token will prevent normal operation.

Recommended Mitigation

Add a constructor with _disableInitializers() to both LevelOne and LevelTwo contracts:

// LevelOne.sol
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }
// LevelTwo.sol
+ /// @custom:oz-upgrades-unsafe-allow constructor
+ constructor() {
+ _disableInitializers();
+ }

Additionally, implement stronger validation for the USDC address in the initialize function:

function initialize(address _principal, uint256 _schoolFees, address _usdcAddress) public initializer {
if (_principal == address(0)) {
revert HH__ZeroAddress();
}
if (_schoolFees == 0) {
revert HH__ZeroValue();
}
if (_usdcAddress == address(0)) {
revert HH__ZeroAddress();
}
+ // The most secure approach is to hardcode the official USDC address per network
+ require(_usdcAddress == 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, "Not official USDC"); // Ethereum Mainnet
+ // For other networks, appropriate addresses should be used or a trusted registry
principal = _principal;
schoolFees = _schoolFees;
usdc = IERC20(_usdcAddress);
__UUPSUpgradeable_init();
}

The special comment /// @custom:oz-upgrades-unsafe-allow constructor is required to tell the OpenZeppelin Upgrades plugin that this constructor is intentionally included and safe.

References

Updates

Lead Judging Commences

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

contract can be re-initialized

The system can be re-initialized by an attacker and its integrity tampered with due to lack of `disableInitializer()`

Support

FAQs

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