Hawk High

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

Permanent State Lock After Session End

Summary

The contract lacks functionality to reset the session state after a session has ended, creating a permanent lock where inSession remains true forever. This prevents starting new sessions or enrolling new students after the first session ends, severely limiting the contract's long-term usability.

Vulnerability Details

The contract implements a session system with a start mechanism but no corresponding end or reset functionality:

function startSession(uint256 _cutOffScore) public onlyPrincipal notYetInSession {
sessionEnd = block.timestamp + 4 weeks;
inSession = true;
cutOffScore = _cutOffScore;
emit SchoolInSession(block.timestamp, sessionEnd);
}
modifier notYetInSession() {
if (inSession == true) {
revert HH__AlreadyInSession();
}
_;
}

While the contract sets a sessionEnd timestamp, it never changes the inSession flag back to false when this time is reached. Critical functions like startSession() and enroll() use the notYetInSession modifier, which will permanently revert after the first session is started.

Impact

After the first session ends (4 weeks after startSession()):

  1. The inSession flag remains true indefinitely

  2. New educational sessions cannot be started due to the notYetInSession modifier

  3. New students cannot enroll for the same reason

  4. The school effectively becomes locked in a permanent "in session" state despite the session having ended

  5. The only way to "reset" this state is through a contract upgrade, which is an overly complex solution

This creates a fundamental limitation on the contract's lifecycle, requiring constant upgrades to maintain basic functionality.

Tools Used

Manual Review

Recommendations

Implement a function to reset the session state after sessionEnd is reached:

function endSession() public onlyPrincipal {
require(block.timestamp >= sessionEnd, "Session has not ended yet");
require(inSession == true, "No active session");
inSession = false;
emit SessionEnded(sessionEnd, block.timestamp);
}

Alternatively, modify functions that use the notYetInSession modifier to also check if the current time is past sessionEnd:

modifier notYetInSession() {
if (inSession == true && block.timestamp < sessionEnd) {
revert HH__AlreadyInSession();
}
_;
}

This would automatically consider a session ended after the sessionEnd timestamp has passed.


POC

function testPermanentStateLock() public {
// Setup
_teachersAdded();
console.log("==================== INITIAL STATE ====================");
console.log("Session active:", levelOneProxy.getSessionStatus());
// Enroll a student before session
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
console.log("Student enrolled successfully");
console.log("Total students:", levelOneProxy.getTotalStudents());
// Start a session
console.log("\n==================== STARTING SESSION ====================");
vm.prank(principal);
levelOneProxy.startSession(70);
console.log("Session started with cutoff score:", levelOneProxy.cutOffScore());
console.log("Session active:", levelOneProxy.getSessionStatus());
console.log("Session end timestamp:", levelOneProxy.sessionEnd());
console.log("Current timestamp:", block.timestamp);
// Fast forward past session end (4 weeks + 1 day)
uint256 timeJump = 4 weeks + 1 days;
console.log("\n==================== FAST FORWARD PAST SESSION END ====================");
console.log("Fast forwarding time by:", timeJump / 1 days, "days");
vm.warp(block.timestamp + timeJump);
console.log("New current timestamp:", block.timestamp);
console.log("Session end timestamp:", levelOneProxy.sessionEnd());
console.log("Time past session end:", block.timestamp - levelOneProxy.sessionEnd(), "seconds");
console.log("Session still active?", levelOneProxy.getSessionStatus());
// Try to start a new session - will fail
console.log("\n==================== ATTEMPTING TO START NEW SESSION ====================");
vm.prank(principal);
try levelOneProxy.startSession(80) {
console.log("New session started - UNEXPECTED!");
} catch Error(string memory reason) {
console.log("Error starting new session:", reason);
} catch {
console.log("Failed to start new session due to AlreadyInSession error");
}
// Try to enroll a new student - will fail
console.log("\n==================== ATTEMPTING TO ENROLL NEW STUDENT ====================");
vm.startPrank(dan);
usdc.approve(address(levelOneProxy), schoolFees);
try levelOneProxy.enroll() {
console.log("New student enrolled - UNEXPECTED!");
} catch Error(string memory reason) {
console.log("Error enrolling student:", reason);
} catch {
console.log("Failed to enroll new student due to AlreadyInSession error");
}
vm.stopPrank();
// Verify we're past session end but still "in session"
console.log("\n==================== FINAL STATE VERIFICATION ====================");
console.log("Current time is after session end:", block.timestamp > levelOneProxy.sessionEnd());
console.log("Session is still considered active:", levelOneProxy.getSessionStatus());
console.log("\nCONCLUSION: The contract is permanently locked in 'inSession' state");
console.log("No new sessions can be started and no new students can enroll");
assertGt(block.timestamp, levelOneProxy.sessionEnd());
assertTrue(levelOneProxy.getSessionStatus());
}

Output

forge test --mt testPermanentStateLock -vv
[⠊] Compiling...
[⠃] Compiling 1 files with Solc 0.8.26
[⠊] Solc 0.8.26 finished in 774.42ms
Compiler run successful!
Ran 1 test for test/LeveOnelAndGraduateTest.t.sol:LevelOneAndGraduateTest
[PASS] testPermanentStateLock() (gas: 546621)
Logs:
==================== INITIAL STATE ====================
Session active: false
Student enrolled successfully
Total students: 1
==================== STARTING SESSION ====================
Session started with cutoff score: 70
Session active: true
Session end timestamp: 2419201
Current timestamp: 1
==================== FAST FORWARD PAST SESSION END ====================
Fast forwarding time by: 29 days
New current timestamp: 2505601
Session end timestamp: 2419201
Time past session end: 86400 seconds
Session still active? true
==================== ATTEMPTING TO START NEW SESSION ====================
Failed to start new session due to AlreadyInSession error
==================== ATTEMPTING TO ENROLL NEW STUDENT ====================
Failed to enroll new student due to AlreadyInSession error
==================== FINAL STATE VERIFICATION ====================
Current time is after session end: true
Session is still considered active: true
CONCLUSION: The contract is permanently locked in 'inSession' state
No new sessions can be started and no new students can enroll
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 17.21ms (3.95ms CPU time)
Ran 1 test suite in 103.65ms (17.21ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Updates

Lead Judging Commences

yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Validated
Assigned finding tags:

session state not updated

`inSession` not updated after during upgrade

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