Hawk High

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

Bursary Balance Update Failure in graduateAndUpgrade" bursary tracking issues contract upgrade risks more specific title

Summary

The graduateAndUpgrade function in LevelOne.sol fails to update the bursary variable after paying teachers and the principal, causing an inflated balance. This misrepresents available funds, risking errors or exploits in future contract upgrades. Updating bursary by subtracting total payouts fixes the issue.

Vulnerability Details

The LevelOne::graduateAndUpgrade function processes payouts to teachers (teacherWage * listOfTeachers.length) and the principal (principalWage) but does not update the LevelOne::bursary state variable, which tracks the total student fees collected. This results in an inflated bursary value that does not reflect the actual remaining funds after payouts. The bug can lead to incorrect financial accounting and issues in future contract upgrades, as the next implementation may rely on an accurate bursary value for fund allocation or logic.

Proof of Concept:

  • Students pay fees, increasing bursary (e.g., bursary = 100 ETH).

  • graduateAndUpgrade pays 10 ETH to teachers and 5 ETH to the principal (total payout = 15 ETH).

  • Contract balance decreases by 15 ETH, but bursary remains 100 ETH instead of updating to 85 ETH.

  • Future functions or upgraded contracts read 100 ETH, leading to incorrect assumptions about available funds.

PoC

// Test to demonstrate that the bursary variable is not updated in graduateAndUpgrade
function test_bursaryDoesNotUpdateInGraduateAndUpgrade() public {
// Setup: Principal adds two teachers (Alice and Bob)
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
vm.stopPrank();
// Setup: Six students (Clara, Dan, Eli, Fin, Grey, Harriet) enroll and pay fees
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(eli);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(fin);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(grey);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(harriet);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// Setup: Principal starts session with cut-off score of 70
vm.prank(principal);
levelOneProxy.startSession(70);
vm.stopPrank();
// Simulate passage of time (4 weeks) to allow graduation
vm.warp(block.timestamp + 4 weeks);
// Record bursary balance before calling graduateAndUpgrade
uint256 bursaryBalanceBefore = levelOneProxy.bursary();
// Setup: Deploy LevelTwo implementation and prepare upgrade data
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
// Execute graduateAndUpgrade as principal, which should pay teachers and principal
vm.startPrank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
vm.stopPrank();
// Record bursary balance after graduateAndUpgrade
uint256 bursaryBalanceAfter = levelOneProxy.bursary();
// Log balances for debugging
console2.log("before:", bursaryBalanceBefore, "after:", bursaryBalanceAfter);
// Assert that bursary balance remains unchanged, proving the vulnerability
assertEq(bursaryBalanceBefore, bursaryBalanceAfter, "Bursary balance should not change due to missing update in graduateAndUpgrade");
}

Impact

  1. Incorrect Accounting: bursary misrepresents the contract’s available funds, potentially causing errors in functions or audits that depend on it.

  2. Upgrade Risks: If LevelOne uses a proxy pattern for upgrades, the inflated bursary could lead to over-allocation, logic errors, or exploits in the new implementation.

  3. Security: Incorrect bursary values may enable unauthorized withdrawals or miscalculations if used in access control or fund distribution logic.

Tools Used

Manual Review, Foundry

Recommendations

After payouts in graduateAndUpgrade, update bursary to reflect the remaining balance:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
.
.
.
uint256 totalTeachers = listOfTeachers.length();
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
+ bursary = bursary - ((payPerTeacher * totalTeachers) + principalPay);
.
.
}
Updates

Lead Judging Commences

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

bursary not updated

The bursary is not updated after wages have been paid in `graduateAndUpgrade()` function

Support

FAQs

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

Give us feedback!