Summary
LevelOne.sol and LevelTwo.sol have different storage layouts, which may lead to storage collisions.
Vulnerability Details
Storage layout in LevelOne.sol below:
contract LevelOne is Initializable, UUPSUpgradeable {
address principal;
bool inSession;
uint256 schoolFees;
uint256 public sessionEnd;
uint256 public bursary;
uint256 public 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;
IERC20 usdc;
}
Storage layout in LevelTwo.sol below:
contract LevelTwo is Initializable {
address principal;
bool inSession;
uint256 public sessionEnd;
uint256 public bursary;
uint256 public cutOffScore;
mapping(address => bool) public isTeacher;
mapping(address => bool) public isStudent;
mapping(address => uint256) public studentScore;
address[] listOfStudents;
address[] listOfTeachers;
IERC20 usdc;
uint256 public constant TEACHER_WAGE_L2 = 40;
uint256 public constant PRINCIPAL_WAGE_L2 = 5;
uint256 public constant PRECISION = 100;
}
LevelOne.sol and LevelTwo.sol both are the implementation of a proxy contract, but their storage layout is not exactly the same.
Impact
Storage inconsistency can cause functions to read from or write to incorrect storage slots, leading to serious errors.
PoC
pragma solidity 0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {DeployLevelOne} from "../script/DeployLevelOne.s.sol";
import {GraduateToLevelTwo} from "../script/GraduateToLevelTwo.s.sol";
import {LevelOne} from "../src/LevelOne.sol";
import {LevelTwo} from "../src/LevelTwo.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
contract LevelOneAndGraduateTest is Test {
DeployLevelOne deployBot;
GraduateToLevelTwo graduateBot;
LevelOne levelOneProxy;
LevelTwo levelTwoImplementation;
address proxyAddress;
address levelOneImplementationAddress;
address levelTwoImplementationAddress;
MockUSDC usdc;
address principal;
uint256 schoolFees;
address alice;
address bob;
address clara;
address dan;
address eli;
address fin;
address grey;
address harriet;
function setUp() public {
deployBot = new DeployLevelOne();
proxyAddress = deployBot.deployLevelOne();
levelOneProxy = LevelOne(proxyAddress);
usdc = deployBot.getUSDC();
principal = deployBot.principal();
schoolFees = deployBot.getSchoolFees();
levelOneImplementationAddress = deployBot.getImplementationAddress();
alice = makeAddr("first_teacher");
bob = makeAddr("second_teacher");
clara = makeAddr("first_student");
dan = makeAddr("second_student");
eli = makeAddr("third_student");
fin = makeAddr("fourth_student");
grey = makeAddr("fifth_student");
harriet = makeAddr("six_student");
usdc.mint(clara, schoolFees);
usdc.mint(dan, schoolFees);
usdc.mint(eli, schoolFees);
usdc.mint(fin, schoolFees);
usdc.mint(grey, schoolFees);
usdc.mint(harriet, schoolFees);
}
function test_poc_storage_collision() external {
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
vm.record();
LevelOne(levelOneImplementationAddress).getSchoolFeesToken();
(bytes32[] memory levelOneReads,) = vm.accesses(levelOneImplementationAddress);
LevelTwo(levelTwoImplementationAddress).getSchoolFeesToken();
(bytes32[] memory levelTwoReads,) = vm.accesses(levelTwoImplementationAddress);
assertTrue(levelOneReads.length == levelTwoReads.length);
for (uint i = 0; i < levelOneReads.length; i++) {
assertTrue(levelOneReads[i] == levelTwoReads[i]);
}
}
}
Logs:
[541782] LevelOneAndGraduateTest::test_poc_storage_collision()
├─ [445078] → new LevelTwo@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 2223 bytes of code
├─ [0] VM::record()
│ └─ ← [Return]
├─ [2605] LevelOne::getSchoolFeesToken() [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000
├─ [0] VM::accesses(LevelOne: [0x34A1D3fff3958843C43aD80F30b94c510645C316])
│ └─ ← [Return] [0x000000000000000000000000000000000000000000000000000000000000000c], []
├─ [2560] LevelTwo::getSchoolFeesToken() [staticcall]
│ └─ ← [Return] 0x0000000000000000000000000000000000000000
├─ [0] VM::accesses(LevelTwo: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
│ └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000009], []
├─ [0] VM::assertTrue(true) [staticcall]
│ └─ ← [Return]
├─ [0] VM::assertTrue(false) [staticcall]
│ └─ ← [Revert] assertion failed
└─ ← [Revert] assertion failed
Tools Used
Manual.
Recommendations
Make sure that all implementation have the same storage layout.