Description:
Students who do not meet the cutoff score are still upgraded to LevelTwo
as there are no checks to filter them out.
Impact:
This violates the invariant "Any student who doesn't meet the cutOffScore
should not be upgraded"
Proof of Concept:
function test_students_below_cutoff_not_removed() public schoolInSession {
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
levelOneProxy.giveReview(harriet, false);
assertEq(levelOneProxy.getTotalStudents(), 6);
assertEq(levelOneProxy.studentScore(harriet), 90);
assertEq(levelOneProxy.cutOffScore(), 100);
assertLt(levelOneProxy.studentScore(harriet), levelOneProxy.cutOffScore());
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
LevelTwo levelTwoProxy = LevelTwo(proxyAddress);
assertEq(levelTwoProxy.getTotalStudents(), 6);
}
Recommended Mitigation:
There are many ways to include this logic, depending on the required logic. The following shows the most straight forward way which is to call expel
the students that did not meet the cutoff score, then call upgrade. Other ways that do not use expel
would also work if expel
is not the desired outcome.
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
if (_levelTwo == address(0)) {
revert HH__ZeroAddress();
}
++ for (uint256 n = 0; n < listOfStudents.length; n++) {
++ address student = listOfStudents[n];
++ if (studentScore[student] < cutOffScore) {
++ expel(student);
++ }
++ }
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}