Hawk High

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

First Review Can Always Be Given Immediately Due to Uninitialized lastReviewTime

Summary

The contract does not enforce the review interval for a student's first review because lastReviewTime defaults to zero. This allows a teacher to give the first review immediately after session start, bypassing the intended waiting period. The review interval check only restricts subsequent reviews, not the initial one.

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");
// where `false` is a bad review and true is a good review
if (!review) {
studentScore[_student] -= 10;
}
// Update last review time
lastReviewTime[_student] = block.timestamp;
emit ReviewGiven(_student, review, studentScore[_student]);
}

Vulnerability Details

Description:
The giveReview function enforces a review interval by requiring that block.timestamp is greater than or equal to lastReviewTime[_student] + reviewTime. However, for a student who has never been reviewed, lastReviewTime[_student] defaults to zero. As a result, the first review can be given at any time after session start, regardless of the intended review interval.


Exploit Scenario:
Immediately after the session starts, a teacher may unintentionally give a review to a student before the required one-week interval has passed. Because lastReviewTime is uninitialized and defaults to zero, the contract permits the first review to be submitted right away. This could result in student scores being affected earlier than intended, simply due to a teacher acting without realizing the review interval has not yet elapsed.

PoC

function test_giveReview_allowsFirstReviewBeforeInterval() public {
console2.log("time of block:", block.timestamp);
console2.log("time of block harriet:", levelOneProxy.getLastReviewTime(harriet));
// Simulate a real-world block.timestamp (e.g., 1,700,000,000)
vm.warp(1_700_000_000);
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
vm.stopPrank();
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.prank(principal);
levelOneProxy.startSession(70);
vm.stopPrank();
vm.startPrank(alice);
levelOneProxy.giveReview(eli, false);
vm.stopPrank();
assert(levelOneProxy.studentScore(eli) == 90);
}

Impact

  1. Teachers can immediately give a review to any new student without waiting for the review interval (e.g., 1 week).

  2. This bypasses the intended time-based restriction for the first review, potentially allowing for unfair or unintended manipulation of student scores right after enrollment.

Tools Used

Foundry

Recommendations

Initialize lastReviewTime for each student at session start to the current block timestamp. This enforces the review interval for the first review and closes the vulnerability.

function startSession(uint256 _cutOffScore) public onlyPrincipal notYetInSession {
sessionEnd = block.timestamp + 4 weeks;
inSession = true;
cutOffScore = _cutOffScore;
+ uint256 numberOfStudents = listOfStudents.length;
+ for(uint256 i = 0, i < numberOfStudents, i++) {
+ lastReviewTime[listOfStudents[i]] = block.timestamp;
+ }
emit (block.timestamp, sessionEnd);
}
Updates

Lead Judging Commences

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

session state not updated

`inSession` not updated after during upgrade

Support

FAQs

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