Hawk High

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

ETH handling issue:Unrecoverable ETH and Unintended USDC in Contract

Summary

The LevelOne and LevelTwo contracts lack mechanisms to handle or recover ETH sent to them and have no way to recover USDC sent outside of the enrollment process. Any ETH or unintended USDC sent to these contracts becomes permanently trapped with no recovery mechanism.

similar example:-

  • Issue: Uniswap v1 had no mechanism to handle ERC20 tokens sent directly to its contracts (outside swaps). Tokens like USDC or DAI sent via direct transfers were permanently stuck.

Vulnerability Details

The LevelOne contract has no functions to handle incoming ETH transfers:

// LevelOne.sol - Missing receive() and fallback() functions
contract LevelOne is Initializable, UUPSUpgradeable {
using SafeERC20 for IERC20;
// No ETH handling functions exist
// No receive() function
// No fallback() function
}

Similarly, LevelTwo also lacks ETH handling mechanisms:

// LevelTwo.sol - Missing receive() and fallback() functions
contract LevelTwo is Initializable {
using SafeERC20 for IERC20;
// No ETH handling functions exist
}

Additionally, there's no mechanism to handle or recover USDC tokens that might be directly sent to either contract outside of the normal enrollment flow. The only USDC interaction is through the enroll() function:

// LevelOne.sol:246-259
function enroll() external notYetInSession {
if (isTeacher[msg.sender] || msg.sender == principal) {
revert HH__NotAllowed();
}
if (isStudent[msg.sender]) {
revert HH__StudentExists();
}
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
emit Enrolled(msg.sender);
}

If USDC is sent directly via transfer or transferFrom rather than through the enrollment process, those tokens won't be accounted for in the bursary and would remain stuck in the contract.

Impact

It leads to permanent loss of any mistakenly sent ETH

  • It causes unaccounted USDC to be stuck in the contract

  • The vulnerability exists throughout the contract lifecycle

  • The issue affects both implementations (LevelOne and LevelTwo)

Tools Used

Manual code review

Recommendations

  1. Add ETH Handling Functions:

    // Add to LevelOne.sol
    receive() external payable {
    // Optional: emit event for tracking
    emit EthReceived(msg.sender, msg.value);
    }


** 2.Add Similar Functions to LevelTwo:**

// Add to LevelTwo.solreceive() external payable {// Optional: emit event for trackingemit EthReceived(msg.sender, msg.value);}
modifier onlyPrincipal() {
require(msg.sender == principal, "Not authorized");
\_;
}
receive() external payable {
// Optional: emit event for tracking
emit EthReceived(msg.sender, msg.value);
}
function withdrawEth() external onlyPrincipal {
uint256 balance = address(this).balance;
require(balance > 0, "No ETH to withdraw");
(bool success, ) = principal.call{value: balance}("");
require(success, "ETH transfer failed");
}
function recoverUSDC() external onlyPrincipal {
uint256 balance = usdc.balanceOf(address(this));
if (balance > 0) {
usdc.safeTransfer(principal, balance);
}
}

Updates

Lead Judging Commences

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

Support

FAQs

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