The LevelTwo implementation does not maintain consistent storage layout with LevelOne, creating a dangerous mismatch between the contract's code and its storage. After upgrading, functions from LevelOne remain accessible through the proxy and operate on storage variables not declared in LevelTwo, risking silent data corruption.
After upgrading to LevelTwo, these storage slots still exist but aren't properly defined in the code. Our testing demonstrates that:
While access controls limit who can perform upgrades, the technical debt and potential for errors in future development remains significant.
function test_UpgradeableImplementationVulnerability_WithImpact() public {
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
vm.stopPrank();
console.log("==================== BEFORE UPGRADE ====================");
vm.prank(principal);
bytes32 schoolFeesSlot = bytes32(uint256(1));
uint256 customFees = 12345;
vm.store(address(levelOneProxy), schoolFeesSlot, bytes32(customFees));
uint256 verifySchoolFees = levelOneProxy.getSchoolFeesCost();
console.log("Custom School Fees set in LevelOne:", verifySchoolFees);
assertEq(verifySchoolFees, customFees, "Direct storage write failed");
levelTwoImplementation = new LevelTwo();
bytes32 implSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
vm.store(
address(levelOneProxy),
implSlot,
bytes32(uint256(uint160(address(levelTwoImplementation))))
);
vm.prank(principal);
(bool success, ) = address(levelOneProxy).call(
abi.encodeWithSignature("graduate()")
);
console.log("==================== AFTER UPGRADE ====================");
console.log("Upgrade successful:", success);
LevelTwo levelTwoProxy = LevelTwo(address(levelOneProxy));
console.log(
"LevelTwo constant TEACHER_WAGE_L2:",
levelTwoProxy.TEACHER_WAGE_L2()
);
assertTrue(
levelTwoProxy.TEACHER_WAGE_L2() == 40,
"Not using LevelTwo implementation"
);
bytes32 storedFeesBytes = vm.load(
address(levelOneProxy),
schoolFeesSlot
);
uint256 storedFees = uint256(storedFeesBytes);
console.log(
"Reading schoolFees storage slot after upgrade:",
storedFees
);
assertEq(
storedFees,
customFees,
"schoolFees was not preserved in storage"
);
uint256 corruptedValue = 999;
console.log("Corrupting schoolFees storage slot with:", corruptedValue);
vm.store(
address(levelOneProxy),
schoolFeesSlot,
bytes32(corruptedValue)
);
bytes32 corruptedBytes = vm.load(
address(levelOneProxy),
schoolFeesSlot
);
uint256 corruptedFees = uint256(corruptedBytes);
console.log("Value after corruption:", corruptedFees);
assertEq(corruptedFees, corruptedValue, "Failed to corrupt storage");
console.log("==================== IMPACT ====================");
console.log("Original schoolFees:", customFees);
console.log("Corrupted schoolFees:", corruptedFees);
console.log(
"This proves that a key state variable is still accessible in"
);
console.log("storage but not declared in the LevelTwo implementation.");
console.log(
"If LevelTwo uses this same storage slot for a different purpose,"
);
console.log("it would corrupt the original schoolFees variable.");
console.log("==================== CONCLUSION ====================");
console.log("STORAGE LAYOUT VULNERABILITY CONFIRMED:");
console.log(
"1. After upgrading to LevelTwo, the schoolFees storage slot still exists"
);
console.log(
"2. LevelTwo doesn't declare the schoolFees variable, creating a mismatch"
);
console.log(
"3. The variable can be corrupted if LevelTwo uses the same storage slot"
);
}