Hawk High

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

Bursary Not Adjusted After Payments

Summary

The graduateAndUpgrade function transfers funds to the principal and teachers based on the bursary amount, but fails to update the bursary state variable after these transfers. This leads to two serious consequences:

  • Reentrancy Risk: An attacker can re-enter the function before the bursary is updated, draining the contract repeatedly.

  • Incorrect Accounting: The bursary value remains at 100%, misrepresenting the actual funds left and breaking protocol invariants.


Vulnerability Details

This is a classic case of violating the Checks-Effects-Interactions pattern. By not updating the state (bursary) before external calls, the function allows for both inaccurate accounting and potential reentrancy exploits.

Exploit Scenario:

  • bursary = 1000 USDC

  • On first call:

    • 50 USDC sent to principal (5%)

    • 350 USDC sent to teachers (35%)

    • bursary remains 1000 USDC → wrong

  • On second call:

    • Transfers again calculated based on full 1000 USDC

    • Another 400 USDC drained

  • Result: 800 USDC withdrawn instead of only 400, leaving only 200 USDC instead of the expected 600

Vulnerable Code:

uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION; // 5%
uint256 teachersShare = (bursary * TEACHER_WAGE) / PRECISION; // 35%
// Transfer without updating bursary ❌
usdc.safeTransfer(principal, principalPay);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
// bursary remains unchanged ❌

Impact

  • Protocol Security: Allows for repeated draining of funds using stale bursary values.

  • Accounting Errors: Leads to double-counting and misrepresentation of funds.

  • Loss of Funds: Users can withdraw more than intended due to repeated calculations based on the original bursary.


Tools Used

Manual review


Recommendations

Fix Implementation: Apply Checks-Effects-Interactions

Update the bursary state before making any external transfers:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
// ... session and review checks ...
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
uint256 teachersShare = (bursary * TEACHER_WAGE) / PRECISION;
// ========== CRITICAL FIX ========== //
// Deduct funds from bursary first (effects)
bursary -= (principalPay + teachersShare); // ✅
// ================================== //
// Now perform external calls (interactions)
usdc.safeTransfer(principal, principalPay);
if (totalTeachers > 0) {
uint256 payPerTeacher = teachersShare / totalTeachers;
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
}
// ... upgrade logic ...
}

Key Fixes

  • Checks-Effects-Interactions Pattern:

    • Always update internal state (bursary) before calling external functions.

  • Accurate Bursary Tracking:

    • The updated bursary now correctly reflects the remaining 60% after payments.

  • Reentrancy Resistance:

    • Prevents re-entrant calls from recalculating based on outdated values.


Why This Matters

  • Financial Integrity: Maintains the correct bursary value post-payment to ensure smooth upgrades and accurate fund flow.

  • Protocol Security: Eliminates the risk of fund draining via reentrancy or duplicate withdrawals.

  • Invariant Compliance: Complies with the protocol’s design, where 60% of the bursary is preserved post-upgrade for future use.

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

Support

FAQs

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