Introduction
Smart contracts on Ethereum are immutable by design. Once deployed, their bytecode cannot be changed. This creates a fundamental tension: how do you fix bugs, patch vulnerabilities, or add features to a live contract managing millions of dollars? The answer is the proxy pattern — a delegate call architecture that separates storage from logic, allowing the logic contract to be swapped while preserving state.
This article explores the two dominant proxy patterns (Transparent and UUPS), examines the storage collision risks that have caused real exploits, and walks through secure implementation practices.
How Proxy Delegation Works
At its core, a proxy contract uses delegatecall to execute code from an implementation contract in the context of the proxy's storage. The proxy holds all state variables and ETH, while the implementation holds only logic.
- Receives the call and hits the
fallback()function - Reads the implementation address from a specific storage slot
- Forwards the entire calldata via
delegatecall - Returns or reverts based on the implementation's response
Transparent Proxy Pattern (TPP)
The Transparent Proxy (EIP-1967) solves the function selector clash problem by routing calls differently based on whether the caller is the admin or a regular user. Admin calls go to the proxy's own functions (upgrade, admin transfer), while all other calls are delegated to the implementation.
The downside: every call requires an additional SLOAD to check the admin address, costing ~2,100 extra gas per transaction.
UUPS Pattern
Universal Upgradeable Proxy Standard (EIP-1822) moves the upgrade logic into the implementation contract itself. The proxy becomes extremely lightweight — just a fallback + constructor. This saves gas on every call but requires every new implementation to include the upgrade mechanism.
If you deploy a new implementation without
_authorizeUpgrade(), the proxy becomes permanently frozen. This is the most common UUPS footgun.
Storage Collision Risks
The most dangerous pitfall in proxy patterns is storage collision. Since the proxy and implementation share the same storage space (via delegatecall), any mismatch in storage layout between upgrades can corrupt data silently.
- Never reorder or remove existing state variables
- Only append new variables at the end
- Use storage gaps (
uint256[50] __gap) for future-proofing - Use OpenZeppelin's storage checker in CI
Comparison: TPP vs UUPS
Both patterns are battle-tested, but they serve different needs. TPP is simpler to reason about and harder to brick, while UUPS is more gas-efficient and flexible.
Secure Implementation Checklist
- Use
initializermodifier instead of constructors - Call
_disableInitializers()in implementation constructor - Implement
_authorizeUpgradewith proper access control - Add storage gaps in all base contracts
- Run OpenZeppelin's upgrade safety checks in CI
- Use a timelock or multisig for upgrade governance
- Test the full upgrade cycle in staging before mainnet
Commentaires
Les commentaires utilisent GitHub Discussions via Giscus. Connectez-vous avec GitHub pour participer.
Activez Giscus sur votre repo GitHub pour afficher les commentaires.