Upgradeable Contracts and Proxy Patterns
The Immutability Problem
Deployed bytecode is fixed, but a contract can delegate execution to another implementation. A proxy uses that indirection to support changes while keeping its address and state.
In practice, most protocols need upgradability - to fix bugs, add features, or respond to governance decisions. Proxy patterns solve this.
How Proxy Patterns Work
The core idea: separate the contract you call (the proxy) from the code that runs (the implementation).
- Users interact with the Proxy contract (fixed address, holds all storage).
- The Proxy uses
delegatecallto forward execution to the Implementation contract. delegatecallexecutes the implementation's code but uses the proxy's storage and context.- To upgrade, you deploy a new implementation and tell the proxy to point to it.
The user's experience never changes - they always interact with the same address. But the logic behind that address can be swapped.
Common Proxy Patterns
Transparent Proxy (OpenZeppelin)
The most widely used pattern. Key rules:
- The admin can upgrade the implementation but cannot call implementation functions.
- Regular users can call implementation functions but cannot upgrade.
- This separation prevents the admin from accidentally calling implementation functions.
UUPS (Universal Upgradeable Proxy Standard)
The upgrade logic lives in the implementation contract itself, not in the proxy:
- Smaller proxy contract (cheaper to deploy).
- The implementation must include the upgrade function.
- If you deploy an implementation without the upgrade function, the contract becomes permanently non-upgradeable (a dangerous foot-gun).
Diamond Pattern (EIP-2535)
Multiple implementation contracts ("facets") share a single proxy:
- Each function can be routed to a different facet.
- Enables modular architecture for complex protocols.
- More complex to audit but more flexible.
Storage layout compatibility
The most dangerous pitfall in proxy upgrades is storage collision. Since the proxy and implementation share storage, they must use the same storage layout.
If Version 1 has:
slot 0: address owner
slot 1: uint256 balance
And Version 2 changes to:
slot 0: uint256 totalSupply // ← COLLISION: overwrites owner!
slot 1: address owner
The new code reads slot 0 as totalSupply, but it still contains the old owner address. This corrupts state silently and can be catastrophic.
Rules to prevent collision:
- Never reorder or remove existing storage variables.
- Only append new variables at the end.
- Use storage gaps (reserved empty slots) to leave room for future variables.
- Use tools like OpenZeppelin's Upgrade Plugins which automatically check for collisions.
Security Considerations
Who Controls Upgrades?
An upgrade authority may be able to change contract behavior and access to assets. Review:
- Multisig: Require multiple signatures (e.g., 3/5 Gnosis Safe).
- Timelock: Enforce a delay (24-48 hours) between initiating and executing an upgrade. This gives users time to exit if they disagree.
- Governance: For mature protocols, upgrades should require token-holder votes.
Audit Checklist for Proxy Contracts
- Verify the proxy pattern (Transparent, UUPS, or Diamond).
- Check who controls the upgrade admin - is it a multisig with a timelock?
- Verify storage layout compatibility between versions.
- Ensure the implementation's
initialize()function can only be called once. - Check for
selfdestructin the implementation (it would destroy the implementation, not the proxy, but can still cause issues).
Initializing the proxy and locking the implementation
Initialize proxy state atomically as part of deployment where supported. The implementation contract should have its own initializers disabled so it cannot be taken over directly. These are separate operations; setting state on the implementation does not initialize the proxy's storage.
Key Takeaways
- Proxy patterns enable upgradability by separating storage (proxy) from logic (implementation).
- Storage collisions are the most common and dangerous upgrade bug.
- Review upgrade authorization, signing arrangements, delays, and the actions the authority can perform.
- Initialize proxy state and disable implementation initializers using the framework's documented deployment pattern.
- Use OpenZeppelin's upgrade tools to catch storage layout errors automatically.
Quiz: Upgradeable Contracts and Proxy Patterns
1 / 5Why can't smart contracts be updated directly?