5/5
## A Guide to Upgradeable Smart Contracts Blockchains derive their power from three core properties: they are decentralized, transparent, and immutable. Smart contracts, as programs running on a blockchain, inherit these characteristics. Immutability, in particular, is a foundational feature that ensures a contract's logic, once deployed, cannot be altered. This creates a trustless environment where users can verify the code and be certain its rules will never change. This immutability acts as a double-edged sword. While it guarantees a single source of truth and removes the need to trust a central party, it also presents significant challenges. What happens if a critical bug is discovered? How can a protocol evolve to add new features or implement gas-saving optimizations? Because the deployed code is permanent, these changes are impossible in a strictly immutable system. To address these limitations, the web3 ecosystem developed the concept of **upgradeable smart contracts**, providing a way to modify logic while preserving a contract's state and address. ### The Core Distinction: Logic vs. State To grasp how a contract can be "upgraded" despite being immutable, you must distinguish between its logic and its state. * **Logic:** This is the contract's code—the functions, rules, and operations you write in Solidity. Once deployed to the blockchain, this logic is unchangeable. * **State:** This is the data stored within the contract's variables, such as user balances, ownership records, or configuration settings. The contract's logic is specifically designed to read and modify this state. Consider this simple `Counter` contract: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Counter { uint256 public number; // This is the STATE // This is the LOGIC function increment() public { number += 1; } } ``` After deployment, the `increment()` function (the logic) is permanently fixed. However, the value of the `number` variable (the state) is designed to change every time `increment()` is called. Immutability applies to the code, not the data it manages. Upgradeability patterns leverage this separation to swap out the logic while keeping the state intact. ### Three Methods for Managing Contract Changes Developers have three primary methods for handling contract evolution, each with distinct trade-offs in decentralization, complexity, and user experience. #### 1. The Parameterization Method This is the simplest approach and is less of a true "upgrade" and more of a pre-planned configuration. It involves building flexibility into the initial contract by including special functions, often called **setter functions**, that allow a privileged address (like an owner) to modify key variables. For instance, you could add a `setNumber` function to the `Counter` contract, allowing an owner to change its value directly. This is useful for adjusting parameters like fee percentages or oracle addresses. * **Advantages:** * Extremely simple to implement. * **Disadvantages:** * **Limited Scope:** You can only change pre-defined variables, not fix bugs in function logic or add entirely new capabilities. * **Requires Foresight:** You must anticipate every variable that might need changing *before* the initial deployment. * **Centralization Risk:** Granting a single address the power to change critical parameters undermines decentralization. This risk can be mitigated by assigning control of these functions to a multi-signature wallet or a decentralized governance contract. #### 2. The Social Migration Method This method fully embraces immutability. Instead of modifying an existing contract, the development team deploys an entirely new, improved version of the contract. The team then leads a "social migration," encouraging the community, users, and integrated protocols to move their activity and assets to the new contract address. **Uniswap** is the canonical example of this method. They have released V1, V2, and V3 as distinct, independent sets of contracts. While liquidity and usage naturally gravitate toward the newest version, the older ones remain functional and immutable on the blockchain. * **Advantages:** * **Maintains True Immutability:** The original contracts are untouched, preserving their decentralized and trustless nature. * **No Centralized Upgrade Power:** There is no special function that an admin could exploit. * **Complete Flexibility:** Allows for a total architectural overhaul between versions. * **Disadvantages:** * **High Coordination Cost:** Persuading an entire ecosystem to migrate is a massive and difficult undertaking. * **Complex State Migration:** Manually moving user data like balances and approvals from the old contract to the new one is a complex, costly, and error-prone process. * **Fragmented Liquidity:** In DeFi, this method splits liquidity across multiple versions of the protocol, reducing capital efficiency. #### 3. The Proxy Pattern The proxy pattern is the most common and powerful method for implementing true upgradeability. This architectural pattern cleverly separates a contract's state and address from its logic. It works using two main components: 1. **The Proxy Contract:** This is the contract that users interact with. It holds the contract's state (all storage variables) and has a stable, permanent address. 2. **The Implementation Contract:** This contract contains all the business logic (the functions). It is stateless. When a user calls a function on the Proxy Contract, the proxy uses a low-level EVM function called `delegatecall` to forward the call to the current Implementation Contract. The `delegatecall` function is special: it executes the code from the Implementation Contract, but it does so within the context of the Proxy Contract's storage. This means the logic from the implementation acts directly on the state stored in the proxy. To perform an upgrade, developers simply deploy a new Implementation Contract (V2) with the updated logic. They then execute a single transaction on the Proxy Contract to tell it to point to the address of this new implementation. Users continue interacting with the same proxy address, completely unaware that the underlying logic has been replaced. The state is seamlessly preserved because it has always lived in the proxy. ### Security Considerations for Proxy Patterns While powerful, proxy patterns introduce new layers of complexity and critical security risks that must be managed carefully. **Storage Clashes** A `delegatecall` applies the implementation's logic to the proxy's storage layout. Solidity assigns storage variables to "slots" based on their order of declaration in the code, not by their names. If a V2 implementation changes the order of variables, declares a new variable before an existing one, or changes a variable's type, it will misinterpret the data stored in the proxy's slots. This leads to state corruption, where the contract reads and writes to the wrong variables, with catastrophic results. **The Golden Rule of Proxy Storage:** When upgrading, you can **only append new state variables**. You must never reorder, remove, or change the type of existing state variables. **Function Selector Clashes** A function selector is the first four bytes of the cryptographic hash of a function's signature. Because this identifier is so short, it is possible for two different functions (e.g., `transfer(address,uint256)` and `destroy(string)`) to have the exact same 4-byte selector. This is known as a function selector clash. The risk arises if an administrative function in the proxy (like `changeAdmin(address)`) has the same selector as a user-facing function in the implementation. A regular user could inadvertently call the proxy's admin function, potentially leading to a hostile takeover of the contract. ### Common Proxy Patterns and Their Solutions To mitigate these risks, several standardized proxy patterns have emerged. 1. **Transparent Proxy Pattern:** This pattern solves function selector clashes by adding routing logic to the proxy. It inspects the address of the caller (`msg.sender`). If the caller is the designated admin, the call is handled by the proxy's own logic. If the caller is any other user, the call is delegated to the implementation. This separation prevents users from ever calling admin functions and vice-versa, even if a selector clash exists. 2. **UUPS (Universal Upgradeable Proxy Standard - EIP-1822):** This pattern, which stands for Universal Upgradeable Proxy Standard, moves the upgrade logic itself out of the proxy and into the implementation contract. This makes the proxy contract smaller, cheaper to deploy, and more universal. It also solves selector clashes by design, as the Solidity compiler will not allow two functions with the same selector to exist within the same contract (the implementation). 3. **Diamond Proxy Pattern (EIP-2535):** This is a highly advanced, modular pattern. Instead of pointing to a single implementation, a Diamond proxy can delegate calls to *multiple* implementation contracts, known as "facets." A central mapping within the proxy routes each function selector to its corresponding facet. This allows for granular upgrades (updating only one part of a complex system) and helps developers manage contracts that would otherwise exceed the maximum contract size limit. ### A Final Word of Caution Upgradeable smart contracts are a powerful tool, but they should not be the default choice. They introduce centralization, as a privileged address must be able to authorize upgrades. They also add significant complexity and new attack surfaces. The primary goal of web3 development should always be to progress towards decentralized, immutable systems. Use upgradeability as a means to an end—to fix critical bugs and safely transition to a more secure state—but do so with extreme caution and a clear plan to eventually relinquish control.
Blockchains derive their power from three core properties: they are decentralized, transparent, and immutable. Smart contracts, as programs running on a blockchain, inherit these characteristics. Immutability, in particular, is a foundational feature that ensures a contract's logic, once deployed, cannot be altered. This creates a trustless environment where users can verify the code and be certain its rules will never change.
This immutability acts as a double-edged sword. While it guarantees a single source of truth and removes the need to trust a central party, it also presents significant challenges. What happens if a critical bug is discovered? How can a protocol evolve to add new features or implement gas-saving optimizations? Because the deployed code is permanent, these changes are impossible in a strictly immutable system.
To address these limitations, the web3 ecosystem developed the concept of upgradeable smart contracts, providing a way to modify logic while preserving a contract's state and address.
To grasp how a contract can be "upgraded" despite being immutable, you must distinguish between its logic and its state.
Logic: This is the contract's code—the functions, rules, and operations you write in Solidity. Once deployed to the blockchain, this logic is unchangeable.
State: This is the data stored within the contract's variables, such as user balances, ownership records, or configuration settings. The contract's logic is specifically designed to read and modify this state.
Consider this simple Counter contract:
After deployment, the increment() function (the logic) is permanently fixed. However, the value of the number variable (the state) is designed to change every time increment() is called. Immutability applies to the code, not the data it manages. Upgradeability patterns leverage this separation to swap out the logic while keeping the state intact.
Developers have three primary methods for handling contract evolution, each with distinct trade-offs in decentralization, complexity, and user experience.
This is the simplest approach and is less of a true "upgrade" and more of a pre-planned configuration. It involves building flexibility into the initial contract by including special functions, often called setter functions, that allow a privileged address (like an owner) to modify key variables.
For instance, you could add a setNumber function to the Counter contract, allowing an owner to change its value directly. This is useful for adjusting parameters like fee percentages or oracle addresses.
Advantages:
Extremely simple to implement.
Disadvantages:
Limited Scope: You can only change pre-defined variables, not fix bugs in function logic or add entirely new capabilities.
Requires Foresight: You must anticipate every variable that might need changing before the initial deployment.
Centralization Risk: Granting a single address the power to change critical parameters undermines decentralization. This risk can be mitigated by assigning control of these functions to a multi-signature wallet or a decentralized governance contract.
This method fully embraces immutability. Instead of modifying an existing contract, the development team deploys an entirely new, improved version of the contract. The team then leads a "social migration," encouraging the community, users, and integrated protocols to move their activity and assets to the new contract address.
Uniswap is the canonical example of this method. They have released V1, V2, and V3 as distinct, independent sets of contracts. While liquidity and usage naturally gravitate toward the newest version, the older ones remain functional and immutable on the blockchain.
Advantages:
Maintains True Immutability: The original contracts are untouched, preserving their decentralized and trustless nature.
No Centralized Upgrade Power: There is no special function that an admin could exploit.
Complete Flexibility: Allows for a total architectural overhaul between versions.
Disadvantages:
High Coordination Cost: Persuading an entire ecosystem to migrate is a massive and difficult undertaking.
Complex State Migration: Manually moving user data like balances and approvals from the old contract to the new one is a complex, costly, and error-prone process.
Fragmented Liquidity: In DeFi, this method splits liquidity across multiple versions of the protocol, reducing capital efficiency.
The proxy pattern is the most common and powerful method for implementing true upgradeability. This architectural pattern cleverly separates a contract's state and address from its logic.
It works using two main components:
The Proxy Contract: This is the contract that users interact with. It holds the contract's state (all storage variables) and has a stable, permanent address.
The Implementation Contract: This contract contains all the business logic (the functions). It is stateless.
When a user calls a function on the Proxy Contract, the proxy uses a low-level EVM function called delegatecall to forward the call to the current Implementation Contract. The delegatecall function is special: it executes the code from the Implementation Contract, but it does so within the context of the Proxy Contract's storage. This means the logic from the implementation acts directly on the state stored in the proxy.
To perform an upgrade, developers simply deploy a new Implementation Contract (V2) with the updated logic. They then execute a single transaction on the Proxy Contract to tell it to point to the address of this new implementation. Users continue interacting with the same proxy address, completely unaware that the underlying logic has been replaced. The state is seamlessly preserved because it has always lived in the proxy.
While powerful, proxy patterns introduce new layers of complexity and critical security risks that must be managed carefully.
Storage Clashes
A delegatecall applies the implementation's logic to the proxy's storage layout. Solidity assigns storage variables to "slots" based on their order of declaration in the code, not by their names. If a V2 implementation changes the order of variables, declares a new variable before an existing one, or changes a variable's type, it will misinterpret the data stored in the proxy's slots. This leads to state corruption, where the contract reads and writes to the wrong variables, with catastrophic results.
The Golden Rule of Proxy Storage: When upgrading, you can only append new state variables. You must never reorder, remove, or change the type of existing state variables.
Function Selector Clashes
A function selector is the first four bytes of the cryptographic hash of a function's signature. Because this identifier is so short, it is possible for two different functions (e.g., transfer(address,uint256) and destroy(string)) to have the exact same 4-byte selector. This is known as a function selector clash.
The risk arises if an administrative function in the proxy (like changeAdmin(address)) has the same selector as a user-facing function in the implementation. A regular user could inadvertently call the proxy's admin function, potentially leading to a hostile takeover of the contract.
To mitigate these risks, several standardized proxy patterns have emerged.
Transparent Proxy Pattern: This pattern solves function selector clashes by adding routing logic to the proxy. It inspects the address of the caller (msg.sender). If the caller is the designated admin, the call is handled by the proxy's own logic. If the caller is any other user, the call is delegated to the implementation. This separation prevents users from ever calling admin functions and vice-versa, even if a selector clash exists.
UUPS (Universal Upgradeable Proxy Standard - EIP-1822): This pattern, which stands for Universal Upgradeable Proxy Standard, moves the upgrade logic itself out of the proxy and into the implementation contract. This makes the proxy contract smaller, cheaper to deploy, and more universal. It also solves selector clashes by design, as the Solidity compiler will not allow two functions with the same selector to exist within the same contract (the implementation).
Diamond Proxy Pattern (EIP-2535): This is a highly advanced, modular pattern. Instead of pointing to a single implementation, a Diamond proxy can delegate calls to multiple implementation contracts, known as "facets." A central mapping within the proxy routes each function selector to its corresponding facet. This allows for granular upgrades (updating only one part of a complex system) and helps developers manage contracts that would otherwise exceed the maximum contract size limit.
Upgradeable smart contracts are a powerful tool, but they should not be the default choice. They introduce centralization, as a privileged address must be able to authorize upgrades. They also add significant complexity and new attack surfaces. The primary goal of web3 development should always be to progress towards decentralized, immutable systems. Use upgradeability as a means to an end—to fix critical bugs and safely transition to a more secure state—but do so with extreme caution and a clear plan to eventually relinquish control.
A comprehensive introduction to Upgradeable Smart Contracts & Proxies - Weigh the benefits against the risks of upgradability for immutable smart contracts and explore different upgrade patterns, including parameterization, social migration, and proxies.
Previous lesson
Previous
Next lesson
Next
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on February 17, 2026
Duration: 37min
Duration: 3h 06min
Duration: 5h 03min
Duration: 6h 22min
Duration: 2h 48min
Duration: 1h 24min
Duration: 4h 28min
Duration: 1h 20min
Duration: 1h 11min
Course Overview
About the course
Advanced smart contract development
How to develop a stablecoin
How to develop a DeFi protocol
How to develop a DAO
Advanced smart contracts testing
Fuzz testing
Manual verification
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Web3 engineer, educator, and Cyfrin co-founder. Patrick's smart contract development and security courses have helped hundreds of thousands of engineers kickstarting their careers into web3.
Guest lecturers:
Last updated on February 17, 2026