Contract Upgradeability in Solidity

Table of contents
- Why Upgradeability Matters
- The Core Challenge
- Common Upgradeability Patterns
- 1. Proxy Pattern (Delegatecall-Based)
- 2. Beacon Proxy Pattern
- 3. Eternal Storage Pattern (Obsolete)
- 🧠 Critical Concept: Storage Collisions
- 🔐 Role of Admin and Security
- UUPS Proxy (Modern Upgradeability)
- Risks and Best Practices
- When to Use Upgradeability
- Summary

Smart contracts, once deployed on the blockchain, are immutable. That means their code can’t be changed, not a single line, not a single typo. While this immutability provides security and trust, it becomes a problem when you want to fix bugs, add features, or adapt to changes in business logic. That’s where contract upgradeability comes in.
In this article, we’ll explore:
Why upgradeability matters
The challenges of implementing it
The main patterns (with examples)
Key risks to watch out for
Why Upgradeability Matters
Imagine deploying a decentralised finance (DeFi) protocol, only to realise after launch that a small bug causes users to lose money. Without upgradeability, your only option is to deploy a new contract and ask everyone to move over, a nightmare for users and reputation.
Upgradeability lets developers:
Patch bugs or vulnerabilities
Introduce new features
Adjust to regulatory or user needs
Avoid redeploying and migrating entire systems
The Core Challenge
Smart contracts are stored at an address. Once deployed, they’re stuck in that address like concrete. The EVM (Ethereum Virtual Machine) doesn’t support changing contract logic directly. So the idea is:
Separate logic (code) from data (storage).
That way, you can swap out logic while keeping storage intact.
Common Upgradeability Patterns
Let’s look at the most popular patterns that enable upgradeability.
1. Proxy Pattern (Delegatecall-Based)
This is the most widely used pattern (used by OpenZeppelin, Uniswap, and many others).
How it works:
A proxy contract holds all the storage/state.
The proxy forwards function calls to a logic contract (implementation) using
delegatecall
.delegatecall
allows the logic code to run in the context of the proxy, meaning it accesses the proxy’s state.
📦 Components:
Proxy contract (persistent address)
Logic contract(s) (can be upgraded anytime)
Admin who controls upgrades
contract Proxy {
address public implementation;
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Call failed");
}
function upgrade(address _newImplementation) external {
implementation = _newImplementation;
}
}
Pros:
Flexible: can upgrade logic multiple times
Keeps the same address and storage
⚠️ Cons:
Needs careful storage layout (more on this below)
If misused, it can introduce vulnerabilities
2. Beacon Proxy Pattern
This pattern improves on the proxy by using a beacon to manage upgrades for multiple proxies.
📦 Roles:
Beacon stores the implementation address.
All proxies refer to the beacon.
Upgrading the beacon upgrades all proxies.
Useful for systems like ERC-1155 or NFT collections where multiple similar contracts are deployed.
3. Eternal Storage Pattern (Obsolete)
This older method separates logic and storage via manual mapping. But it's harder to maintain and has been replaced by proxy patterns. You likely won’t use it, but it's good to know it existed.
🧠 Critical Concept: Storage Collisions
When using delegatecall
The logic contract must be carefully designed to match the storage layout of the proxy. If the layout doesn’t align, you’ll get bugs or total contract failure.
Best practice: Use OpenZeppelin’s
Initializable
andUUPSUpgradeable
contracts to manage layout safely.
🔐 Role of Admin and Security
Since upgradeable contracts allow logic to change, who controls upgrades becomes very important.
Questions to ask:
Who is the admin?
Can upgrades be paused or vetoed?
Are changes transparent to users?
It is also important to implement timelocks, multi-sig approvals, or DAO-based governance.
UUPS Proxy (Modern Upgradeability)
UUPS (Universal Upgradeable Proxy Standard) is a lighter version of the proxy pattern. Instead of the proxy holding the upgrade logic, the logic contract includes an upgradeTo
function itself.
Benefits:
Gas efficient (logic contract handles upgrade)
Preferred by OpenZeppelin in newer projects
Example:
contract MyContract is UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation)
internal override onlyOwner {}
}
Risks and Best Practices
Storage Layout Must Stay Consistent
- Always append new variables, never reorder.
No Constructors
- Use
initialize()
Instead, withinitializer
modifier.
- Use
Security Risks
- Improper access control can lead to an upgrade takeover.
Upgrades Should Be Transparent
- Consider on-chain logs, community voting, or public upgrade delay.
When to Use Upgradeability
Projects that evolve (DeFi, DAOs, protocols)
Smart contracts with complex logic or business models
Experimental projects where bugs might show up later
Summary
Upgradeability is both a superpower and a double-edged sword. It allows flexibility in a rigid blockchain world, but introduces new responsibilities. If done right, it becomes a core pillar of maintainable and scalable smart contract systems.
Subscribe to my newsletter
Read articles from akindewa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
