Hawk High

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

No Enforcement of cutOffScore Allows Unqualified Students to Graduate (Invariant Broken)

Description: The graduateAndUpgrade() function fails to enforce that students meet the required cutOffScore before being upgraded to the next contract level. While cutOffScore is defined and set during startSession(), it is never referenced again. As a result, all students are implicitly treated as eligible for graduation regardless of performance.

This breaks the logical expectation that graduation should be merit-based and undermines the scoring system implemented via studentScore.

Impact: Any student, including those with a failing score, can be passed forward to LevelTwo. This:

  • Voids the purpose of reviews and scoring,

  • Introduces inconsistencies in evaluation logic across levels,

  • May lead to unexpected behavior or vulnerabilities in LevelTwo if it assumes all students met a baseline qualification.

Proof of Concept: Paste the following test in the test suite...

function test_upgradeSucceedsEvenWithFailingStudent() public {
// Setup: Add 1 teacher
vm.prank(principal);
levelOneProxy.addTeacher(alice);
// Enroll two students: one will pass, one will fail
address studentPass = dan;
address studentFail = clara;
vm.startPrank(studentPass);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(studentFail);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// Start the session with a cutoff score of 70
vm.prank(principal);
levelOneProxy.startSession(70);
// Give reviews
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
levelOneProxy.giveReview(studentFail, false); // score drops to 90
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
levelOneProxy.giveReview(studentFail, false); // score drops to 80
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
levelOneProxy.giveReview(studentFail, false); // score drops to 70
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
levelOneProxy.giveReview(studentFail, false); // score drops to 60 (failing)
// Deploy LevelTwo implementation
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
// Attempt upgrade — this will succeed despite clara failing
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
LevelTwo levelTwoProxy = LevelTwo(address(levelOneProxy));
console2.log("LevelTwo total students after upgrade:", levelTwoProxy.getTotalStudents());
// Assert both students were included, showing no cutoff enforcement
assertEq(levelTwoProxy.getTotalStudents(), 2);
}

Logs:

[PASS] test_upgradeSucceedsEvenWithFailingStudent() (gas: 1036651)
Logs:
LevelTwo total students after upgrade: 2

Recommended Mitigation: Filter listOfStudents before upgrade, or explicitly store eligible students. Example:

Patch
address[] memory qualified;
for (uint256 i = 0; i < listOfStudents.length; i++) {
if (studentScore[listOfStudents[i]] >= cutOffScore) {
qualified.push(listOfStudents[i]);
}
}
Updates

Lead Judging Commences

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