How to write upgradable smart contracts in solidity!

Himanshu Chawla
QuillHash
Published in
8 min readApr 25, 2018

While working on the smart contracts audits product QuillAudits at QuillHash, we are given 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 armor to prevent damage made by programming bugs after the contract got deployed.

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

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 the 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 the fourth 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 developers 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 errors 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 the blockchain. People need to trust 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 that 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 on the upgrade mechanism and in the follow-up post i will try to come up with the best contract upgrading governance mechanism.

So let's start with the implementation approach !!

  • The most important thing to consider when upgrading contracts is 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 a delegated contract will contain the functionality.
  • The storage structure of both these contracts must be similar.
  • To upgrade the logic of the contract we need to inform the proxy contract of the address of the new delegate contract.
  • When a transaction is sent to a 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 the 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 the delegate contract by calling the 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 that is getting used by our delegate contract. So to prevent this unauthorized state change we can use the address of the 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 be 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:-

The delegate contract contains the actual functionality of dApp.It also contains a local copy of the 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 the 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 the already deployed contract in ethereum. So the obvious solution is to create a new contract and the new contract too will contain a local copy of the Key-Value contract. Here we are creating a 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 the actual keyStorage contract in every version of the delegate contract.In this way, every version of the delegate contract shares a 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 the updated contract.It sounds stupid.So we will not store actual copies of key storage contracts in every version of the delegate contract.To get a shared storage proxy contract comes in to 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 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 proxy contract we are extending StorageState contract which will contain a global variable to store address of keyStorage contract.

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

Now user will always interact with dapp via 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 contract contains the reference to actual keyStorage contract.Delegate contracts contains local copy of keyStorage contract to get the getter ,setter functions logic and to have similar storage structure like proxy contract but actual storage changes are getting done from the context of proxy contract only.

Deploying and testing it together:-

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

We are calling getNumberOfOwners() three times in test case.First to get the state change by DelegateV1 contract .Second time to get the state modified by DelegateV1 from DelegateV2 contract and we success fully 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 with out 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:-

call upgrade to DelegateV2

You can see the complete code here:-

https://github.com/Quillhash/upradeableToken.git

Thanks for reading. Hopefully this guide has been useful to you and will help you to write upgradable smart contracts in solidity and Also do check out our earlier blog posts.

At QuillAudits We provide smart contracts auditing and ÐApps pen testing services for deFi, NFT projects ( 400+ projects secured ).

Overview of our audit process and expertise

https://audits.quillhash.com/smart-contracts-auditing-explained

To be up to date with our work, Join Our Community :-

Telegram | Twitter | Facebook | LinkedIn

References:

https://blog.colony.io/writing-upgradeable-contracts-in-solidity-6743f0eecc88

https://medium.com/level-k/flexible-upgradability-for-smart-contracts-9778d80d1638

https://medium.com/cardstack/upgradable-contracts-in-solidity-d5af87f0f913

https://blog.zeppelinos.org/smart-contract-upgradeability-using-eternal-storage/

https://medium.com/rocket-pool/upgradable-solidity-contract-design-54789205276d

--

--

Himanshu Chawla
QuillHash

Full stack developer (Ethereum, hyperledger, aws, react js, node js, golang)