Shyft Tech Update: Implementing Smart Contract Upgradability

Over the past few months I’ve been working with Shyft to implement a form of contract upgradability specifically designed for working with tokens.

There is currently no set method of implementing contract upgrades. In this article, I will cover various methods of “upgrading” a contract, and their respective pros and cons.

The methods:

  1. Keep your main contract as a “database,” storing information such as balances, and allow functions to be upgraded by doing external calls.
  2. Keep a registry of different versions of contracts; the registry contract will then be called and relay the call to the current version of the contract. Alternatively, if the old version is called, it will send it to the registry, which will then send it to the newest contract version.
  3. Use a proxy contract to perform delegate calls and an eternalStorage contract to keep contract storage.

The first method requires forwards compatibility; we must know in advance which methods we want to upgrade.

The second method requires both forwards and backwards compatibility of contracts; make sure the original contract has plenty of get/set functions so you’re able to get previously stored values. This allows for the new contract to get values as needed from the old contract.

This article is going to include some Solidity assembly. For background on inline assembly, see my previous article.

Method #1: External calls with predetermined functions

Forwards compatibility: deploy the original contract with built-in functions that will perform an external call to the upgraded version of the function. The issue with this method is that we need to have the exact same function name and parameters as specified in the call. Also, we are limited to predetermined functions.

An example:

bool isUpgraded;
address newContract;
mapping (address => uint256) balances;
function setBalance(address _addr, uint256 _val) {
if(isUpgraded){
// function signature
bytes4 sig = bytes4(keccak256(“setBalance(address,uint256)”)); //make sure the hash doesn’t have spaces
newContract.call(sig, _addr, _val); // call setBalance in new contract, passing in signature and parameters
}
else {
balances[_addr] = _val;
}
}

Maybe there’s another way. Let’s try passing in the function signature:

contract OldContract {
// _sig = bytes4(keccak256(“callMe()”));
function upgradeMe(bytes4 _sig) {
newContract.call(_sig);
}
}
contract NewContract {
function callMe() {

}
}

There is a problem here: if callMe() has parameters, how do we know in the old contract what they’ll be? Same issue as before: we need to predetermine the parameter types for the upgraded functions.

What if you want to get the return value from the call? We need to use assembly! Assembly in solidity is designated by `assembly { … }`. Within the inline assembly block, syntax is different than usual. New variables are declared with ` let var := … ` opposed to the usual type x = …

Inside the assembly block, we directly use opcodes as specified in the ethereum yellow paper. For specifics on the opcodes used, refer to the solidity documentation.

contract OldContract {
// _sig = bytes4(keccak256(“sum(uint,uint)”));

function upgradeMe(bytes4 _sig, address _newContract, uint _a, uint _b) returns (uint ans) {
assembly {
// free memory pointer : 0x40

let x := mload(0x40) // get empty storage location
mstore ( x, sig ) // 4 bytes — place signature in empty storage
mstore (add(x, 0x04), _a) // 32 bytes — place first argument next to 4-bit signature
mstore (add(x, 0x24), _b) // 32 bytes — place second argument after first argument

let ret := call (gas,
_newContract,
0, // no wei value passed to function
x, // input
0x44, // input size = 32 + 32 + 4 bytes
x, // output stored at input location, save space
0x20 // output size = 32 bytes
)

ans := mload(x)
mstore(0x40, add(x,0x20)) // update free memory pointer
}
}
}
contract NewContract {
function sum(uint _a, uint _b) returns (uint) {
return _a + _b;
}
}

In this implementation, we are doing a call to an external function. This will only change the values of variables in the called contract, not the caller. To use the new function as a “library” function, we will use `delegatecall`. This allows the variables inside the caller contract to be changed.

contract oldContract {
bool isUpgraded;
address newContract;
mapping (address => uint256) balances;
function setBalance(address _addr, uint256 _val) {
if(isUpgraded){
// function signature
bytes4 sig = bytes4(keccak256(“setBalance(address,uint256)”)); //make sure the hash doesn’t have spaces
// delegate call will allow the called function to make changes on the values inside this contract
// usually used for libraries
newContract.delegatecall(sig, _addr, _val); // delegatecall setBalance in new contract, passing in signature and parameters
}
else {
balances[_addr] = _val;
}
}
}
contract newContract {
function setBalance(address _addr, uint256 _val){
// do stuff, changing balances in oldContract
}
}

For this method, we would also want to use an interface. An interface is a contract that doesn’t implement any functions, but instead has abstract function declarations in it. Abstract functions end with a `;` instead of a `{ … }`. The functions declared in the interface will need to be implemented by another contract that inherits from it, otherwise the code won’t compile. Interfaces force you to use predetermined functions. An interface for the above would look like this:

interface oldContract {
function setBalance(address _addr, uint256 _val);
}
interface newContract {
function setBalance(address _addr, uint256 _val);
}
contract oldContract {
// same code as before
}
contract newContract {
// same code as before
}

Method #2: Use a registry contract

A registry contract keeps track of the latest version of the contract. The registry contract would have identical function names and parameters as the user would call a function in the registry contract, which would then perform a call to the function with the same name in the latest version of the contract.

If updating a storage value, the registry contract would first have to get the needed storage values from the previous version of the contract. In terms of gas costs, this method will be around the same as the first method, as they both involve an external call and a storage update.

A simple example:

contract Registry {
address[] versions;
uint currentVersion;
mapping(address => bool) isUpdated;

function addBalance(address _addr, uint256 _val) {
bytes4 sig = bytes4(keccak256(“addBalance(address,uint256)”));
if(!isUpdated[_addr]) {
bytes4 sigGet = bytes4(keccak256(“getBalance(address)”));
address prevVersion = versions[currVersion-1];

// use assembly to call getBalance in previous contract so we can update
// value in current contract correctly
assembly {
let x := mload(0x40) // get empty storage location
mstore ( x, sigGet ) // 4 bytes — place signature in empty storage
mstore (add(x, 0x04), _addr) // 20 bytes — place first argument next to 4-bit signature

let ret := call (gas,
prevVersion,
0, // no wei value passed to function
x, // input
0x18, // input size = 20 + 4 bytes
x, // output stored at input location, save space
0x20 // output size = 32 bytes
)

let ans := mload(x)
mstore(0x40, add(x,0x20)) // update free memory pointer
}

uint _val += ans;
}

versions[currVersion].call(sig, _addr, _val);
}
contract oldContract {
mapping (address => uint256) public balances;
function addBalance(address _addr, uint256 _val) {
balances[_addr] += _val;
}

function getBalance(address _addr) returns (uint) {
return balances[_addr];
}
}
contract newContract {
function addBalance(address _addr, uint256 _val){
// do stuff
}
}

In practice, you would want the upgradable function to have more functionality.

Method #3: Use a proxy contract and eternal storage contract

For an example, check out contracts/upgradeability in the POA bridge contracts repo. Also, aragonOS has an awesome implementation of a DAO with upgradability built in.

Suggestions

General upgradability suggestions:

  1. Save storage in original or separate storage contract; this would preclude large data transfers.
  2. To add new functions, we want to pass in a new function signature to a pre-created function that performs a delegatecall to new function (a proxy function).
  3. Encapsulate logic in “libraries” to keep contracts modular.

What was implemented at Shyft?

In the Shyft smart contract suite, I implemented a reduced form of upgradability in which only certain functions for Shyft tokens were made upgradable. I used a registry contract to keep track of token contract versions and only certain functions were made upgradable. Commonly-used functions should be fixed, as adding an external call to them will increase gas costs.

I hope this article was helpful for understanding how we’ve implemented smart contract upgrades. Thanks for reading!

***
Shyft is building the world’s first modern, secure, multi-stakeholder Blockchain-based trust network that enables KYC/AML attested data transfers. Join our Telegram (
https://t.me/shyftnetwork), follow us on Twitter (https://twitter.com/shyftnetwork), GitHub (https://github.com/ShyftNetwork) and other channels found on https://www.shyft.network/