Hawk High

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

LevelTwo causes a critical storage layout mismatch with LevelOne, leading to corrupted state after upgrade

Summary

In a UUPS proxy pattern, the contract logic is upgradeable, but the storage layout must remain consistent across versions. However, LevelTwo introduces a different storage layout than LevelOne, omitting several variables and constants. This creates a storage collision, leading to corrupted values for critical state variables (e.g., sessionEnd, usdc, principal, etc.) once the upgrade is performed.

Vulnerability Details

In LevelOne, the following variables exist at the top of storage:

address principal; // slot 0
bool inSession; // slot 1
uint256 schoolFees; // slot 2 ❗️
uint256 public immutable reviewTime = 1 weeks; // slot 3 ❗️
uint256 public sessionEnd; // slot 4
...
IERC20 usdc; // last variable

In LevelTwo, several of these are missing:

address principal; // slot 0
bool inSession; // slot 1
uint256 sessionEnd; // slot 2 ❗️
...
IERC20 usdc; // shifted to wrong slot

As a result:

  • sessionEnd in LevelTwo reads from schoolFees in LevelOne

  • bursary reads from the immutable reviewTime

  • usdc points to a meaningless address

  • principal and access control may still appear correct (if it’s early in the layout), but all other logic silently breaks

This causes unpredictable behavior and renders the upgraded contract functionally broken, even if the upgrade appears successful.

Impact

  • Storage state becomes inconsistent and corrupted

  • Key values such as usdc, sessionEnd, cutOffScore, or mappings will behave incorrectly

  • Transfers may fail or go to incorrect addresses

  • Entire contract logic becomes unreliable post-upgrade

  • Cannot safely access or trust historical data

Tools Used

Recommendations

Ensure that LevelTwo exactly mirrors the full storage layout of LevelOne, in the same order, including:

  • schoolFees

  • reviewTime (even if unused — still reserves a slot)

  • reviewCount

  • lastReviewTime

Only after preserving all original variables should new state variables (or renamed constants) be appended at the end.

This is a fundamental requirement of safe upgradeable contract development.

Example correction:

contract LevelTwo is Initializable, UUPSUpgradeable {
address principal;
bool inSession;
uint256 schoolFees;
uint256 public immutable reviewTime;
uint256 sessionEnd;
uint256 bursary;
uint256 cutOffScore;
mapping(address => bool) public isTeacher;
mapping(address => bool) public isStudent;
mapping(address => uint256) public studentScore;
mapping(address => uint256) private reviewCount;
mapping(address => uint256) private lastReviewTime;
address[] listOfStudents;
address[] listOfTeachers;
// append new vars below...
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 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.