Share with your friends:

Applications always need new features in order to meet the demands of their users. So… all you need to do is deploy the new set of smart contracts with the new features. Easy peasy, right? Not so fast! We are missing some crucial details.

  • Shouldn’t old data persist in the new set of smart contracts?
  • Will the change in your smart contract addresses affect your dApp integration with various service providers?
  • For apps like stateofthedapps etherscan, etc., how would you make them aware of the change in your smart contract addresses?

We also faced similar issues during the creation of the next-generation standard for blockchain based securities called polymath-core. Since we believe in innovation and creativity, we decided to make our smart contracts (registries) upgradable to solve these problems. It is important to note that many projects have been researching the different techniques to make smart contracts upgradable; we will be presenting our approach in this post.

Thanks to ZeppelinOS, for putting their time and effort into creating generalized techniques/proxy patterns to build upgradable smart contracts. From all of those proxy patterns, we are going to explore Eternal Storage proxy pattern that we used in our registries contracts to make them upgradable. If you are curious to learn more about the different proxy patterns for upgradability, then read this awesome article by Elena.

Why are only registries upgradable?

In order to update core parts of our protocol while still allowing issuers to retain full control over any issued tokens, we only added upgradability to our registry and control contracts. Tokens generated through the protocol remain immutable although we are looking at innovative ways to tackle updating tokens in a future release of polymath-core.

The architectural design of Polymath registries

Polymath protocol has three main registries:

  1. SecurityTokenRegistry.sol
  2. ModuleRegistry.sol
  3. PolymathRegistry.sol

SecurityTokenRegistry(aka STR) and ModuleRegistry(aka MR) are upgradable registries while thePolymathRegistry(aka PR) is not because it doesn’t have complex features like the other ones. It is only a registry for the addresses of different the smart contracts in Polymath ecosystem.

Driving contracts for upgradability

  1. OwnedUpgradeabilityProxy.sol
  2. Proxy.sol

Basically, we picked up those contracts from zeppelin labs which made our lives easier when implementing it for our contracts. Below, I describe some of the most valuable functions of the previously mentioned contracts.

fallback()

The key function is the fallback function, which uses function calls to delegate the implementation contract and later affect the storage of the proxy contract. This helps to change the implementation contract without migrating the data, as all of the data resides in the proxy contracts instead of in the implementation contract.

During the function call, the function selector searches the 4-byte function signature first within the proxy contract functions. If the function signature matches within the proxy contract, the function gets executed or else the fallback function gets executed and performs a linear search on the list of function signatures of the delegate contract (~implementation contract). It would also execute the function once the match gets found. The remaining msg.data value gets copied using calldatacopy(0, 0, calldatasize) and used for the function arguments…

switch result
// delegatecall returns 0 on error.
case 0 { revert(0, returndatasize) }
default { return(0, returndatasize) }

Lastly, the switch statement returns either the returned data of the function or throws an exception if something went wrong. For a piece with more in-depth knowledge, please refer to this article.

upgradeToAndCall()

This is one of the key functions used to set the implementation/logic contract and the version of the implementation. The version is specifically used to differentiate from the various logic implementation of the contract.

require(address(this).call.value(msg.value)(_data), “Fail in executing the function of implementation contract”);

It is calling the contract fallback function with msg.data as _data . _data is the encoded value of the function signature and its argument value. It is variable in size but first 4-bytes are fixed for function signature which helps to decide which function needs to be executed. Mainly this upgradeToAndCall()function is used to set the implementation contract and initialize the value of some state variables. For some reason, the implementation contract has some bugs or may need to add more functionality. In this case, the most suitable function is to set the implementation contract to upgradeTo() because we don’t want to initialize the state variables again.

Why we choose Eternal Storage over other proxy patterns?

Upgrade the smart contracts

There may be n possible ways that exist to upgrade a smart contract. Who really knows? Here we focused on three possible proxy patterns:

  1. Unstructured storage proxy pattern
  2. Inherited storage proxy pattern
  3. Eternal storage (aka ES) proxy pattern

Each proxy pattern has the same way of handling the function call that I’ve described above. The major difference is the techniques to store variables in storage.

Registries are one of the critical contracts of our ecosystem. We especially didn’t want to mess up the storage for these. We also didn’t want that at the time of writing the smart contract, so we needed to take care of state variable declaration order. We were more centric to our registries functionalities so we choseEternal Storage over the unstructured and inherited storage proxy pattern.

Eternal storage has its own downsides that I am going to explain one by one below and how exactly we mitigated them.

1. Increasing readability of the code

Two things that need to be taken care of while adopting ES pattern are the proxy and implementation contracts that inherit the same set of storage. For this, we specifically created a set of mappings where all the state variable values reside and theEternalStorage.sol contract gets inherited by both the proxy and implementation contracts.

The normal contract storing of variables looks like this:

uint256 test;
test = 10000;

and the upgradable contract (ES type):

uintStorage[keccak256(abi.encodePacked("test"))] = 1000;

You can compare how much readability gets affected for the singleton variable. Think about the struct, mappings, arrays, etc. To resolve this, we created a set of helper functions that increased the readability of the code.

When using this variable, it is stored like this:

set(keccak256(abi.encodePacked("test")), 1000);

Above is the gist of some setters and getters that we used in our contracts. Find the full contract here.

2. Increased Contract Size

Contract bytecode size has a limitation of 24 KB as per the EIP170. While our contract bytecode size reached 23.5 KB we reduced it using libraries to factor out the encoding part of the variables.

Using this library we are able to reduce the contract size by 3.5KB and it also helped to increase the readability of the code.

set(getKey("test"), 1000);

There were also some suggestions from our auditors to use the precomputed hash values for the singleton type of variables which helps to reduce the bit size of the contract bytecode.

Everything above are all things we’ve had to learn while making our smart contract (registries) upgradable. If you want to learn more about Eternal Storage then you can follow this EIP! If you are curious to develop more robust, unlimited sized, upgradable contracts, I recommend following this EIP.

(This article is originally posted by Satyam Agrawal on Polymath Blog.)