Smart contracts are immutable by default — but protocols need to fix bugs and add features. Proxy patterns solve this by separating storage (proxy) from logic (implementation). But each pattern has different security properties, gas costs, and complexity. Choosing wrong can cost you everything.
Pattern 1: Transparent Proxy (OpenZeppelin)
The admin interacts with the proxy's admin functions. Everyone else's calls are delegated to the implementation. This prevents function selector clashing.
// Deploy
Implementation impl = new Implementation();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
adminAddress,
abi.encodeCall(Implementation.initialize, (params))
);
Pros: Simple mental model, battle-tested. Cons: Extra gas for every call (admin check), admin can't interact with the implementation.
Pattern 2: UUPS (Universal Upgradeable Proxy Standard)
The upgrade logic lives in the implementation contract, not the proxy. Smaller proxy, cheaper calls, but forgetting upgrade logic in a new implementation makes it non-upgradeable.
contract MyProtocol is UUPSUpgradeable, OwnableUpgradeable {
function _authorizeUpgrade(address newImpl) internal override onlyOwner {}
}
Pros: Gas efficient, cleaner proxy. Cons: Implementation must include upgrade logic — if forgotten, proxy is stuck forever.
Pattern 3: Diamond (EIP-2535)
A single proxy delegates to multiple implementation contracts (facets). Each function selector routes to a different facet. Used for protocols exceeding Ethereum's 24KB contract size limit.
Pros: Modular, no size limit, partial upgrades. Cons: Complex, storage management across facets is tricky, larger attack surface.
Pattern 4: Beacon Proxy
Multiple proxies point to a single beacon that stores the implementation address. Upgrading the beacon upgrades all proxies simultaneously.
Pros: Deploy many instances efficiently, upgrade all at once. Cons: No per-instance customization of logic.
Choosing the Right Pattern
| Pattern | Best For | Gas Cost | Complexity |
|---|---|---|---|
| Transparent | Simple protocols, single admin | Higher | Low |
| UUPS | Most DeFi protocols | Lower | Medium |
| Diamond | Large protocols (>24KB) | Variable | High |
| Beacon | Factory patterns (many instances) | Lower | Medium |
Universal Upgrade Safety Rules
- ✅ Use
_disableInitializers()in implementation constructors - ✅ Never reorder or remove storage variables
- ✅ Use storage gaps (
uint256[50] __gap) for future expansion - ✅ Run storage layout validation (OpenZeppelin upgrades plugin)
- ✅ Add timelock to upgrade operations
- ✅ Test the full upgrade flow in staging before mainnet
Upgrades introduce some of the most subtle bugs. Audit your upgrade mechanism before it becomes a vulnerability.