Skip to main content
Sigvex

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

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 "@audited/proxy/transparent/TransparentUpgradeableProxy.sol";

// UUPS (Universal Upgradeable Proxy Standard)
import "@audited/proxy-utils/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.

References