Vulnerability Details
When the teacher calls giveReview and outputs a review, giveReview does not update the value of reviewCount. As a result, the check require(reviewCount[_student] < 5) may still pass.
function giveReview(address _student, bool review) public onlyTeacher {
if (!isStudent[_student]) {
revert HH__StudentDoesNotExist();
}
require(reviewCount[_student] < 5, "Student review count exceeded!!!");
require(block.timestamp >= lastReviewTime[_student] + reviewTime, "Reviews can only be given once per week");
if (!review) {
studentScore[_student] -= 10;
}
lastReviewTime[_student] = block.timestamp;
emit ReviewGiven(_student, review, studentScore[_student]);
}
Impact
The invariant that “System upgrade should not occur if any student has not gotten 4 reviews” can be broke. If reviewCount is not updated correctly, the system will not know whether all students have received 4 reviews to perform upgrade.
Additionally, graduateAndUpgrade does not check reviewCount of each student.
PoC
Alter reviewCount's visibility to public:
contract LevelOne is Initializable, UUPSUpgradeable {
mapping(address => uint256) public reviewCount;
}
PoC:
pragma solidity 0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {DeployLevelOne} from "../script/DeployLevelOne.s.sol";
import {GraduateToLevelTwo} from "../script/GraduateToLevelTwo.s.sol";
import {LevelOne} from "../src/LevelOne.sol";
import {LevelTwo} from "../src/LevelTwo.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
contract LevelOneAndGraduateTest is Test {
DeployLevelOne deployBot;
GraduateToLevelTwo graduateBot;
LevelOne levelOneProxy;
LevelTwo levelTwoImplementation;
address proxyAddress;
address levelOneImplementationAddress;
address levelTwoImplementationAddress;
MockUSDC usdc;
address principal;
uint256 schoolFees;
address alice;
address bob;
address clara;
address dan;
address eli;
address fin;
address grey;
address harriet;
function setUp() public {
deployBot = new DeployLevelOne();
proxyAddress = deployBot.deployLevelOne();
levelOneProxy = LevelOne(proxyAddress);
usdc = deployBot.getUSDC();
principal = deployBot.principal();
schoolFees = deployBot.getSchoolFees();
levelOneImplementationAddress = deployBot.getImplementationAddress();
alice = makeAddr("first_teacher");
bob = makeAddr("second_teacher");
clara = makeAddr("first_student");
dan = makeAddr("second_student");
eli = makeAddr("third_student");
fin = makeAddr("fourth_student");
grey = makeAddr("fifth_student");
harriet = makeAddr("six_student");
usdc.mint(clara, schoolFees);
usdc.mint(dan, schoolFees);
usdc.mint(eli, schoolFees);
usdc.mint(fin, schoolFees);
usdc.mint(grey, schoolFees);
usdc.mint(harriet, schoolFees);
}
function test_poc_giveReview() external schoolInSession {
vm.startPrank(alice);
vm.warp(block.timestamp + 1 weeks);
levelOneProxy.giveReview(harriet, false);
uint256 count1 = levelOneProxy.reviewCount(harriet);
vm.warp(block.timestamp + 2 weeks);
levelOneProxy.giveReview(harriet, false);
uint256 count2 = levelOneProxy.reviewCount(harriet);
vm.stopPrank();
assertNotEq(count1, count2);
}
modifier schoolInSession() {
_teachersAdded();
_studentsEnrolled();
vm.prank(principal);
levelOneProxy.startSession(70);
_;
}
function _teachersAdded() internal {
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
vm.stopPrank();
}
function _studentsEnrolled() internal {
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(eli);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(fin);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(grey);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(harriet);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
}
}
Log show the reviewCount remains the same:
├─ [0] VM::startPrank(first_teacher: [0xeeEeC5A3afd714e3C63A0b1ef6d80722Bcc514b3])
│ └─ ← [Return]
├─ [0] VM::warp(604801 [6.048e5])
│ └─ ← [Return]
├─ [29010] ERC1967Proxy::fallback(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b], false)
│ ├─ [28533] LevelOne::giveReview(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b], false) [delegatecall]
│ │ ├─ emit ReviewGiven(student: six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b], review: false, studentScore: 90)
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [1391] ERC1967Proxy::fallback(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b]) [staticcall]
│ ├─ [914] LevelOne::reviewCount(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b]) [delegatecall]
│ │ └─ ← [Return] 0
│ └─ ← [Return] 0
├─ [0] VM::warp(1814401 [1.814e6])
│ └─ ← [Return]
├─ [5110] ERC1967Proxy::fallback(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b], false)
│ ├─ [4633] LevelOne::giveReview(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b], false) [delegatecall]
│ │ ├─ emit ReviewGiven(student: six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b], review: false, studentScore: 80)
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [1391] ERC1967Proxy::fallback(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b]) [staticcall]
│ ├─ [914] LevelOne::reviewCount(six_student: [0x983Cec4DF373E6f3809b4483fEBf6C9469B0769b]) [delegatecall]
│ │ └─ ← [Return] 0
│ └─ ← [Return] 0
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::assertNotEq(0, 0) [staticcall]
│ └─ ← [Revert] assertion failed: 0 == 0
└─ ← [Revert] assertion failed: 0 == 0
Tools Used
Manual.
Recommendations
function giveReview(address _student, bool review) public onlyTeacher {
if (!isStudent[_student]) {
revert HH__StudentDoesNotExist();
}
require(reviewCount[_student] < 5, "Student review count exceeded!!!");
require(block.timestamp >= lastReviewTime[_student] + reviewTime, "Reviews can only be given once per week");
if (!review) {
studentScore[_student] -= 10;
}
lastReviewTime[_student] = block.timestamp;
> reviewCount[_student] += 1;
emit ReviewGiven(_student, review, studentScore[_student]);
}