An upgradeable proxy keeps a tiny contract at a fixed address that never holds real logic. Every call is forwarded to an implementation contract via delegatecall, which runs the implementation's code but reads and writes the proxy's storage — msg.sender and all state stay on the proxy:
proxy.storage[slot] ←→ implementation.code
(bool ok,) = implementation.delegatecall(msg.data);
Solidity assigns storage variables to slots in declaration order, starting at slot 0. That is safe for V1 alone, but the proxy's storage is untyped — slot 0 is just 32 raw bytes. If V2 declares its variables in a different order, or inserts a new variable before the old ones, slot 0 keeps holding V1's owner address while V2's code now reads it as something else entirely:
V1: slot0=owner slot1=balance slot2=paused
V2 (careless): slot0=feeRate slot1=owner slot2=balance ...
→ V2.feeRate() actually reads the old owner address
→ V2.owner() actually reads the old balance
This is the real storage-collision bug class behind several production upgrade incidents. EIP-1967 fixes it by moving admin-critical variables (implementation address, admin, beacon) to storage slots computed as keccak256("eip1967.proxy.implementation") − 1 — a pseudo-random 256-bit slot number astronomically unlikely to collide with any sequential variable a contract author declares, regardless of how V2's layout changes.
- Naive / EIP-1967 — switch which slot-allocation strategy the proxy uses.
- Upgrade to V2 — performs the delegatecall switch with the variable count set by the slider.
- New variables slider — simulates a careless V2 that inserts variables ahead of the inherited ones instead of appending them.