The upgrade from LevelOne contract to LevelTwo contract introduces a storage layout mismatch that results in a critical storage collision. Specifically, variables such as schoolFees, sessionEnd, bursary and cutOffScore in LevelOne are removed or reordered in LevelTwo, leading to misalignment of subsequent storage slots. When the implementation slot in the ERC1967 proxy contract is upgraded to LevelTwo, previously initialized storage slots will be interpreted incorrectly, potentially causing incorrect contract behavior or fund mismanagement.
In Solidity, storage layout is strictly positional and upgrades must maintain storage variable order and structure to avoid corruption. The V1 contract contains several state variables that are removed or restructured in V2:
LevelOne includes schoolFees, reviewCount and lastReviewTime.
LevelTwo removes these variables entirely.
LevelOne includes studentScore, reviewCount and lastReviewTime.
LevelTwo removes these mappings entirely.
LevelOne includes sessionEnd, bursary and cutOffScore.
LevelTwo moved these variables up, each variable has moved up one slot. (ie. from slot2 to slot1)
LevelOne includes isTeacher, isStudent, and studentScore.
LevelTwo moved these mappings up, each mapping has moved up one slot. (ie. from slot2 to slot1)
LevelOne includes listOfStudents and listOfTeachers.
LevelTwo moved these dynamic arrays up, each array has moved up one slot. (ie. from slot2 to slot1)
This kind of misalignment can lead to:
Corrupted logic based on incorrect state values (e.g., sessionEnd reading a leftover schoolFees value),
Broken access control or roles (due to incorrect isTeacher/isStudent values),
Financial errors in bursary distribution or wage calculation.
In order to demonstrate the impact of this vulnerability, we need to slightly modify the original source code:
Then, we slightly modify the test_confirm_can_graduate() test function in order to see how storage collision impacts.
Finally, run the PoC:
You should be able to see the output below in your console:
After the upgrade, the variable usdc points to an unused storage slot, this will cause the contract to be Denial-Of-Service.
The impact is high because:
State corruption may result in incorrect session states, faulty role permissions, and misallocated funds.
It may break critical contract logic that relies on these state variables, potentially bricking the application or causing loss of user funds.
The upgrade would appear successful on-chain but the contract will silently misbehave post-upgrade.
This kind of vulnerability can undermine trust in the protocol and may require a forceful rollback or redeployment to resolve, both of which carry reputational and operational costs.
Manual Code Review
Foundry
Slither-Read-Storage
Never remove or reorder storage variables in upgradeable contracts. Instead, append new variables after existing ones.
Introduce a LevelTwoStorage struct and inherit it after LevelOneStorage to ensure layout preservation.
Include comprehensive upgrade tests that simulate the full upgrade from LevelOne to LevelTwo and validate expected state values.
Consider using a gap pattern (i.e., reserved storage slots) to allow for future-safe expansion without shifting existing variables.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.