Smart Contracts are immutable by design. Once deployed, a smart contract cannot be modified. If users want to update the smart contract to add new functionalities or fix any security vulnerabilities, they must deploy the updated smart contract, resulting in a new contract address. The immutability of smart contracts provides a high level of security and trust, but it also limits smart contract developers from introducing new functionalities or fixing bugs.
Sometimes, developers identify bugs or security vulnerabilities after the launch of smart contract. Such cases could lead to significant financial loss. There have been multiple smart contract exploits in the past, such as the DAO hack that resulted in millions of dollars in losses.
Enabling upgradeability for the smart contract can be an effective way to fix such bugs before they impact the outcomes. This approach provides a means to add new functionalities or address any bugs while maintaining the contract’s state.
There are multiple ways to implement upgradeable smart contracts, such as proxy and data separation patterns. In this blog, I will focus on how to use proxy patterns to implement upgradeable smart contracts.
What is a proxy pattern?
The proxy pattern is a structural design pattern in which one class stands in for and represents the functionality of another class. It has two components: the proxy contract and the implementation contract.
In a smart contract with a proxy pattern, users interact with the proxy contract rather than the implementation contract. Both the proxy and implementation contracts remain immutable, but upgrades are enabled by allowing the proxy contract to interact with the latest implementation contract. This ensures that end users do not notice any changes in the Dapp and can use the updated version of the smart contract.
The proxy contract is responsible for handling user interaction and data storage. It stores the address of the implementation contract in a pseudo-random storage slot. When users make calls to the proxy contract, the fallback function is triggered. This function internally executes a delegateCall to the implementation contract, causing a change in the state of the proxy contract.
Proxy Patterns to create upgradeable contracts in solidity
1. Transparent Proxy pattern (TPP)
2. Universal Upgradeable Proxy Standard (UUPS) – EIP-1822 b
3. Beacon Proxy Pattern
4. Diamond Proxy Pattern – EIP-2535
Transparent Proxy Pattern (TPP)
The transparent proxy pattern includes the upgrade functionality in the proxy contract. The proxy contract should have the upgradeTo(address)
method, which will update the address of the implementation contract.
Since the upgrade method is present in the proxy contract, there may be situations where there is an upgradeTo(address)
method in the implementation contract serves a different purpose than the upgradeTo(address)
method in the proxy contract. In such cases, it becomes unclear whether the user intended to invoke the upgradeTo(address)
method of the proxy or implementation contracts.
To handle such scenarios, Openzeppelin has provided an implementation for the transparent proxy pattern. In this implementation, the delegation of contract calls depends on the caller’s address. The contract calls are delegated to the implementation contract only if the caller is not a proxy admin.
Universal Upgradeable Proxy Standard (UUPS)
UUPS (Upgradeable Proxy Pattern System) includes upgrade functionality in the implementation contract. The proxy contracts delegate calls to the implementation contract. The implementation contract should have the upgradeTo(address)
method that allows the proxy to point to the latest version of the smart contract.
Providing upgradeability to future versions is solely dependent on the developer. If the developer removes the upgradeTo(address)
method from a newer version, the smart contract becomes immutable, and no new versions can be deployed. This can be useful when you have thoroughly tested your contract and want to freeze updates.
In the transparent proxy pattern, the fallback method needs to check whether the caller is a proxy admin before delegating the call to the implementation contract. However, this is not the case with UUPS. In UUPS, the upgradeTo(address)
method is present in the implementation contract, and there can be an upgradeTo(address)
method in the proxy pattern that serves a different purpose.
Beacon Proxy Pattern
The Beacon proxy pattern comprises three components: the proxy contract, the beacon contract, and the implementation contract. In this pattern, the beacon contract stores the address of the implementation contract, while the proxy contract stores the address of the beacon contract.
When the user calls the proxy contract, it retrieves the address of the beacon contract. Subsequently, it calls the beacon contract to obtain the address of the implementation contract. Once the proxy contract has the address of the implementation contract, it delegates the calls to the implementation contract.
This pattern is beneficial when multiple proxy contracts point to the same implementation contract. If we were to use the transparent proxy pattern or UUPS pattern, we would need to update the address of the implementation contract in all the proxy contracts. However, with the beacon pattern, only the address in the beacon contract needs updating.
Diamond Proxy Pattern
The Diamond proxy pattern is a design pattern that divides contracts into multiple smaller contracts, overcoming the 24 kB size limitation of a smart contract. The pattern comprises a proxy contract and multiple smaller contracts called facets. The proxy contract stores the function selector and the corresponding facet address, enabling it to identify the correct facet address when a function is called.
Upgradeability can be achieved by updating the facet address stored in the proxy contract.
Proxy pattern comparison: Transparent vs. UUPS vs. Beacon vs. Diamond
Transparent Proxy Pattern | UUPS (Universal Upgradeable Proxy Standard) | Beacon Proxy Pattern | Diamond Proxy Pattern | |
Need Proxy Contract? | Yes | Yes | Yes | Yes |
Uses Delegation? | Yes | Yes | Yes | Yes |
Upgrade Function Present In? | Proxy Contract | Implementation Contract | Beacon Contract | Not specified in EIP. Typically, in implementation contracts |
Can make future contracts immutable? | No | Yes. Remove upgradeTo function from the implementation contract | No | Yes |
Ideal for multiple proxies? | No. Needs setting the new contract address in all proxies | No. Needs setting the new contract address in all proxies | Yes. In case of new implementation, set the new contract address in the beacon contract | No. Needs setting the new contract address in all proxies |
Gas Fees | More. Need to check if user is admin before every delegate call | Less. No need to check if user is admin before every delegate call | More. Gas required to get beacon contract address, get implementation contract address from beacon contract | More. Gas required to get the contract (facet) which has the specified function |
Max Contract Size | 24 kB | 24 kB | 24 kB | 24 kB per facet. Can support multiple facets |
Implementation Complexity | Medium | Medium | Medium | Medium |
Library | Openzeppelin | Openzeppelin | Openzeppelin |
Concerns with using proxy for smart contract upgrades
Storage Collision: A storage collision can occur if the address slot of the proxy contract variable overlaps with the address slot of the implementation contract variable, given that contract variables are stored in address slots. Furthermore, any modification to the order of variables may result in a storage collision between two versions of the implementation contracts.
Unitialized Implementation: The implementation contracts should be initialized only once using the initialization function. In some cases, developers might forget to initialize the function or neglect to add a check that restricts the initialization function to be called only once. In such instances, hackers can exploit the contract by calling the initialization function and modifying the contract’s state.
Proxy contract exploits
During the bug bounty program of Harvest Finance, uninitialized implementation contracts for Uniswap V3 vault proxies were discovered. This critical bug could potentially cause the implementation contract’s self-destruction, rendering the proxy contracts useless. For more details, click here.
In July 2022, a bug in the initialization code, allowing repeated invocations of the initialize function, compromised the Audius smart contracts.
Other patterns for creating upgradeable smart contracts
Data separation pattern: This pattern involves separating the storage and logic contracts. The logic contract interacts with the storage contract to retrieve or update any storage variables. While the logic contract can be upgraded, the storage contract is not intended for upgrades. For a more detailed understanding of this pattern, please refer to the following links.
Contract Upgrade Anti-patterns
None of the patterns we have discussed in this blog addresses the verification of contracts prior to the upgrade. It is crucial to verify that the new version of the contract includes all the necessary business methods, fallback functions, etc. This can be achieved by adding a verification layer alongside the proxy, business, and storage layers. For more details, please refer to this link.
Points to consider if you are creating upgradeable contracts
- Avoid implementing the proxies yourself. Instead, utilize the proxy implementations provided by libraries such as OpenZeppelin.
- Do not forget to initialize the implementation contract. Ensure that the initialization function can only be called once.
- Do not initialize the state variables during declaration or in the constructor. Instead, initialize the state variables in the initialization function.
- Do not change the order or type of the contract state variables when declaring them. If you need to add new state variables, do so after the already declared variables. In the case of the diamond proxy pattern, use either app storage or diamond storage.
- Prefer the UUPS method over the transparent proxy pattern, as UUPS consumes less gas.
- Ensure that the proxy admin account is secure.
- Have your contract audited by smart contract auditors.
Conclusion
In this blog, I delved into techniques for upgrading smart contracts through proxy patterns. In this methodology, the proxy contract employs delegateCall to transfer the call to the implementation contract. However, it’s crucial to highlight that inadequately implemented upgradeable smart contracts can expose vulnerabilities to potential attacks.
References:
- https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable
- https://docs.openzeppelin.com/contracts/4.x/api/proxy
- https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916
- https://dev.to/envoy_/upgradable-erc-20-smart-contract-part-1-5433
- https://blog.logrocket.com/using-uups-proxy-pattern-upgrade-smart-contracts
- https://proxies.yacademy.dev/pages/proxies-list/#the-proxy
- https://blog.trailofbits.com/2020/10/30/good-idea-bad-design-how-the-diamond-standard-falls-short/
- https://www.hindawi.com/journals/scn/2023/8455894/