Contract Upgradeability in Solidity

akindewaakindewa
4 min read

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 and UUPSUpgradeable 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

  1. Storage Layout Must Stay Consistent

    • Always append new variables, never reorder.
  2. No Constructors

    • Use initialize() Instead, with initializer modifier.
  3. Security Risks

    • Improper access control can lead to an upgrade takeover.
  4. 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.

3
Subscribe to my newsletter

Read articles from akindewa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

akindewa
akindewa