# Storage Slot Collisions in Upgradeable Contracts
Upgradeable contracts use delegatecall to separate logic and state. But delegatecall does not change where storage is read — it always uses the proxy's storage slots.
Get the layout wrong and you corrupt state. Get it very wrong and you lose funds.
How Proxy Storage Works
When a proxy delegates to an implementation:
contract Proxy {
address public implementation; // slot 0
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
contract ImplementationV1 {
address public implementation; // MUST match proxy slot 0
uint256 public balance; // slot 1
}
If the implementation does not reserve slot 0, balance would overwrite implementation and the proxy would break.
Storage Collision Example
Here is a dangerous upgrade:
// V1
contract TokenV1 {
address public owner; // slot 0
uint256 public totalSupply; // slot 1
}
// V2 — WRONG
contract TokenV2 {
uint256 public decimals; // slot 0 — COLLIDES WITH owner
address public owner; // slot 1 — COLLIDES WITH totalSupply
uint256 public totalSupply; // slot 2
}
If you upgrade to V2, decimals overwrites owner. The contract is now controlled by address(0).
EIP-1967 Standard Slots
EIP-1967 defines fixed storage slots for proxy metadata:
// Implementation slot
bytes32 constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
// Admin slot
bytes32 constant ADMIN_SLOT =
bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);
These slots are far from slot 0, so they never collide with normal state variables.
function _setImplementation(address newImplementation) internal {
assembly {
sstore(IMPLEMENTATION_SLOT, newImplementation)
}
}
Unstructured Storage Pattern
OpenZeppelin's proxy uses unstructured storage — state variables are stored at computed slots instead of sequential slots.
contract MyImplementation {
bytes32 constant OWNER_SLOT = keccak256("mycontract.owner");
function owner() public view returns (address) {
bytes32 slot = OWNER_SLOT;
address value;
assembly {
value := sload(slot)
}
return value;
}
function setOwner(address newOwner) internal {
bytes32 slot = OWNER_SLOT;
assembly {
sstore(slot, newOwner)
}
}
}
This guarantees no collision with proxy slots.
Diamond Storage (ERC-7201)
For contracts with multiple facets (like EIP-2535 Diamonds), Diamond Storage isolates each facet's state:
library TokenStorage {
bytes32 constant STORAGE_SLOT = keccak256("diamond.token.storage");
struct Layout {
mapping(address => uint256) balances;
uint256 totalSupply;
}
function layout() internal pure returns (Layout storage l) {
bytes32 slot = STORAGE_SLOT;
assembly {
l.slot := slot
}
}
}
contract TokenFacet {
function balanceOf(address user) external view returns (uint256) {
return TokenStorage.layout().balances[user];
}
}
Each facet gets its own namespace.
Tools to Detect Collisions
1. Foundry vm.load
Read storage slots directly in tests:
function testStorageLayout() public {
address proxy = address(myProxy);
bytes32 implSlot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
address impl = address(uint160(uint256(vm.load(proxy, implSlot))));
assertEq(impl, address(implementation));
}
2. OpenZeppelin Storage Layout Hash
forge inspect MyContract storage-layout --pretty
This shows the exact slot for every variable.
3. Slither Upgradeability Checks
slither . --detect upgrade-layout
Slither detects layout changes between versions.
Real-World Incidents
1. Parity Multisig (2017)
The Parity wallet library was used as an implementation via delegatecall. A user called initWallet() directly on the library, making themselves the owner. They then called kill(), destroying the library and bricking all proxies.
Loss: 513k ETH (~$150M at the time)
2. OUSD (2020)
Origin Dollar's upgrade added a new state variable at the wrong position. Rebase rewards were written to the wrong slot, corrupting balances.
Loss: $7M
Best Practices
forge inspect storage-layout before every upgradeSummary
Storage collisions are silent and catastrophic. EIP-1967 and unstructured storage solve the proxy collision problem. But you still need discipline when adding new state variables. Always validate storage layout before upgrading.