One of the first things we learn in Solidity/Ethereum is that contracts are immutable. If you upload a contract on the blockchain, you can’t change its source code and therefore you can’t change its logic.
We must be extremely careful with the code we use: if we make one simple mistake it could be extremely expensive. That’s why we consider alternatives as a way to avoid this limitation. Let’s see how we can use an upgradeable contracts.
Contracts Old School
The simplest thing we can do is upload a new version of the contract (and thus a new deploy and a new address). But this has its issues, for example let’s think about what it was like to have a shop a few years ago (before the internet). If we wanted to move to another address, we had no way of notifying all our clients and potential future clients of this change. The same thing happens in smart contracts, all users must know that the address has changed, but not having a user registry makes it very difficult.
Ways to create a upgradeable contracts
There are currently 4 ways to create an upgradeable contracts.
Using an interface / proxy
Taking advantage of inheritance and polymorphism, an interface with the contract structure can be exposed. So now we have two contracts: one with the contract definition (and external calls) and the other with the function’s implementation. The user interacts with the contract through the interface and if an update is required, the address of the contract is simply changed for the new version in a simple variable inside the contract.
It is not possible to modify the contract’s structure since we MUST respect its interface, but its main disadvantage is that the stored values will remain in the contract that has the logic, and if we want to update it with that of another contract, we will lose the storage state.
What is done to solve the storage issue is to have a “proxy” contract that has the same structure as the one with the logic, and it is accessed through the delegatecall instruction. It is SUPER IMPORTANT to keep the same variable structure in the contracts since we will be sharing the storage, and if both do not have the same variable structure, a collision could occur. A possible solution for this is that they both inherit from a base contract that sets the same variable-structure for all of the contract versions.
It seems to be a better alternative but in terms of gas it is expensive: we are always adding variables that we will most likely never use in every logic contract we implement. This can be improved if a third-party storage contract is used, for this, we can use solutions such as OpenZeppelin or combine it with an unstructured storage.
OpenZeppelin Transparent Proxy
If you are a user of the OpenZeppelin contracts you have noticed this plugin for sure. Inside, it uses a mechanism known as Transparent Proxy, which uses unstructured storage to avoid collisions (as long as we add the variables at the end and never at the beginning). This proxy is already integrated as a plugin and is compatible with Hardhat and Truffle.
This is one of the most popular and most complex, since we have an intermediary contract, but instead of aiming for a single final contract, each function will be implemented in a different smart contract. In addition, each “Facet” contract (as they are called) will have a fixed memory space to avoid collisions. A major advantage is, a whole contract deployment is not required, just a contract with one function implementation. That’s cheaper than deploying a complete-structure contract with all of the functions and also as there is just one function per contract, the size limit is very hard to reach. It is undoubtedly the favoured solution for the complex scenarios that are created today and there is even an EIP that refers to it.
Syntethix Router Proxies
At ETH Latam event, Alejandro Santander (known as The Ethernaut) presented a solution called Router Proxies. It is quite similar to the Diamond Pattern, but simpler. I recommend watching this talk!
Metamorphosis Smart Contract
What? Well, we are not actually modifying the contract, but if we had the ability to remove the contract from storage, we could force a deployment to that same address, then replace one contract with another. And this is how this technique works. Through the selfdestruct() statement, it is possible to remove a contract from the blockchain storage, and using the create2 operation, a new version of the contract is deployed to THE SAME ADDRESS.
Ok, but… is it okay to modify a Smart Contract?
In centralized software, not only is this not a bad thing, it is a necessary thing. What happens in decentralized projects? Who should update the code? Is a DAO reliable enough? On the one hand, an immutable contract seems more reliable than an upgradeable contracts but it also makes it more vulnerable to attacks. In my opinion, it is good to consider all these alternatives and use them if the project requires a sophisticated architecture. Making a contract upgradeable makes its code much harder to understand and trace, but improves its resistance to attacks. If the strength of a project is its simplicity, I would not work on making the contract updatable, but I would do so in complex projects with multiple contracts.