Final ERC defining well-known, collision-resistant storage slots for proxy implementation, admin, and beacon addresses. Lets block explorers, indexers, and wallets reliably detect upgradeable contracts.
- 01upgradeable contract patterns
- 02explorer/indexer detection
- 03transparent proxies
- 04UUPS proxies
- 05beacon proxies
- pnpm add @openzeppelin/contracts @openzeppelin/contracts-upgradeable
- forge install vectorized/solady
Store the implementation address at `bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)` (`0x360894...382bbc`), the admin at `keccak256('eip1967.proxy.admin') - 1` (`0xb53127...d6103`), and the beacon at `keccak256('eip1967.proxy.beacon') - 1` (`0xa3f0ad...133d50`). Use OpenZeppelin's `ERC1967Utils` (or Solady's `ERC1967`) helpers — never roll the slot constants by hand. Emit `Upgraded(address)`, `AdminChanged(address,address)`, and `BeaconUpgraded(address)` from those helpers so Etherscan, Tenderly, and Sourcify proxy detection light up automatically.
- ⚑Slot uniqueness is load-bearing — using `keccak256('eip1967.proxy.implementation')` directly (without the `- 1`) lands on a hashable preimage and explorers will not recognize the proxy.
- ⚑Hand-coding slot constants instead of importing from OpenZeppelin/Solady risks one-bit typos that silently break upgrade paths and detection.
- ⚑ERC-1967 only standardizes storage layout — it does NOT define who can upgrade. Pair it with a transparent proxy admin or UUPS `_authorizeUpgrade` for access control.
- ⚑Implementation contracts behind ERC-1967 proxies must be initializer-based (no constructor state) and use `__gap` arrays to reserve future storage in upgradeable inheritance chains.
- ⚑Forgetting to emit `Upgraded` after `_setImplementation` makes off-chain indexers miss upgrades, even though the proxy still works.