This article presents several patterns for creating upgradeable smart contracts on Ethereum and other EVM-compatible blockchains. The code used is open-source (under MPL-2.0 license) and available on GitHub.
As an example, we show how to use it to deploy the CMTA Token (CMTAT) in a scalable and cost-effective way. The CMTAT is an open standard suitable for the tokenization of various financial instruments and is adopted by multiple actors within the finance sector. Its implementation on Ethereum and EVM blockchain is written in Solidity and is also an extension of the ERC-20 standard. For our proof of concept, we used the CMTAT version 2.5.1.
Introduction to proxy pattern architecture
On Ethereum and other EVM chains, smart contracts are immutable: once a contract is deployed, it is not possible to change its code. You can, however, modify the contract’s behavior by adjusting parameters set within the contract. But what if you want to add a new functionality or fix a critical bug? This is not possible with the standard deployment setup.
To address this limitation, many developers devised a way to update the behavior of a smart contract after its deployment. This workaround uses a specific EVM opcode, delegatecall. This opcode can be used inside a smart contract called a proxy) to perform a call to another smart contract (called implementation or logic contract), and all operations performed by this second smart contract will apply to the storage of the first one. This architecture is now widely used and known as proxy pattern architecture.
In summary, there are two main contracts:
-
The proxy contract, which stores the memory and delegates its calls to the logic or implementation contract.
-
The logic or implementation contract which contains all the code to perform the operations.
Proxy - Basic architecture
Thus, if we want to modify how the first smart contract proxy works, we can redirect it to point to another smart contract through a process commonly called an upgrade.
- This new implementation can add new features or fix a bug discovered in the previous implementation used by the proxy.
- Moreover, there are generally several restrictions to respect regarding the management memory to avoid storage collision between implementation versions. Since the version v5.0.0, the library OpenZeppelin implements the standard ERC-7201 which standardizes a new method to reduce the risk of collisions, and this is also the case for CMTAT since the version 2.5.0.
When you use the CMTAT, the implementation or contract logic will be the CMTAT. Here, the user calls the ERC20 function transfer from the CMTAT through the proxy.
In addition to enabling upgrades, this architecture also helps reduce deployment costs if you have several deployments to carry out. A proxy is significantly cheaper to deploy than its implementation because the latter contains all the application logic. It is therefore possible to deploy a single implementation and have several proxies point to it.
Proxy - Save deployment gas
Also, some proxy architectures do not include the upgrade functionality and are used solely to save gas during deployment. This is the case with the clone proxy architecture.
In the remainder of the article, we will explain how CMTAT, an ERC-20 smart contract for tokenization, can be deployed using a proxy.
Proxy pattern architecture
There are five main variations of the proxy architecture:
-
Minimal/Clone proxy (ERC-1167)
-
Beacon proxy (ERC-1967)
-
Transparent proxy (ERC-1967)
-
UUPS proxy (ERC-1822)
-
Diamond Proxy (ERC-2535)
CMTAT can be deployed with the clone proxy, transparent proxy, and beacon proxy architectures.
A specific version of CMTAT also exists for deployment with the UUPS architecture.
CMTAT cannot be deployed with the Diamond Proxy architecture, as its storage is not compatible with this architecture.
Clone proxy
In this architecture, the implementation cannot be upgraded; therefore, it is only used to save gas during deployment.
Since the proxy cannot be upgraded, clone proxies are cheaper to deploy than other variants.
Transparent proxy
In this architecture, there are three contracts:
-
Proxy admin contract: controls and upgrades the proxy contract.
-
Transparent proxy contract: acts as the main entry point for the contract user.
-
Implementation contract: contains the code of your smart contract, in this case, the CMTAT.
This architecture is more complex than a clone proxy because the proxy can be upgraded.
Moreover, the upgrade function is coded within the proxy itself, making the proxy larger to deploy than a UUPS proxy.
Upgrade
When we want to upgrade the proxy, it must be able to recognize that the call must not be delegated to the implementation.
To enable this, the most common way, notably used by OpenZeppelin, is to use a contract called Proxy Admin to manage the proxy. This contract acts as the proxy’s owner and can be called by a standard EOA to upgrade the proxy.
Thus, all EOA can interact with the proxy, but when the call originates from the Proxy Admin (for example, to upgrade the proxy), the proxy recognizes that the call must not be delegated.
This operation is executed by calling the upgrade function from the ProxyAdmin.
OpenZeppelin - ProxyAdmin.sol#L62
The ProxyAdmin will then call itself the transparent proxy to perform the upgrade, updating the transparent proxy’s storage memory to point to the new implementation address.
Schema
UUPS proxy
With a UUPS proxy, the logic to upgrade the contract resides in the implementation, making the proxy cheaper to deploy. Initially, it was considered less secure than a transparent proxy due to a certain attack targeting the implementation. However, OpenZeppelin introduced several improvements to protect against this threat. Moreover, one of the main vulnerabilities was tied to the use of selfdestruct, but the potential impact has been eliminated with the Cancun upgrade in March 2024, which disabled the main effect of this opcode.
Beacon proxy
A beacon proxy is very useful if you want to manage all your proxies in one place.
Unlike the transparent proxy, the beacon proxy does not point directly to the implementation contract. Instead, it stores the address of another contract called the Beacon contract. This contract is responsible for storing the address of the implementation. When an entity (EOA or contract) calls the proxy, the proxy then calls the beacon contract to retrieve the implementation and delegate the call to it.
For example:
-
The user (an EOA) calls the mintfunction on the proxy contract.
-
The proxy calls the beacon contract to get the address of the implementation.
-
The proxy calls the implementation contract with a delegateCall.
Upgrade
In the case of a beacon proxy, the upgrade is performed by calling the beaconcontract with the function upgradeTo(address newImplementation).
Schema
Diamond proxy
Diamond proxies are modular smart contract systems that can be upgraded or extended after deployment. By analogy with the previous types of proxies, it’s as if the implementation contract were split into several different contracts which can be set inside the proxy. This is a valuable functionality because, with this architecture, the implementation contract has virtually no size limit.
But unfortunately, this is more complex to manage, and one of the main libraries used to develop smart contracts, OpenZeppelin, has limited compatibility with this architecture.
Summary
Type |
ERC |
Upgradeable |
|
Gas efficient |
|
Complexity |
CMTAT 2.5.0 |
|
---|---|---|---|---|---|---|---|---|
|
|
Individually |
Grouped |
Deployment |
Function Calls |
|
Support |
Factory available |
Minimal/Clone proxy |
❌ | ❌ |
✅ |
✅ |
Low |
✅ |
❌ |
|
Transparent proxy |
✅ |
❌ |
❌ |
✅ |
Medium |
✅ |
✅ |
|
UUPS proxy |
✅ |
❌ |
✅ |
✅ |
Medium |
Partial |
✅ |
|
Beacon proxy |
❌ |
✅ |
✅ |
❌ |
Medium |
✅ |
✅ |
|
Diamond proxy |
✅ |
❌ |
❌ |
❌ |
Height |
❌ |
❌ |
Factory
A factory contract is a contract that allows easy deployment of other contracts. In our case, the factory is used to deploy our CMTAT with one of the supported proxy architectures (UUPS, Transparent, or Beacon).
-
When deploying with a proxy architecture, a factory contract can store the implementation in its storage and use it for all deployments. When a factory’s user wants to deploy a new proxy, they do not need to specify the implementation again in its deployment, as it is stored and managed by the factory.
-
A factory contract is also very practical for deploying a contract at the same address in several different EVM blockchains. This is done by deploying deterministic smart contracts, where the address of the smart contract depends on a configurable parameter (salt), allowing us to determine the future contract address based on this configurable parameter before deployment.
Currently, a factory is available in the CMTAT repository to deploy Transparent, UUPS, and Beacon proxies.
Deterministic smart contract
Before deploying our contract, it’s helpful to have a quick summary of how a contract address is computed when deploying inside another contract (in this case, our Factory).
The address of a contract will depend on the opcode used: create or create2.
For this section, the main references used are two articles: Deploying Smart Contracts Using CREATE2 by OpenZeppelin and The Ultimate Guide to create, create2 and create3 by Solichain/Adam Boudjemaa.
Create
With the opcode create, the address computation is as follows:
ContractAdress = hash (factoryContractAddress, factoryNonce)
As we can see, with create, the address is determined by the factory contract's nonce and its address.
Every time a contract is deployed through the factory, the nonce increases by 1; it is basically a counter.
Since the nonce is a publicly known value, it is possible to predict the address of a contract. But there is a significant drawback: if the factory is used to deploy another contract before your planned deployment, the nonce will be incremented before your deployment. This can happen if several users are using the factory, which is not very convenient.
Create2
create2 is a specific opcode provided by the EVM that computes a contract address differently:
ContractAdress = hash(0xFF, factoryContractAddress, salt, hash(bytecode))
0xFF is a constant used to prevent collisions with create.
Among these different parameters, the most interesting is the salt. This parameter is passed to create2 and is configurable, unlike the nonce for create. It allows us to determine and influence the future contract address.
In our different factories, we use create2 to deploy deterministic smart contracts.
Transparent proxy factory
The factory will use the same implementation for each transparent proxy deployed. Each transparent proxy has its own proxy admin, deployed inside the constructor of the transparent proxy. Each transparent proxy can upgrade its implementation to a new one independently, without impact on other proxies.
Beacon proxy factory
The factory will use the same beacon for each beacon proxy. This beacon provides the address of the implementation contract, a CMTAT_PROXY contract. If you upgrade the beacon to point to a new implementation, it will change the implementation contract for all beacon proxies.
Proof of concept
In our proof of concept, we deployed a beacon factory and used it to deploy our various CMTAT tokens.
The beacon factory created for CMTAT already contains all the necessary code.
Deployment
-
Deploy the CMTAT Factory; We will let the factory deploy a new CMTAT_PROXY implementation for our beacon contract.
-
Use the factory to deploy our new CMTAT token.
-
Upgrade the beacon contract to point to a new implementation.
CMTAT Factory Deployment
Here is the code for the factory constructor:
Here are the different parameters for the constructor:
Parameter |
Type |
Description |
Value |
---|---|---|---|
implementation_ |
address |
CMTAT implementation |
0x0000000000000000000000000000000000000000 |
factoryAdmin |
address |
Factory admin |
0x5950537bf855bf8b6246ba75da3540031910e83b |
beaconOwner |
address |
Beacon owner |
0x5950537bf855bf8b6246ba75da3540031910e83b |
useCustomSalt_ |
bool |
yes, if we want use a custom salt with create2 (deterministic contract), otherwise use a counter |
True |
Taurus-CAPITAL: Factory contract
Remark: Since the constructor will deploy another smart contract, CMTAT, it is important to set a sufficiently high gas limit, e.g., 700,000,000.
Result
Transaction: 0x0a54f0c11f9d3d308113e68570c56a0c0b136719815e5068a1894479accd8f77.
The contract is deployed at the following address: 0x7b5db0157b07c060edf7adf20a60a8187d79d902.
We can see the beacon address and the implementation address by calling the public function beacon.
Taurus-CAPITAL: Factory contract
Gas fee
Since we have deployed the implementation inside our constructor, the transaction used a significant amount of gas, 6,173,246. However, as we will see in the rest of the article, deploying a new CMTAT proxy will cost less.
Beacon contract
Our beacon contract is deployed at the following address: 0x8d99f8701a5e225acabe2bf6192bf7648211d0a3.
Taurus-CAPITAL: Beacon contract
Deploy a first CMTAT Proxy
To deploy our CMTAT proxy, the factory provides a specific function deployCMTAT:
The function has the following signature:
deployCMTAT(bytes32,(address,(string,string,uint8),(string,string,string),(address,address,address,address)))
-
Suggested gas limit: 7,000,000.
-
Function argument:
Argument name |
Struct /tuple name |
Child argument name |
Value |
---|---|---|---|
deploymentSaltInput |
|
|
0x0000000000000000000000000000000000000000000000000000000000000001 |
cmtatArgument |
|
|
|
|
CMTATAdmin |
|
0x5950537bf855bf8b6246ba75da3540031910e83b |
|
ERC20Attributes |
|
|
|
|
Name Irrevocable |
CMTAT Beacon |
|
|
Symbol Irrevocable |
CMTATB |
|
|
Decimals Irrevocable |
0 |
|
baseModuleAttributes |
|
|
|
|
tokenId |
0 |
|
|
terms |
|
|
|
information |
Beacon CMAT 0x01 |
|
engines |
|
|
|
|
ruleEngine |
0x0000000000000000000000000000000000000000 |
|
|
debtEngine |
0x0000000000000000000000000000000000000000 |
|
|
authorizationEngine |
0x0000000000000000000000000000000000000000 |
|
|
documentEngine |
0x0000000000000000000000000000000000000000 |
Determine the address before deploying
Since we deployed with a custom salt, the address can be determined before deployment by calling the read function Computed Proxy Address with the same argument which will be used to deploy our proxy.
In our case, the function returns: 0xF6B2F12aBCACD9Ac0C5e386485f30c60Efe1bf81.
Taurus-CAPITAL: Factory contract
Execute the function
We will now execute the function with the different parameters.
Taurus-CAPITAL: Factory contract
Result
See the transaction on Sepolia Etherscan.
We can find our CMTAT smart contract by reviewing the different events emitted by the transaction.
Etherscan - Sepolia
The address is 0xF6B2F12aBCACD9Ac0C5e386485f30c60Efe1bf81.
Since we are deploying only the proxy, the transaction requires 443,404 of gas, compared to 6,173,246 for our factory.
With the same gas limit as our factory, we only use 6.33% instead of 88% for our factory. This represents a reduction of 82%.
Etherscan - Transaction details
In Taurus-CAPITAL, we can retrieve the information provided during the smart contract deployment.
Taurus-CAPITAL: Beacon Proxy 1
Deploy a second CMTAT Proxy
To test the Upgrade of a proxy, we deploy a second CMTAT Proxy.
Taurus-CAPITAL: Factory contract
See the transaction on Sepolia Etherscan.
As with the first deployment, the gas usage remains the same:
Etherscan - Transaction details
The contract address is 0xae3b21f93F3baF83D7e35877D7b49911366482DD.
Taurus-CAPITAL: Beacon Proxy 2
Upgrade
We will upgrade our beacon proxy to a new implementation.
Deploy the new implementation
First, we deploy our new implementation:
Taurus-CAPITAL: CMTAT Proxy Implementation
The contract is deployed at the following address: 0x564b56dcbcd907e1199a8a20e86eed2704713cae.
See the transaction on Etherscan Sepolia.
Upgrade the proxy
To upgrade the proxy, we will call the upgradeTo function from our Beacon contract with the address of our new implementation.
github.com/OpenZeppelin - UpgradeableBeacon.sol#L52
Taurus-CAPITAL: Beacon contract
Result
Before the upgrade
Taurus-CAPITAL: Beacon contract
After the upgrade
The implementation address has changed for our new address: 0x564b56dcbcd907e1199a8a20e86eed2704713cae.
Taurus-CAPITAL: Beacon contract
Our two CMTAT proxies show the same CMTAT version:
Taurus-CAPITAL: Beacon Proxy 1 & 2
Conclusion
Proxy contracts are very useful architecture components that provide two main benefits:
-
Allow upgrading the behavior of your main entry point to fix a vulnerability or add a new feature.
-
Reduce deployment gas costs by re-using the same implementation for several different contracts.
A factory contract offers several benefits:
-
Simplifies deployment by storing the implementation and deploying the proxy itself.
-
Enables deployment of deterministic contracts, which is particularly useful for cross-chain tokenization to ensure the same address on different EVM blockchains.
It is because of these different advantages that factory contracts have been available for the CMTAT since version v2.4.0. See the project on GitHub.