7 min read
By default, smart contracts deployed on the Ethereum blockchain are immutable. While this helps achieve decentralization and security, it can reduce the functionality of a smart contract.
Solving this problem requires using upgradeable smart contract patterns during development. Upgradeable smart contracts, created using proxy patterns, enable developers to modify contract functionality after deployment—without harming security or decentralization.
What We Will Do
- Define Upgradeable Smart Contracts and summarize their use-cases
- Understand how upgradeable smart contracts work
- Learn what is needed to create an upgradeable smart contract
What You Will Need
A basic understanding of:
- Smart contracts
What is a Smart Contract?
Before delving into upgradable smart contracts, let us recap what a smart contract is. In simple terms, a smart contract is a program that runs at a specific address on the blockchain. Smart contracts are self-executing in that they can perform operations automatically, provided the right functions are called. For example, a smart contract can send tokens to your address once you call the specific function.
Ethereum smart contracts are deployed in the Ethereum Virtual Machine (EVM), a Turing-complete virtual machine that enables the execution of code on the blockchain. A basic rule of the EVM environment is that contract code cannot be altered after deployment, which explains why smart contracts are labeled "immutable."
What is an Upgradeable Smart Contract?
Contrary to popular belief, smart contracts *can* be upgraded. However, it is important to clarify that "upgradeable" and "mutable" are two different concepts.
An upgradeable smart contract uses a special feature called a "proxy pattern" that gives developers some leeway to modify contract logic post-deployment. Below is a breakdown of the components in the proxy-contract architecture:
1. The proxy contract
2. The execution or logic contract
The first contract (proxy contract) is deployed at the start and contains the contract storage and balance. Meanwhile, the second contract (execution contract) stores contract logic used in executing functions.
In the proxy pattern, the proxy contract stores the address of the execution contract. When users send requests, the message goes through the proxy contract, which routes it to the execution contract. Afterward, the proxy contract receives the result of the computation from the execution contract and returns it to the user.
Now, the proxy contract itself cannot be modified. However, we can create additional execution contracts with updated contract logic and reroute message calls to the new contract. With proxy patterns, you are not changing the smart contract in any way. All you are doing is deploying a new logic contract and asking the proxy contract to reference it instead of the old contract. This new logic contract can have additional functionality or fixes for an old bug.
Why Use an Upgradeable Contract?
Part of Ethereum's appeal lies in the immutability of smart contracts. Users trust that developers cannot implement arbitrary changes, which encourages them to interact freely with decentralized applications.
The changing nature of an upgradeable smart contract can, therefore, raise concerns—especially around decentralization and security. However, there are significant improvements that can come from implementing proxy patterns in smart contracts, such as:
Like every program, smart contracts often have flaws and vulnerabilities. But, without the option to upgrade a contract, fixing a deadly bug is difficult—if not outright impossible.
Using an upgradeable smart contract makes it easier for developers to solve identified problems. You only have to create a new execution contract and update the proxy contract to point away from the old contract.
With an option to fix bugs, smart contract developers can assure users of safety.
Making Improvements to the Product
As decentralized applications (dapps) go mainstream, they will need constant improvements. For this to work, the smart contract—which serves as the backend—must be open to modification.
Upgradeable smart contract patterns will enable the addition of new functionality and improve overall user experience. However, it is important that control over upgrades is decentralized, to avoid malicious actions.
How to Make a Smart Contract Upgradeable
Since most proxy patterns rely on the DELEGATECALL EVM opcode, we will explain how delegate calls work briefly:
In a regular message call (CALL) between two contracts, the calling contract (contract A) sends data payload to the called contract (contract B). Afterward, contract B executes contract logic and returns the result to contract A. You must understand two things about regular message calls:
- The called contract (contract B) always reads and writes to its storage.
- Information about the message call, such as msg.sender and msg.value can change during a message call.
In a delegate call (DELEGATECALL), the called contract (contract B) holds the code used in performing operations. However, the actual logic execution happens within the context of the calling contract (contract A). This has the following implications:
- contract A can execute logic stored in contract B as if calling an internal function.
- Read-write operations executed in contract B only affect contract A's storage.
- The msg.sender and msg.value remain the same during the call.
From the Solidity docs:
This [delegate calls] means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.
Once you understand delegate calls, the idea of a proxy pattern becomes easier to grasp. The proxy contract holds the address of the logic contract and delegates all calls to it. And since the proxy uses the DELEGATECALL function, it can use code from the logic contract for operations.
Beyond delegate calls, upgradeable contracts need another function for controlling upgrades. This function is what you use to change the address of the execution contract referenced in the proxy.
A simple method is to build the upgrade function into the proxy contract and use onlyAdmin to prevent unauthorized upgrades. This ensures that only entities with admin-level access can upgrade the proxy to a new logic contract.
Developers like this pattern since the proxy stores upgrade logic and the execution contract can accept delegate calls without needing complex design. However, using a simple proxy pattern introduces "the function selector clash" problem. This informed the development of a new pattern, called the "transparent proxy".
As explained earlier, simple proxy contracts use the DELEGATECALL function to redirect function calls to the logic contract, which holds the executable code. The proxy also uses a function to change the logic contract’s address to enable contract upgrades.
However, this approach can run into problems if the proxy’s upgrade management function has the same identifiers as another function in the logic contract. In this case, it becomes difficult to know what contract got called—the proxy contract or the logic contract?
Function selector clashes can lead to errors or, worse, malicious exploits. To solve the problem, the Open Zeppelin team developed the Transparent Proxy Pattern. Below is a breakdown of how the transparent proxy pattern works.
The transparent proxy pattern interprets message calls based on their origin (msg.sender):
- If msg.sender points to an external address, the proxy automatically delegates it to the logic contract. This is even if the function called is similar to any of the proxy’s internal functions.
- If msg.sender points to an administrator, the proxy does not delegate the call. Instead, it will perform the operation (provided it can understand the function).
The implication is that only administrators can call the proxy management function. If an external address calls a clashing logic contract function, the operation simply reverts. The only problem with using the transparent proxy pattern is that you must pay higher gas fees. That’s because the EVM needs extra gas to load the execution contract address for each call and also to execute the delegation logic. Moreover, the transparent proxy pattern itself is more expensive to deploy than average.
Universal Upgrade Proxy Standard
Another, albeit less-popular proxy pattern, is the Universal Upgrade Proxy Standard (UUPS). Like the transparent proxy pattern, universal upgradable proxies use the DELEGATECALL function. The main difference is that the logic contract, not the proxy, manages the upgrade functionality.
In this pattern, the logic contract still writes to the proxy contract’s storage and has its address stored in the latter. However, every logic contract inherits a Proxiable contract that provides upgrade functionality.
Using the Proxiable contract stored in the execution contract, you can update the contract address referenced in the proxy contract. That way, you can keep using new execution contracts so long as they all inherit the same Proxiable contract.
Universal upgradeable proxies are beneficial because they reduce the likelihood of a function selector clash. While the Solidity compiler cannot detect clashing functions across two contracts, it can reject them when they occur within the same contract. Moreover, the proxy contract in this case has a smaller storage footprint, making it cheaper to deploy.
Using universal upgradable proxies, however, comes with a cost. If you upgrade the proxy contract to a logic contract that did not inherit the upgrade functionality, it is impossible to upgrade the proxy in the future.
If you have read the guide up until this point, congratulations! You now understand how to design upgradeable smart contracts and why using one may be good for your dApp.
Subscribe to our newsletter for more articles and guides on Ethereum. If you have any feedback, feel free to reach out to us via Twitter. You can always chat with us on our Discord community server, featuring some of the coolest developers you’ll ever meet :)