Hawk High

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

High: LevelOne::graduateAndUpgrade – bursary Not Updated After Wage Payouts can Cause DoS on Subsequent Calls

Description

The LevelOne::graduateAndUpgrade function distributes the bursary to teachers and the principal. However, the function does not update the bursary state variable after these payouts are made. This violates the project's documented invariant:

Invariant: “Remaining 60% should reflect in the bursary after upgrade.”

As a result, the bursary variable retains the full pre-distribution value, even though 40% has already been transferred out of the contract. This may seem like a misaccounting issue at first, but it introduces a critical denial-of-service condition.

Impact

If graduateAndUpgrade() is called again (e.g. as part of a planned re-upgrade (after another 4-week term)), the function will recalculate payouts based on the stale bursary value. Since the contract no longer holds the full amount, ERC20 transfers will fail, and the entire transaction will revert. This locks up the upgrade path and prevents the principal from initiating further transitions, violating protocol liveness and payout guarantees.

Proof of Concept

Add to LevelOneAndGraduateTest.t.sol and run forge test --match-test test_bursary_updated_after_graduation -vvv => The test will fail, proving that the bursary and the balance don't match after the upgrade/payouts.

function test_bursary_updated_after_graduation() public schoolInSession {
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
// Before upgrade, bursary and actual balance match
assertEq(levelOneProxy.bursary(), usdc.balanceOf(proxyAddress));
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
LevelTwo levelTwoProxy = LevelTwo(proxyAddress);
// Internal bursary is not updated, but the balance went down after payouts
// After upgrade/payout, bursary and actual balance don't match
assertEq(levelTwoProxy.bursary(), usdc.balanceOf(proxyAddress)); // ❌ fails
}

Recommended Mitigation

Update the bursary variable after payouts to reflect only the remaining 60%:

uint256 totalTeacherWage = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
...
bursary -= (totalTeacherWage + principalPay);

This ensures bursary always represents the actual undistributed funds, as required by the protocol's invariant and prevents denial of service on repeated or retry calls.

Updates

Lead Judging Commences

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

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