Hawk High

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

High: Missing cutOffScore enforcement allows ineligible students to graduate

LevelOne::graduateAndUpgrade() upgrades the proxy to LevelTwo and finalises the academic session but never checks each student’s studentScore against cutOffScore. Because cutOffScore is only set (in LevelOne::startSession) and never read/compared, the invariant "Any student whose score is below the cut‑off must not be upgraded" is violated.

Impact

  • Logical integrity broken — students who failed the session still exist in LevelTwo, undermining the core business rule of the system.

  • Follow-on bugs — later Level Two logic may assume every student passed the cut‑off, leading to incorrect calculations or privilege escalation.

Severity is marked High as one of the fundamental business rules is bypassed.

Proof of Concept

Add to LevelOneAndGraduateTest.t.sol and run `forge test --match-test test_student_below_cutoff_still_graduates -vvv` => The test will pass, proving that the student Harriet is still present in LevelTwo despite having a score of 60 (cutoff is 70).

function test_student_below_cutoff_still_graduates()
public
schoolInSession
{
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
// Lower Harriet’s score from 100 → 60 (< cutOffScore)
vm.startPrank(alice);
for (uint256 i; i < 4; i++) {
vm.warp(block.timestamp + 1 weeks);
levelOneProxy.giveReview(harriet, false);
}
vm.stopPrank();
assertLt(
levelOneProxy.studentScore(harriet),
levelOneProxy.cutOffScore()
);
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
LevelTwo levelTwoProxy = LevelTwo(proxyAddress);
// Harriet should not be a student anymore; assert should fail but passes
assertTrue(levelTwoProxy.isStudent(harriet));
}

Recommended Mitigation

Multiple mitigation options exist: either revert or automatically expel students who are under the cutoff at the time of the upgrade/graduation.

  1. Revert if any student is below the cut‑off
    Add a pre‑check before the upgrade:

    function _checkCutOff() internal view {
    for (uint256 i; i < listOfStudents.length; ++i) {
    require(studentScore[listOfStudents[i]] >= cutOffScore,
    "Some students below cut-off");
    }
    }
    // call _checkCutOff() inside graduateAndUpgrade()
  2. Automatic expulsion option
    Iterate through listOfStudents and levelOne::expel() every address with a score below cutOffScore before authorising the upgrade. This keeps the single‑transaction UX while upholding the invariant:

    function _expelFailures() internal {
    uint256 i = 0;
    while (i < listOfStudents.length) {
    address s = listOfStudents[i];
    if (studentScore[s] < cutOffScore) {
    expel(s);
    } else {
    ++i;
    }
    }
    }
    // call _expelFailures() inside graduateAndUpgrade()

Either approach ensures no student with a score lower than the cutoff can pass into Level Two.

Updates

Lead Judging Commences

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

cut-off criteria not applied

All students are graduated when the graduation function is called as the cut-off criteria is not applied.

Support

FAQs

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