Upgradeable proxy contracts use delegatecall to run implementation code in the proxy's storage context. If both the proxy and the implementation store variables at the same storage slot, they will overwrite each other — often corrupting the implementation address or ownership data.
A naive proxy stores the implementation address at slot 0. If the implementation also has a state variable at slot 0 (e.g., an owner), then calling the implementation via the proxy will read and write that same slot — confusing the owner and the implementation address.
// VULNERABLE — proxy stores implementation at slot 0
contract NaiveProxy {
address public implementation; // slot 0
fallback() external {
(bool ok,) = implementation.delegatecall(msg.data);
require(ok);
}
}
// Implementation also has slot 0:
contract MyLogic {
address public owner; // COLLISION with proxy's implementation slot!
function initialize(address _owner) public {
owner = _owner; // accidentally overwrites proxy.implementation!
}
}
// EIP-1967: store implementation at a pseudo-random slot
bytes32 constant IMPL_SLOT = bytes32(uint256(
keccak256("eip1967.proxy.implementation")
) - 1);
function _setImplementation(address impl) private {
assembly { sstore(IMPL_SLOT, impl) }
}
// Or use OpenZeppelin's TransparentUpgradeableProxy or UUPS
// which handle storage layout via EIP-1967 automatically