Summary
The function graduateAndUpgrade
breaks the invariant:
“System upgrade cannot take place unless the school’s sessionEnd
has been reached.”
Vulnerability Details
The contract currently allows the principal
to call graduateAndUpgrade()
immediately after startSession()
— even though the session period (4 weeks) hasn't elapsed.
There is no check for block.timestamp >= sessionEnd
, so the upgrade can happen prematurely.
POC:
This test passes without reverting, proving the bug
function test_fuzz_UpgradeBeforeSessionEndShouldFail(address levelTwo) public {
vm.assume(levelTwo != address(0));
levelOne.initialize(PRINCIPAL, schoolFee, address(usdc));
vm.prank(PRINCIPAL);
levelOne.startSession(50);
vm.prank(PRINCIPAL);
levelOne.graduateAndUpgrade(levelTwo, "");
}
Ran 1 test for test/LevelOne.t.sol:LevelOneTest
[PASS] test_fuzz_UpgradeBeforeSessionEndShouldFail(address) (runs: 257, μ: 176492, ~: 176492)
Traces:
[176492] LevelOneTest::test_fuzz_UpgradeBeforeSessionEndShouldFail(0x4E489a0C8d553D7888dF6c29C71762E76f7EF0b6)
├─ [0] VM::assume(true) [staticcall]
│ └─ ← [Return]
├─ [92624] LevelOne::initialize(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], 1000000000000000000 [1e18], MockUSDC: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
│ ├─ emit Initialized(version: 1)
│ └─ ← [Stop]
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [47258] LevelOne::startSession(50)
│ ├─ emit SchoolInSession(startTime: 1, endTime: 2419201 [2.419e6])
│ └─ ← [Stop]
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [18224] LevelOne::graduateAndUpgrade(0x4E489a0C8d553D7888dF6c29C71762E76f7EF0b6, 0x)
│ ├─ [7850] MockUSDC::transfer(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], 0)
│ │ ├─ emit Transfer(from: LevelOne: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], value: 0)
│ │ └─ ← [Return] true
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 43.52ms (42.23ms CPU time)
Ran 1 test suite in 540.04ms (43.52ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Impact
Violates a critical time-based invariant of the school system.
Students may be "graduated" without completing their session or reviews.
Can lead to premature payout of wages and inaccurate academic progression logic.
Tools Used
Foundry (Forge)
Fuzz testing
Recommendations
Add a check inside graduateAndUpgrade
:
require(block.timestamp >= sessionEnd, "Session has not ended yet");
Ran 1 test for test/LevelOne.t.sol:LevelOneTest
[PASS] test_fuzz_UpgradeBeforeSessionEndShouldFail(address) (runs: 257, μ: 176492, ~: 176492)
Traces:
[176492] LevelOneTest::test_fuzz_UpgradeBeforeSessionEndShouldFail(0xd0E36678b36617E8b66369B2060fF4B1eA81E237)
├─ [0] VM::assume(true) [staticcall]
│ └─ ← [Return]
├─ [92624] LevelOne::initialize(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], 1000000000000000000 [1e18], MockUSDC: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
│ ├─ emit Initialized(version: 1)
│ └─ ← [Stop]
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [47258] LevelOne::startSession(50)
│ ├─ emit SchoolInSession(startTime: 1, endTime: 2419201 [2.419e6])
│ └─ ← [Stop]
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [18224] LevelOne::graduateAndUpgrade(0xd0E36678b36617E8b66369B2060fF4B1eA81E237, 0x)
│ ├─ [7850] MockUSDC::transfer(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], 0)
│ │ ├─ emit Transfer(from: LevelOne: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca], value: 0)
│ │ └─ ← [Return] true
│ └─ ← [Stop]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 50.26ms (39.52ms CPU time)
Ran 1 test suite in 630.11ms (50.26ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)