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.
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.
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.
Update the bursary variable after payouts to reflect only the remaining 60%:
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.
The bursary is not updated after wages have been paid in `graduateAndUpgrade()` function
The bursary is not updated after wages have been paid in `graduateAndUpgrade()` function
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.