Hawk High

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

[H] storage layout inconsistent

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 {
// slot: 0
address principal;
// slot: 0, offset: 20
bool inSession;
// slot: 1
uint256 schoolFees;
// slot: 2
uint256 public sessionEnd;
// slot: 3
uint256 public bursary;
// slot: 4
uint256 public cutOffScore;
// slot: 5
mapping(address => bool) public isTeacher;
// slot: 6
mapping(address => bool) public isStudent;
// slot: 7
mapping(address => uint256) public studentScore;
// slot: 8
mapping(address => uint256) private reviewCount;
// slot: 9
mapping(address => uint256) private lastReviewTime;
// slot: 10
address[] listOfStudents;
// slot: 11
address[] listOfTeachers;
// slot:12
IERC20 usdc;
}

Storage layout in LevelTwo.sol below:

contract LevelTwo is Initializable {
// slot: 0
address principal;
// slot: 0, offset: 20
bool inSession;
// 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
address[] listOfStudents;
// slot: 8
address[] listOfTeachers;
// slot: 9
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

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
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;
// teachers
address alice;
address bob;
// students
address clara;
address dan;
address eli;
address fin;
address grey;
address harriet;
function setUp() public {
deployBot = new DeployLevelOne();
proxyAddress = deployBot.deployLevelOne();
levelOneProxy = LevelOne(proxyAddress);
// graduateBot = new GraduateToLevelTwo();
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);
// trace slots that `getSchoolFeesToken()` will effect
vm.record();
// levelOne.sol
LevelOne(levelOneImplementationAddress).getSchoolFeesToken();
(bytes32[] memory levelOneReads,) = vm.accesses(levelOneImplementationAddress);
// levelTwo.sol
LevelTwo(levelTwoImplementationAddress).getSchoolFeesToken();
(bytes32[] memory levelTwoReads,) = vm.accesses(levelTwoImplementationAddress);
// assertion: revert if storage collision
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.

Updates

Lead Judging Commences

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

storage collision

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