Blog

How to write upgradable smart contracts in solidity.

Table of Contents

Read Time: 8 minutes

While working on the smart contracts security audits platform QuillAudits at QuillHash, we are giving most of the time to researching 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 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. Here we talk about that How to write upgradable smart contracts in solidity.

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 execute the setters. In the fourth approach, we don’t need to do this redirection, and it is a very flexible approach to updating smart contracts. We found that eternal storage with a proxy contract approach is flawless till now.

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

There is a good reasons 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 deployment 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 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 on the upgrade mechanism, and in a 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 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 a 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 the specified transaction function.
  • 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 a 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 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 the delegate contract by calling key storage getter and setter function for the uint type. For example, we can set the total supply with the key “totalSupply” and 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 be reflected in the proxy contract state when it uses the 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 the already deployed contracts in ethereum. So the obvious solution is to create a new contract, which will contain a local copy of the Key Value contract. Here we are creating DelegateV2.sol contract with onlyOwner modifier added.

We have created a new contract, but the storage of the previous contract is not available in the new version. 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 the same storage. But one thing is not desirable here: we need to tell every user about the updated version of the contract address so that they can use the 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. Getting a shared storage proxy contract comes to the rescue; let’s move on to the 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 the 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 the state of the key storage contract seems to be shared among all versions of the contract but in actually 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 and setter functions logic and to have a similar storage structure like the proxy contract. Still, actual storage changes are getting done from the context of proxy contracts only.

Deploying and testing it together:-

Here the 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.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 the contract owner address, it will throw a revert error.

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

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

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

1,539 Views

Related Articles

View All

Trending

#Alert🚨
$NUWA failed to rug on BSC and was front-run by the MEV bot 0x286E09932B8D096cbA3423d12965042736b8F850.

The bot made ~$110,000 in profit.

Are you concerned about your enterprise's security in Web 3.0? Look no further!

Let's delve deeper into and learn effective solutions to mitigate them. Our experts have covered unconventional approaches, from Zero- Trust Security Model to Bug Bounty Programmes.

🔻🔻🔻

Hey folks👋,

Web3 security is like a game of whack-a-mole, except the moles are hackers who keep popping up no matter how hard you hit them. 🤦‍♀️

But fear not; we've got some tips to keep your crypto safe⬇️⬇️

Unlock the power of Web3 for your enterprise with enhanced security measures!

💪🌐 Our latest blog post delves into the world of Web3-powered enterprises and how to ensure maximum security in this new frontier.🔒

Read part 1 of our series now: 🚀https://blog.quillhash.com/2023/03/15/web3-security-for-enterprise-web3-powered-enterprises-part-1/

#Web3… https://twitter.com/i/web/status/1638154504154628096

Load More

Amidst FTX Saga, Hacker Swept More Than $25 Million in 2nd week of November

The contract reinvested (the earn function was not called) before the user pledged (depositAll function) without settling the reward, which means that when the user pledged, the contract did not settle the previous reward and instead conducted a new investment.

Become a Quiffiliate!
Join our mission to safeguard web3

Sounds Interesting, Right? All you have to do is:

1

Refer QuillAudits to Web3 projects for audits.

2

Earn rewards as we conclude the audits.

3

Thereby help us Secure web3 ecosystem.

Total Rewards Shared Out: $190K+