Storage slots of variables in LevelTwo
does not match those of LevelOne
. After school system upgrades, LevelTwo
implementation contract will attempt to access variables in the wrong storage slot of proxy
contract, resulting in storage collision and breaks the school records.
Before the school system upgrades, LevelOne
implementation contract has modified state of storage slots in proxy
contract in a particular order. In more detail (ref), address
, bool
and uint256
are packed (if possible) and stored in 32 byte slots based on the order of declaration (excluding constant
and immutable
variables). array
s stores the array.length
to the storage slot at declaration, and array data is stored sequentially starting at slot keccak256(abi.encode(ARRAY_SLOT_DECLARATION))
. mapping
of key
:value
pair leaves an empty slot at mapping declaration, with the value
stored at keccak256(abi.encode(KEY, SLOT_INDEX_DECLARATION))
.
After the school system upgrades, LevelTwo
implementation contract does not have the same order of variables as LevelOne
contract, leading to LevelTwo
contract accessing the wrong storage slot locations of the proxy
contract.
Below shows the storage slot layout in the proxy
contract, the variables/functions in LevelOne
and LevelTwo
accessing the corresponding storage slots as well as the storage collision impact. Using two teachers and two students as example for brevity.
Slot | LevelOne | LevelTwo | Collision |
---|---|---|---|
0 | principal | principal | |
1 | inSession | inSession | |
2 | schoolFees | sessionEnd | <@ sessionEnd returns schoolFees (5000e18) |
3 | sessionEnd | bursary | <@ bursary returns sessionEnd (block.timestamp + 4 weeks if session started, 0 otherwise) |
4 | bursary | cutOffScore | <@ cutOffScore returns bursary |
5 | cutOffScore | isTeacher declaration | |
6 | isTeacher declaration | isStudent declaration | |
7 | isStudent declaration | studentScore declaration | |
8 | studentScore declaration | listOfStudents.length | <@ listOfStudents.length returns 0 (listOfStudents returns empty array) |
9 | reviewCount declaration | listOfTeachers.length | <@ listOfTeachers.length returns 0 (listOfTeachers returns empty array) |
10 | lastReviewTime declaration | usdc | <@ usdc returns 0 address |
11 | listOfStudents.length | ||
12 | listOfTeachers.length | ||
13 | usdc | ||
ka(teacher0Addr,5) | isTeacher[teacher0] | <@ isTeacher[teacherAddr] returns false (teacher is de-registered) | |
ka(teacher1Addr,5) | isTeacher[teacher1] | <@ isTeacher[teacherAddr] returns false (teacher is de-registered) | |
ka(teacher0Addr,6) | isTeacher[teacher0] | isStudent[teacher0] | <@ isStudent[teacherAddr] returns true (teacher registered as student) |
ka(teacher1Addr,6) | isTeacher[teacher1] | isStudent[teacher1] | <@ isStudent[teacherAddr] returns true (teacher registered as student) |
ka(student0Addr,6) | isStudent[student0] | <@ isStudent[studentAddr] returns false (student is de-registered) | |
ka(student0Addr,7) | isStudent[student0] | studentScore[student0] | <@ studentScore[studentAddr] returns 1 (student's score reduced) |
ka(student0Addr,8) | studentScore[student0] | ||
ka(student0Addr,9) | reviewCount[student0] | ||
ka(student0Addr,10) | lastReviewTime[student0] | ||
ka(student1Addr,6) | isStudent[student1] | <@ isStudent[studentAddr] returns false (student is de-registered) | |
ka(student1Addr,7) | isStudent[student1] | studentScore[student1] | <@ studentScore[studentAddr] returns 1 (student's score reduced) |
ka(student1Addr,8) | studentScore[student1] | ||
ka(student1Addr,9) | reviewCount[student1] | ||
ka(student1Addr,10) | lastReviewTime[student1] | ||
ka(11) | listOfStudents[0] i.e. student0Addr | ||
ka(11) + 1 | listOfStudents[1] i.e. student1Addr | ||
ka(12) | listOfTeachers[0] i.e. teacher0Addr | ||
ka(12) + 1 | listOfTeachers[1] i.e. teacher1Addr |
**ka(params) denotes keccak256(abi.encode(params))
**Note: minor changes needed to the LevelTwo
contract before running PoC to fix upgradeability issue. (_authorizeUpgrade left unprotected only for demonstration purposes)
Place the following into LevelOne|AndGraduateTest.t.sol
and run
forge test --mt testStorageCollision
Below is the list of impacts after school system upgrade caused by the storage collision
LevelTwo::sessionEnd
returns LevelOne::schoolFees
(5000e18)
LevelTwo::bursary
returns LevelOne::sessionEnd
(block.timestamp + 4 weeks if session started, 0 otherwise)
LevelTwo::cutOffScore
returns LevelOne::bursary
LevelTwo::listOfStudents.length
returns 0 (breaks LevelTwo::getTotalStudents
)
LevelTwo::listOfStudents
returns empty array (breaks LevelTwo::getListOfStudents
)
LevelTwo::listOfTeachers.length
returns 0 (breaks LevelTwo::getTotalTeachers
)
LevelTwo::listOfTeachers
returns empty array (breaks LevelTwo::getListOfTeachers
)
LevelTwo::usdc
returns 0 address (breaks LevelTwo::getSchoolFeesToken
)
LevelTwo::isTeacher[teacherAddr]
returns false (teacher is de-registered)
LevelTwo::isStudent[teacherAddr]
returns true (teacher registered as student)
LevelTwo::isStudent[studentAddr]
returns false (student is de-registered)
LevelTwo::studentScore[studentAddr]
returns 1 (student's score reduced)
Impact: High, school records are broken
Likelihood: High, principal will upgrade school system at the end of school session (after 4 weeks)
Severity: High
Manual review
Add dummy variables to fill up storage slots and prevent storage collisions
LevelTwo.sol
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.