Hawk High

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

The `initialize()` function lacks of front-run protection, allowing unauthorized initialization.

Summary

The initialize() function in LevelOne contract is publicly accessible and lacks any form of access control or front-run protection. This allows any external actor to front-run the intended deployer and invoke the initialize() function first, setting arbitrary initialization parameters and taking control over contract-critical state.

Vulnerability Details

In upgradeable contract systems using proxy patterns, the initialize() function replaces the constructor and is meant to be called once—typically immediately after the proxy is deployed. If this function is left unprotected, malicious actors monitoring mempool transactions could front-run the deployer and call initialize() first.

In this case, the initialize() function accepts parameters such as _principal, _schoolFees, and _usdcAddress, which directly influence the contract’s core logic and access control. Since the function uses the initializer modifier, it can only be called once per contract instance—meaning once a malicious actor calls it, the legitimate deployer will be permanently blocked from initializing the contract correctly.

This effectively grants the attacker permanent control over roles or funds, or renders the contract unusable.

Impact

Unauthorized actors can set _principal, _schoolFees, and _usdcAddress arbitrarily.

  • Legitimate deployer is permanently blocked from initializing the contract.

  • Full control of the contract logic may be hijacked by malicious actors.

  • Potential financial or operational loss depending on contract function.

Tools Used

  • Manual code review

Recommendations

To prevent front-running of the initialize() function and ensure the contract is initialized only by the intended party, consider one of the below solutions:

  • Option 1: initialize() function checks the invoker address is known trusted address. (not recommended, hardcoded values brings less flexability)

address public constant TRUSTED_DEPLOYER = 0x123...abc;
function initialize(...) public initializer {
require(msg.sender == TRUSTED_DEPLOYER, "Not authorized");
if (_principal == address(0)) {
revert HH__ZeroAddress();
}
...
}
  • Option 2: When using ERC1967Proxy, ensure that the initialize() function is invoked during deployment.

    By encoding the initialization call in the _data argument passed to the proxy’s constructor. This guarantees that initialization occurs in the same transaction as the proxy deployment, eliminating any front-run risk.

// script/DeployLevelOne.s.sol
function deployLevelOne() public returns (address) {
usdc = new MockUSDC();
vm.startBroadcast();
levelOneImplementation = new LevelOne();
// Bad practice!
// proxy = new ERC1967Proxy(address(levelOneImplementation), "");
// LevelOne(address(proxy)).initialize(principal, schoolFees, address(usdc));
// Good practice!
bytes memory initialize_calldata = abi.encodeWithSelector(
levelOneImplementation.initialize.selector,
principal,
schoolFees,
address(usdc)
);
proxy = new ERC1967Proxy(address(levelOneImplementation), initialize_calldata);
vm.stopBroadcast();
return address(proxy);
}

Updates

Lead Judging Commences

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

Give us feedback!