Hawk High

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

Storage Collision on Upgrade from `LevelOne` to `LevelTwo`

Summary

The LevelOne contract, a UUPS proxy, upgrades to LevelTwo via the graduateAndUpgrade function. However, LevelOne uses 12 storage slots, while LevelTwo uses 9 misaligned slots, leading to a storage collision. Post-upgrade, LevelTwo variables read incorrect values from LevelOne storage (e.g., sessionEnd reads schoolFees), corrupting critical protocol state.

Vulnerability Details

In UUPS upgradeable contracts, the storage layout must remain compatible across implementations to prevent collisions. LevelOne defines 12 storage slots:

Slot 1: `schoolFees` (uint256)
Slot 2: `sessionEnd` (uint256)
Slot 3: `bursary` (uint256)
Slot 4: `cutOffScore` (uint256)
Slot 5: `isTeacher` (mapping(address => bool))
Slot 6: `isStudent` (mapping(address => bool))
Slot 7: `studentScore` (mapping(address => uint256))
Slot 8: `reviewCount` (mapping(address => uint256))
Slot 9: `lastReviewTime` (mapping(address => uint256))
Slot 10: `listOfStudents` (address[] length)
Slot 11: `listOfTeachers` (address[] length)
Slot 12: `usdc` (IERC20)

LevelTwo Storage Layout (9 slots):

Slot 1: `sessionEnd` (uint256)
Slot 2: `bursary` (uint256)
Slot 3: `cutOffScore` (uint256)
Slot 4: `isTeacher` (mapping(address => bool))
Slot 5: `isStudent` (mapping(address => bool))
Slot 6: `studentScore` (mapping(address => uint256))
Slot 7: `reviewCount` (mapping(address => uint256))
Slot 8: `lastReviewTime` (mapping(address => uint256))
Slot 9: `listOfStudents` (address[] length)

Post-upgrade, LevelTwo variables read incorrect LevelOne slots:

`sessionEnd` (slot 1) = `schoolFees`
`bursary` (slot 2) = `sessionEnd`
`cutOffScore` (slot 3) = `bursary`
`isTeacher` (slot 4) = `cutOffScore`
`isStudent` (slot 5) = `isTeacher`
`studentScore` (slot 6) = `isStudent`
`reviewCount` (slot 7) = `studentScore`
`lastReviewTime` (slot 8) = `reviewCount`
`listOfStudents` (slot 9) = `lastReviewTime`

This misalignment corrupts critical protocol state, violating


Proof of Concept:

Add a new file in folder test as StorageCollisionTest.t.sol and paste all these code there

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test, console} from "forge-std/Test.sol";
import {LevelOne} from "../src/LevelOne.sol";
import {LevelTwo} from "../src/LevelTwo.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {MockUSDC} from "../test/mocks/MockUSDC.sol";
contract StorageCollisionTest is Test {
LevelOne levelOne;
LevelTwo levelTwo;
MockUSDC usdc;
address proxy;
address PRINCIPAL = makeAddr("PRINCIPAL");
address TEACHER = makeAddr("TEACHER");
address STUDENT = makeAddr("STUDENT");
function setUp() public {
usdc = new MockUSDC();
levelOne = new LevelOne();
levelTwo = new LevelTwo();
usdc.mint(STUDENT, 100_000e18);
proxy = address(new ERC1967Proxy(
address(levelOne),
abi.encodeWithSelector(
LevelOne.initialize.selector,
PRINCIPAL,
5000e18,
address(usdc)
)));
}
function testStorageCollisionOnUpgrade() public {
vm.startPrank(PRINCIPAL);
LevelOne(proxy).addTeacher(TEACHER);
vm.stopPrank();
vm.startPrank(STUDENT);
usdc.approve(proxy, 5000e18);
LevelOne(proxy).enroll();
vm.stopPrank();
// 2. Start session
vm.startPrank(PRINCIPAL);
LevelOne(proxy).startSession(70);
vm.stopPrank();
// 3. Simulate weekly reviews
for (uint i = 0; i < 4; i++) {
vm.startPrank(TEACHER);
vm.warp(block.timestamp + 1 weeks);
LevelOne(proxy).giveReview(STUDENT, true);
vm.stopPrank();
}
// Check storage slots before upgrade
bytes32 slot1Before = vm.load(proxy, bytes32(uint256(1))); // schoolFees in L1
bytes32 slot2Before = vm.load(proxy, bytes32(uint256(2))); // sessionEnd in L1
bytes32 slot3Before = vm.load(proxy, bytes32(uint256(3))); // bursary in L1
bytes32 slot4Before = vm.load(proxy, bytes32(uint256(4))); // cutoffScore in L1
bytes32 slot5Before = vm.load(proxy, bytes32(uint256(5))); // isTeacher in L1
bytes32 slot6Before = vm.load(proxy, bytes32(uint256(6))); // isStudent in L1
bytes32 slot7Before = vm.load(proxy, bytes32(uint256(7))); // studentScore in L1
bytes32 slot8Before = vm.load(proxy, bytes32(uint256(8))); // reviewCount in L1
bytes32 slot9Before = vm.load(proxy, bytes32(uint256(9))); // lastReviewTime in L1
bytes32 slot10Before = vm.load(proxy, bytes32(uint256(10))); // listOfStudents in L1
bytes32 slot11Before = vm.load(proxy, bytes32(uint256(11))); // listOfTeachers in L1
bytes32 slot12Before = vm.load(proxy, bytes32(uint256(12))); // usdc in L1
console.log("Pre-upgrade slot1 (schoolFees):", uint256(slot1Before));
console.log("Pre-upgrade slot2 (sessionEnd):", uint256(slot2Before));
console.log("Pre-upgrade slot3 (bursary):", uint256(slot3Before));
console.log("Pre-upgrade slot4 (cutOffScore):", uint256(slot4Before));
console.log("Pre-upgrade slot5 (isTeacher):", uint256(slot5Before));
console.log("Pre-upgrade slot6 (isStudent):", uint256(slot6Before));
console.log("Pre-upgrade slot7 (studentScore):", uint256(slot7Before));
console.log("Pre-upgrade slot8 (reviewCount):", uint256(slot8Before));
console.log("Pre-upgrade slot9 (lastReviewTime):", uint256(slot9Before));
console.log("Pre-upgrade slot10 (listOfStudents):", uint256(slot10Before));
console.log("Pre-upgrade slot11 (listOfTeachers):", uint256(slot11Before));
console.log("Pre-upgrade slot12 (usdc):", uint256(slot12Before));
// Perform upgrade to LevelTwo
vm.startPrank(PRINCIPAL);
LevelOne(proxy).graduateAndUpgrade(address(levelTwo), "");
vm.stopPrank();
// Check storage slots after upgrade
bytes32 slot1After = vm.load(proxy, bytes32(uint256(1))); // sessionEnd in L2
bytes32 slot2After = vm.load(proxy, bytes32(uint256(2))); // bursary in L2
bytes32 slot3After = vm.load(proxy, bytes32(uint256(3))); // cutOffScore in L2
bytes32 slot4After = vm.load(proxy, bytes32(uint256(4))); // Isteacher in L2
bytes32 slot5After = vm.load(proxy, bytes32(uint256(5))); // isStudent in L2
bytes32 slot6After = vm.load(proxy, bytes32(uint256(6))); // studentScore in L2
bytes32 slot7After = vm.load(proxy, bytes32(uint256(7))); // listOfStudents in L2
bytes32 slot8After = vm.load(proxy, bytes32(uint256(8))); // listOfTeachers in L2
bytes32 slot9After = vm.load(proxy, bytes32(uint256(9))); // usdc in L2
console.log("Post-upgrade slot1 (sessionEnd):", uint256(slot1After));
console.log("Post-upgrade slot2 (bursary):", uint256(slot2After));
console.log("Post-upgrade slot3 (cutOffScore):", uint256(slot3After));
console.log("Post-upgrade slot4 (isTeacher):", uint256(slot4After));
console.log("Post-upgrade slot5 (isStudent):", uint256(slot5After));
console.log("Post-upgrade slot6 (studentScore):", uint256(slot6After));
console.log("Post-upgrade slot7 (reviewCount):", uint256(slot7After));
console.log("Post-upgrade slot8 (lastReviewTime):", uint256(slot8After));
console.log("Post-upgrade slot9 (listOfStudents):", uint256(slot9After));
}
}
forge test --mt testStorageCollisionOnUpgrade -vvv
[⠊] Compiling...
[⠊] Compiling 1 files with Solc 0.8.26
[⠒] Solc 0.8.26 finished in 850.04ms
Compiler run successful!
Ran 1 test for test/LevelTwoTest.t.sol:StorageCollisionTest
[PASS] testStorageCollisionOnUpgrade() (gas: 466988)
Logs:
Pre-upgrade slot1 (schoolFees): 5000000000000000000000
Pre-upgrade slot2 (sessionEnd): 2419201
Pre-upgrade slot3 (bursary): 5000000000000000000000
Pre-upgrade slot4 (cutOffScore): 70
Pre-upgrade slot5 (isTeacher): 0
Pre-upgrade slot6 (isStudent): 0
Pre-upgrade slot7 (studentScore): 0
Pre-upgrade slot8 (reviewCount): 0
Pre-upgrade slot9 (lastReviewTime): 0
Pre-upgrade slot10 (listOfStudents): 1
Pre-upgrade slot11 (listOfTeachers): 1
Pre-upgrade slot12 (usdc): 491460923342184218035706888008750043977755113263
Post-upgrade slot1 (sessionEnd): 5000000000000000000000
Post-upgrade slot2 (bursary): 2419201
Post-upgrade slot3 (cutOffScore): 5000000000000000000000
Post-upgrade slot4 (isTeacher): 70
Post-upgrade slot5 (isStudent): 0
Post-upgrade slot6 (studentScore): 0
Post-upgrade slot7 (reviewCount): 0
Post-upgrade slot8 (lastReviewTime): 0
Post-upgrade slot9 (listOfStudents): 0
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.33ms (3.57ms CPU time)
Ran 1 test suite in 88.20ms (14.33ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Incorrect Protocol State: - sessionEnd as 5000e18 (~158 years) allows operations beyond the intended 4-week session. bursary as 2419201 wei underfunds the program, breaking fund distribution logic.

cutOffScore as 5000e18 makes student evaluations impossible.

Access Control Failure: Misaligned isTeacher and isStudent mappings allow unauthorized actions or block legitimate ones.

Data Corruption: listOfStudents reading lastReviewTime corrupts student records, breaking enrollment tracking.

Invariant Violations: Breaks Invariant 4 and Invariant 7, undermining protocol reliability.

Tools Used

Foundry

Recommendations

To prevent the storage collision, align LevelTwo storage layout with LevelOne by maintaining the same slot assignments: Update LevelTwo.sol:
Ensure LevelTwo replicates LevelOne 12-slot layout:

contract LevelTwo is Initializable, UUPSUpgradeable {
address public principal; // Slot 0
+ uint256 public schoolFees; // Slot 1
uint256 public sessionEnd; // Slot 2
uint256 public bursary; // Slot 3
uint256 public cutOffScore; // Slot 4
mapping(address => bool) public isTeacher; // Slot 5
mapping(address => bool) public isStudent; // Slot 6
mapping(address => uint256) public studentScore; // Slot 7
+ mapping(address => uint256) public reviewCount; // Slot 8
+ mapping(address => uint256) public lastReviewTime; // Slot 9
address[] public listOfStudents; // Slot 10
address[] public listOfTeachers; // Slot 11
IERC20 public usdc; // Slot 12
modifier onlyPrincipal() {
require(msg.sender == principal, "Only principal");
_;
}
function graduate() external onlyPrincipal {
delete listOfStudents; // Reset if needed
isLevelTwo = true;
}
function getBursary() external view returns (uint256) {
return bursary;
}
function getTotalStudents() external view returns (uint256) {
return listOfStudents.length;
}
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

storage collision

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!