Description: The two implementations, 'LevelOne' and 'LevelTwo', declare their state variables in different orders. When you perform a UUPS upgrade, the proxy's storage is interpreted by the new implementation according to its own layout. Mismatched layouts cause storage collisions—slots written by one implementation map to the wrong variables in the next.
Impact:
Corrupted state: Data intended for 'bursary' in LevelOne end up in 'cutOffScore' in LevelTwo, leading to wildly incorrect behavior.
Security bypasses: Critical invariants break, and funds or upgrades can be executed under the wrong conditions.
Fund loss or lock: Misinterpreted 'bursary' values can result in under- or over-payment, or leave residual balances stranded.
Proof of Concept:
LevelOne storage:
Name | Type | Slot | Offset | Bytes |
---|---|---|---|---|
principal | address | 0 | 0 | 20 |
inSession | bool | 0 | 20 | 1 |
schoolFees | uint256 | 1 | 0 | 32 |
sessionEnd | uint256 | 2 | 0 | 32 |
bursary | uint256 | 3 | 0 | 32 |
cutOffScore | uint256 | 4 | 0 | 32 |
isTeacher | mapping(address => bool) | 5 | 0 | 32 |
isStudent | mapping(address => bool) | 6 | 0 | 32 |
studentScore | mapping(address => uint256) | 7 | 0 | 32 |
reviewCount | mapping(address => uint256) | 8 | 0 | 32 |
lastReviewTime | mapping(address => uint256) | 9 | 0 | 32 |
listOfStudents | address[] | 10 | 0 | 32 |
listOfTeachers | address[] | 11 | 0 | 32 |
usdc | contract IERC20 | 12 | 0 | 20 |
LevelTwo storage:
Name | Type | Slot | Offset | Bytes |
---|---|---|---|---|
principal | address | 0 | 0 | 20 |
inSession | bool | 0 | 20 | 1 |
sessionEnd | uint256 | 1 | 0 | 32 |
bursary | uint256 | 2 | 0 | 32 |
cutOffScore | uint256 | 3 | 0 | 32 |
isTeacher | mapping(address => bool) | 4 | 0 | 32 |
isStudent | mapping(address => bool) | 5 | 0 | 32 |
studentScore | mapping(address => uint256) | 6 | 0 | 32 |
listOfStudents | address[] | 7 | 0 | 32 |
listOfTeachers | address[] | 8 | 0 | 32 |
usdc | contract IERC20 | 9 | 0 | 20 |
Note: this PoC assumes that the 'Missing UUPS Inheritance in LevelTwo' issue has already been fixed, so that graduateAndUpgrade gets as far as splitting by totalTeachers instead of reverting earlier.
After this fix include the following test in the LevelOneAndGraduateTest.t.sol
file:
Recommended Mitigation:
Adopt a stable storage layout pattern across all versions:
Keep variable order identical in both implementations.
Insert new state variables only at the end of the layout.
Reserve gaps for future growth, e.g.:
This ensures that every storage slot’s meaning remains consistent through each upgrade, preventing collisions and preserving all invariants.
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.