Storage Collision Remediation
How to prevent proxy storage collisions by using EIP-1967 unstructured storage slots for all proxy administrative variables.
Storage Collision Remediation
Overview
Proxy storage collisions occur when a proxy stores administrative data (implementation address, admin address) in sequential storage slots that overlap with the implementation contract’s own state variables. The remediation is to use EIP-1967 unstructured storage slots — deterministic pseudo-random positions far from slot 0 — for all proxy administrative data. Never use sequential slot 0, 1, 2 for proxy internals.
Related Detector: Storage Collision
Recommended Fix
Before (Vulnerable)
// Naive proxy — implementation at slot 0 collides with implementation's variables
contract NaiveProxy {
address public implementation; // slot 0 — COLLISION with implementation slot 0!
address public admin; // slot 1 — COLLISION with implementation slot 1!
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success);
}
}
After (Fixed)
// EIP-1967 compliant proxy — slots at keccak256-derived positions
contract EIP1967Proxy {
// keccak256('eip1967.proxy.implementation') - 1
bytes32 private constant _IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
// keccak256('eip1967.proxy.admin') - 1
bytes32 private constant _ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
function _getImplementation() internal view returns (address impl) {
assembly { impl := sload(_IMPLEMENTATION_SLOT) }
}
function _setImplementation(address newImpl) internal {
require(newImpl.code.length > 0, "Not a contract");
assembly { sstore(_IMPLEMENTATION_SLOT, newImpl) }
}
fallback() external payable {
address impl = _getImplementation();
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()) }
}
}
}
Alternative Mitigations
Use OpenZeppelin’s battle-tested proxies — avoid implementing proxy storage from scratch:
// Transparent Upgradeable Proxy (recommended for most cases)
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
// UUPS (Universal Upgradeable Proxy Standard)
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyImplementation is UUPSUpgradeable {
function _authorizeUpgrade(address) internal override onlyOwner {}
}
Storage gap pattern for upgradeable implementations — reserve unused storage slots to prevent future inheritance collisions:
contract BaseUpgradeable {
uint256 public value;
// Reserve 49 slots for future use — prevents layout shifts in upgrades
uint256[49] private __gap;
}
Common Mistakes
Using storage inheritance without gaps — Solidity inheritance assigns slots sequentially across parent and child contracts. Adding a new state variable to a parent in an upgrade shifts all child slots.
Storing admin data in implementation contract — the implementation contract must be stateless with respect to proxy admin data. All proxy configuration belongs in EIP-1967 slots, not in implementation variables.
Not validating the new implementation address — always check that newImplementation.code.length > 0 before storing. Upgrading to an EOA (externally-owned account) destroys the proxy.