Hawk High

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

Locked Bursary Funds

Summary

60% of collected school fees (bursary) are permanently locked in the contract with no mechanism to transfer them during upgrade.

Vulnerability Details

Root Cause: The graduateAndUpgrade function in LevelOne.sol is intended to only distributes 40% of the bursary, leaving 60% locked in the contract permanently.

Initial State:

  • Contract deployed with bursary funds

  • Ready for upgrade to LevelTwo

Attack Flow:

  1. Students pay school fees, increasing bursary

  2. Principal triggers graduateAndUpgrade

  3. Only 40% distributed (35% teachers + 5% principal)

  4. 60% remains locked forever

  5. New contract gets deployed without access to old funds

Impact

  • Loss of 60% of all collected fees

  • Breaks core economic model

  • Protocol becomes financially unsustainable

  • Students and school lose significant funds


POC

In the POC i left out the teachers payment because the code contains a bug that transfer 35% of the bursary to each teacher rather than sharing the 35% equally among all teachers.

but the POC is sufficient enough and proves that funds are locked up in the contract.

function testLockedBursaryFunds() public {
// Setup - Use existing schoolFees from state variable
address student1 = makeAddr("student1");
usdc.mint(student1, schoolFees);
vm.startPrank(student1);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// Initial bursary check
assertEq(levelOneProxy.bursary(), schoolFees);
// Deploy LevelTwo implementation for upgrade
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
// Prepare upgrade data
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
// Upgrade
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
// Calculate expected balances
// Principal gets 5% (PRINCIPAL_WAGE)
// No teachers added, so no teacher payment
// 95% should remain locked
uint256 remainingBursary = (schoolFees * 95) / 100; // 95% since only principal payment is made
// Check balances
assertEq(usdc.balanceOf(address(levelOneProxy)), remainingBursary);
assertEq(usdc.balanceOf(levelTwoImplementationAddress), 0); // No funds transferred
}
assertEq(usdc.balanceOf(levelTwoImplementationAddress), 0); // No funds transferred

Tools Used

  • Manual Review

  • Foundry Testing Framework

Recommendations

  1. Modify graduateAndUpgrade to transfer remaining funds:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
// ...existing code...
// Transfer remaining 60% to new implementation
uint256 remainingBursary = (bursary * 60) / PRECISION;
usdc.safeTransfer(_levelTwo, remainingBursary);
// ...existing code...
}

2- Add receiving logic in LevelTwo:

function graduate() public reinitializer(2) {// ...existing code...bursary = IERC20(usdc).balanceOf(address(this));}

3 - consider adding a function that only the principal or neccessary school admin can use to transfer funds from the bursary or the contract in other to repurpose the funds or use it for other meaning things.

Updates

Lead Judging Commences

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

stuck funds in system

Funds are stuck in `LevelOne()` contract after upgrade.

Support

FAQs

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