How to write upgradable smart contracts in solidity!

While working on smart contracts security audits platform QuillAudits at QuillHash, we are giving most of the time to research about best security practices in smart contracts. QuillAudits considers the following distinct and crucial facets of the smart contract code: Whether the code is secure. Whether the code corresponds to the documentation (including white paper). Whether the code meets best practices inefficient use of gas, code readability, etc. An approach to upgrade contracts must be in the armour to prevent damage made by programming bugs after contract got deployed.

The Topic of upgradeable contracts is not very new to the world of ethereum. There are some different approaches to upgrade smart contracts.

Also, Read Our Next Article on EOS Smart Contract

Some approaches we considered in development are:-

  1. Separate logic and data.
  2. Partially upgradable smart contracts system.
  3. Separate logic and data in key value pairs.
  4. Eternal storage with proxy contract

With first three approaches, the contract can be updated by pointing users to use the new logic contract (through a resolver such as ENS) and updating the data contract permissions to allow the new logic contract to be able to execute the setters. In forth approach, we don’t need to do this redirection and it is a very flexible approach to update smart contracts. We found that eternal storage with proxy contract approach is flawless till now.

Readers are most welcome to comment if you know any flaw in this approach. It will be very helpful for the developer’s community.

There is a good reason for and against being able to update smart contracts. The good reason is all the recent hacks were based on programming error and could be fixed very easily if it was possible to upgrade those contracts.

However, the ability to upgrade smart contracts after they got deployed is somewhat against the ethics and immutability of blockchain. People need to trust you that you are a good boy. One thing that might make sense would be to have multi-sig-upgrades, where the “OK” from multiple people is necessary before a new contract is deployed and can access the storage. I think that it is storage records which need to be immutable in the blockchain. Logic must be improved with time as in all software engineering practices.No one can guarantee to develop bug-free software in the first version. So Upgradeable smart contracts with some upgrading governance mechanism can save many hacks.

In this post, I will touch the upgrade mechanism and in follow up post I will try to come with best contract upgrading governance mechanism.

So let’s start with implementation approach !!

  • The most important thing to consider when upgrading contracts are how to preserve the state of the original contract in the upgraded contract.
  • The state of the contract can be separated from the functionality of the contract. This approach allows multiple contracts to share the same state.
  • In this approach, a proxy contract will act as an immutable storage contract and delegate contract will contain the functionality.
  • The storage structure of both these contracts must be similar.
  • To upgrade the logic of contract we need to inform the proxy contract the address of new delegate contract.
  • When a transaction is sent to the proxy contract, it does not know about the specified function in the transaction.
  • The proxy contract will proxy the transaction to what we’ll refer to as a “delegate” contract (Which contains the functionality logic). This is done using the native EVM code, delegate call.

With delegate call, 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.
When a proxy contract uses a delegate contract’s functionality, state modifications will happen on the proxy contract. This means that the two contracts need to define the same storage memory. The order that storage is defined in memory needs to match between the two contracts.

We will deploy these contracts initially:-

  1. key Storage Contract (Contains the shared state)
  2. Delegate contractV1 and Delegate contractV2
  3. Proxy Contract (Contains the delegate call functionality)

Key Storage contract:-

It contains common storage for all storage state variables which will be shared among all versions of the smart contract. It also contains getter and setter functions to update and get the value of state from delegate contract.

Key Storage contract can be consumed by any delegate contract via proxy contract once deployed. We cannot create new getter and setters once key storage got deployed so we need to consider this while designing the initial version of the smart contract.

The best approach is to make mappings for every type of field in key storage contract. Where the key of mapping will be the name of the key simply in bytes and value will of the type declared in mapping.

For ex:- mapping(bytes32 => uint)

Now we can use this mapping to set and get an integer value from delegate contract by calling key storage getter and setter function for uint type.For ex: we can set the total supply with the key “totalSupply” and with any uint value.

But wait something is missing, Now anyone can call our key storage contract getter and setter function and update the state of storage which is getting used by our delegate contract. So to prevent this unauthorized state change we can use the address of proxy contract as the key of mapping.

mapping(address => mapping(bytes32 => uint)) uintStorage
In our setter function:
function setUintStorage(bytes32 keyField, uint value) public {
uintStorage[msg.sender][keyField] = value
}

Now as we are using msg.sender address in setter function and only this state change will reflected in proxy contract state when it uses getter function to get the state.Similarly, we can create other state mappings along with getter and setter functions as shown in the code below:-

Delegate contract:-

Delegate contract contains the actual functionality of dApp.It also contains a local copy of KeyStorage contract. In our dApp , if we include a certain functionality and later we found a bug in deployed contract, in that case, we can create a new version of delegate contract.

In the code below, Delegate contract version 1 (“DelegateV1.sol”) is deployed.

After deploying DelegateV1 we noticed the number of owners can be set by any user. So now we want to upgrade the smart contract so that only the owner of the contract can set a number of owners.

We cannot change the code of already deployed contract in ethereum. So obvious solution is to create a new contract and new contract too will contain a local copy of the Key Value contract. Here we are creating DelegateV2.sol contract with onlyOwner modifier added.

Now we have created a new contract but the storage of the previous contract is not available in the new version of the contract. So we can include a reference to actual keyStorage contract in every version of delegate contract. In this way, every version of delegate contract shares the same storage. But one thing is not desirable here, we need to tell every user about the address of the updated version of the contract so that they can use updated contract. It sounds stupid. So we will not store an actual copy of the key storage contract in every version of the delegate contract. To get a shared storage proxy contract comes into the rescue, let’s move on to proxy contract.

Proxy Contract:-
A proxy contract uses the delegatecall opcode to forward function calls to a target contract which can be updated. As delegatecall retains the state of the function call, the target contract’s logic can be updated and the state will remain in the proxy contract for the updated target contract’s logic to use. As with delegatecall, the msg.sender will remain that of the caller of the proxy contract.

A delegate call 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.
SO we just need to pass the address of new version of the contract to proxy contract via upgradeTo function.


The code of proxy contract is quite complicated in fallback function as here low-level delegate call assembly code is used.

Let break it down simply what is getting done in assembly code:-

delegatecall(gas, _impl, add(data, 0x20), mload(data), 0, 0);

In above function delegate call is calling code at “_impl” address with the input “add(data,0x20)” and with input memory size “mload(data)”,delegate call will return 0 on error and 1 on success and result of the fallback function is whatever will be returned by the called contract function.

In the proxy contract, we are extending StorageState contract which will contain a global variable to store the address of keyStorage contract.

The order of extending storage state contract before the ownable contract is important here. This storage state contract will be extended by our delegate contracts and all the functions logic executed in the delegate contract will be from the context of proxy contract. The order of storage structure of Proxy contract and Delegate contract must be the same.

Now the user will always interact with dapp via the same address of proxy contract and state of key storage contract seems to be shared among all versions of contract but in actual only proxy, the contract contains the reference to the actual keyStorage contract. Delegate contracts contain the local copy of keyStorage contract to get the getter, setter functions logic and to have similar storage structure like the proxy contract but actual storage changes are getting done from the context of proxy contract only.

Deploying and testing it together:-

Here the output of test cases will be: 10 10 and 20

How to write upgradable smart contracts in solidity

We are calling getNumberOfOwners() three times in test case. First to get the state change by DelegateV1 contract.S econd time to get the state modified by DelegateV1 from DelegateV2 contract and we successfully managed to retain the state modified by DelegateV1 and third time to get the state modification done by DelegateV2 contract.

Note here that we are calling getNumberOfOwners() every time from the same address of proxy contract. So we successfully managed to update the functionality of our contract without losing the previous state.

If we call setNumberOfOwners() from any other address except account[2] which is contract owner address, it will throw revert error.

Lets wind up the article with some diagrams:-

How to write upgradable smart contracts in solidity 1

At Quillhash we are developing a platform Quillplay to create secure and customised smart contracts templates for users with no programming experience.
https://quillhash.com/quillplay
I will try to come up with some upgrading governance mechanism in next post.
You can see the complete code here:-
https://github.com/Quillhash/upradeableToken.git

References:

Launch your blockchain project with Quillhash: https://quillhash.typeform.com/to/KQ5Hhm

Thanks for reading. Also, do check out our earlier blog posts.