Create your own
Lesson illustration

Upgradeability Patterns: Trade-offs and Choices

Hello! Welcome to your seventh lesson in the "Advanced Solidity & Secure Development" module.

Introduction

In our last lesson, we focused on how smart contracts communicate their state changes to the outside world using events and indexed parameters. This is crucial for building responsive front-ends and off-chain services.

Today, we address a more fundamental challenge: how do you change a smart contract's logic once it's deployed? The immutability of the blockchain is a core security feature, but it presents a significant problem for evolving projects that need to fix bugs or add new features. This lesson will introduce you to the proxy patterns that make smart contract upgradeability possible.

Learning Outcome: By the end of this lesson, you will be able to compare different upgradeability patterns (e.g., Transparent vs. UUPS) and their trade-offs.

We will explore the core mechanism that enables proxies, delegatecall, and then dive into a detailed comparison of the two most prevalent patterns: the Transparent Proxy and the Universal Upgradeable Proxy Standard (UUPS). Understanding their differences is key to making informed architectural decisions for your projects.

1. The Core Problem and the Proxy Solution

Smart contracts are immutable by design. Once deployed, their code cannot be changed. While this builds trust, it means a single bug could be permanent and costly. The community developed proxy patterns to work around this limitation.

The core idea is to separate a contract's state (data) from its logic (code).

  • Proxy Contract: This contract holds the state (storage variables, token balances). Its address is permanent and is the one users interact with.
  • Implementation Contract: This contract contains the business logic. It is stateless and can be replaced with a new version to "upgrade" the system.

But how does the proxy use the implementation's logic while maintaining its own state? The magic is an EVM opcode called delegatecall.

Let's get a clear definition of the problem and the role of delegatecall.

An Introduction to Upgradeable Smart Contracts

This article, 'An Introduction to Upgradeable Smart Contracts' from QuickNode, provides an excellent primer on why upgradeability is needed and how delegatecall works.

Read the sections 'What is an Upgradeable Smart Contract?' and 'How to Make a Smart Contract Upgradeable'. Focus on understanding the distinction between a regular CALL and a DELEGATECALL, especially regarding which contract's storage is modified and how msg.sender is preserved.

To summarize delegatecall: when the Proxy contract delegatecalls the Implementation contract, it's like the Proxy is temporarily borrowing the Implementation's code and running it as its own. All state changes happen in the Proxy's storage, and msg.sender remains the original user who called the Proxy. This is the fundamental mechanism that allows logic to be swapped out while preserving state.

2. The Transparent Proxy Pattern

The Transparent Proxy Pattern was one of the first robust solutions for upgradeability, popularized by OpenZeppelin. In this pattern, the upgrade logic is contained within the proxy contract itself.

The proxy has two distinct modes of operation based on who is calling it:

  1. If the caller is a regular user: The proxy forwards the call (delegatecall) to the implementation contract.
  2. If the caller is the admin: The proxy handles the call itself, assuming it's an administrative function (like upgrading the implementation address). It will not forward the call.

This separation prevents function selector clashes, where a function in the implementation might have the same signature as an admin function in the proxy.

Let's watch a detailed breakdown of this pattern.

Smart Contract Upgradeability 101 | 5 Upgradeability Methods

This video from Owen Thurm's 'Smart Contract Upgradeability 101' series provides a clear, code-driven explanation of the Transparent Proxy pattern.

Watch the section on Transparent Proxies (08:20 - 24:19). Pay close attention to: The explanation of delegatecall and storage context. How the proxy differentiates between an admin and a regular user. The pros and cons discussed at the end of the section, particularly the admin's inability to interact with the logic.

Key Trade-offs of the Transparent Proxy:

  • Pro: It's robust against certain developer errors. Since the upgrade logic is in the proxy, you can't accidentally deploy a new implementation that forgets the upgrade mechanism, "bricking" the contract.
  • Con: The admin account cannot interact with the implementation's logic. Any call from the admin is intercepted by the proxy. This often necessitates a separate "ProxyAdmin" contract to own the proxy, adding complexity.
  • Con: It's more gas-intensive. Every single call requires an if (msg.sender == admin) check, and the proxy contract itself is larger and more expensive to deploy.

3. The UUPS (Universal Upgradeable Proxy Standard) Pattern

UUPS (EIP-1822) is a more modern and increasingly standard approach. It flips the logic of the Transparent pattern on its head.

In the UUPS pattern, the upgrade logic lives inside the implementation contract, not the proxy. The proxy becomes a very simple, minimal contract whose only job is to delegatecall every call to the current implementation address.

The implementation contract inherits functionality (e.g., from OpenZeppelin's UUPSUpgradeable contract) that includes an upgradeTo function. This function, when called, updates the implementation address stored in the proxy's storage.

Let's see how this works.

Smart Contract Upgradeability 101 | 5 Upgradeability Methods

Continuing with the same video, let's now examine the UUPS pattern.

Watch the section on UUPS (24:19 - 28:21). Focus on the key difference: where the upgrade logic resides. Understand the major risk associated with this pattern.

Key Trade-offs of UUPS:

  • Pro: It's more gas-efficient. The proxy is minimal, cheaper to deploy, and doesn't have the gas overhead of checking msg.sender on every call.
  • Pro: It's more flexible. Since the proxy doesn't have special logic, the admin account can freely interact with the implementation logic just like any other user.
  • Con: The primary risk is bricking the contract. If you deploy a new implementation version that forgets to include the upgrade logic (i.e., doesn't inherit UUPSUpgradeable), you will lose the ability to perform any future upgrades.

4. Comparing the Patterns: A Head-to-Head Summary

Now that we've seen both patterns, let's put them side-by-side to make the trade-offs crystal clear.

The Proxy Pattern in Solidity: From Zero to Hero

This article, 'The Proxy Pattern in Solidity: From Zero to Hero', contains an excellent comparison table that summarizes the key differences.

Scroll to the 'Comparison Table' within the 'Types of Proxy Patterns' section. This table provides a concise summary of the trade-offs between Transparent and UUPS proxies regarding gas cost, flexibility, and complexity.

Here is an expanded version of that comparison:

FeatureTransparent ProxyUUPS Proxy
Upgrade Logic LocationIn the Proxy contract.In the Implementation contract.
Gas Cost (Runtime)Higher. Every call includes a check for msg.sender == admin.Lower. The proxy is a minimal forwarder.
Gas Cost (Deployment)Higher. The proxy contract is more complex and larger.Lower. The proxy contract is very small.
Admin InteractionAdmin cannot call implementation functions. Requires a separate admin contract.Admin can call implementation functions.
Function ClashesSolved by routing logic in the proxy, but this creates the admin interaction issue.Not an issue at the proxy level. The proxy has no functions to clash with.
Primary RiskAdmin key compromise is the main threat.Bricking. Upgrading to an implementation without upgrade logic makes it immutable.
Industry StandardThe older, established standard.The modern, recommended standard for new projects due to gas savings and flexibility.

5. Critical "Gotchas" for All Proxy Patterns

Regardless of the pattern you choose, delegatecall-based proxies share some critical considerations that you must manage carefully to avoid catastrophic bugs.

  1. Storage Layout Consistency: The order of state variable declarations in your implementation contracts must be preserved across upgrades. If V1 has uint a; uint b; and V2 has uint b; uint a;, V2 will read the value of a from the proxy's storage into its b variable, corrupting your state. The rule is: always append new state variables to the end. Never reorder, insert, or remove existing variables.

  2. The initialize Function: A contract's constructor is only executed once when it is deployed. With a proxy, the implementation contract is deployed separately and its constructor runs in a vacuum. It does not affect the proxy's state. To set up the initial state of the proxy (like setting the owner), you must create an initialize function in your implementation that acts as a constructor. This function must be protected so it can only be called once.

This article explains these best practices clearly.

The Proxy Pattern in Solidity: From Zero to Hero

Let's review the crucial implementation details that apply to all proxy patterns.

Read the section 'Implementation Best Practices'. Focus on the 'Initialize, Don’t Construct' and 'Storage Layout Inheritance' subsections. These are non-negotiable rules for working with proxies.

Conclusion

You now have a solid theoretical foundation for smart contract upgradeability. You understand the core delegatecall mechanism and can articulate the critical differences and trade-offs between the Transparent and UUPS proxy patterns.

Key Takeaways:

  • Upgradeability separates state (in the Proxy) from logic (in the Implementation) using delegatecall.
  • Transparent Proxies place upgrade logic in the proxy. They are safer against "bricking" but are more expensive and less flexible for the admin.
  • UUPS Proxies place upgrade logic in the implementation. They are the modern standard, offering better gas efficiency and flexibility, but require developer discipline to ensure the upgrade mechanism is preserved in every new version.
  • Critical Rules: You must never change the storage layout of existing variables and must use initialize functions instead of constructors for proxy-aware setup.

Next Steps:

Theory is essential, but practice is where mastery happens. In our next lesson, we will take what we've learned today and apply it directly. You will implement an upgradeable contract using the UUPS proxy pattern, the most common choice for modern Solidity development.

Can't find a good explanation? Sign up and we'll make it for you

Sign up