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.
In LevelOne
, the following variables exist at the top of storage:
In LevelTwo
, several of these are missing:
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.
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
Manual slot-by-slot layout comparison
Solidity storage rules (slots, mappings, constants)
OpenZeppelin Proxies documentation : https://docs.openzeppelin.com/upgrades-plugins/proxies#storage-collisions-between-implementation-versions
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:
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.