Yos Riady software craftsman 🌱

Upgrading Solidity Smart Contracts

Upgrading Solidity Smart Contracts

Web applications today are built iteratively. With continuous delivery, developers release hotfixes and new features hundreds of times a day. We upgrade our software all the time with little ceremony.

Upgradability is something that you don’t truly appreciate - until you’ve written smart contracts. Why? Because smart contracts are immutable - it’s not possible to upgrade the source code of an already deployed contract. In this aspect, developing smart contracts is closer to hardware programming than web development.

At the same time, decentralized applications and smart contracts are a new and highly experimental space. There are constant changes in the security landscape and the cost of failure are in the high tens or hundreds of millions of dollars. Code will need to be changed if errors are discovered or if improvements need to be made. It is no good to discover a bug, but have no way to deal with it.

As software engineers, we seek to build software systems that is modular and supports upgradable components. To handle the large space of smart contract attack vectors, we need a mechanism through which we can safely and securely upgrade our smart contracts. This is especially true when you build complex, perpetual contract systems instead of one-time token sale contracts.

In this article, we’ll explore how you can upgrade smart contracts by:

  • Pausing contracts when things are going wrong, to halt critical operations in case of an attack.
  • Having an effective upgrade path for bugfixes and improvements.
  • Having a secure way for parties to perform an upgrade.

Holding the Door with Circuit Breakers

Upgrading a smart contract is a non-trivial, slow process. A developer needs to first analyze the issues present in the current version before working on a new implementation. When rapid response is required, it’s sometimes useful to pause our contracts and enter some kind of ‘maintenance mode’. That’s where circuit breakers come in.

Circuit breakers stop execution if certain conditions are met, and can be useful when new errors are discovered. For example, some actions can be suspended in a contract if a bug is discovered. During this maintenance period, developers can write new contracts containing the fix, deploy them, and replace the old contracts while the exploit is stopped in its tracks.

To add circuit breakers, use a Pausable contract. This contract provides whenPaused and whenNotPaused function modifiers that you can apply to certain functions.

pragma solidity 0.4.25;

import "../roles/PauserRole.sol";

/**
 * @title Pausable
 * @dev Base contract which allows children to implement an emergency stop mechanism.
 */
contract Pausable is PauserRole {
    event Paused(address account);
    event Unpaused(address account);

    bool internal _paused;

    /**
    * @return true if the contract is paused, false otherwise.
    */
    function isPaused() public view returns(bool) {
        return _paused;
    }

    /**
    * @dev Modifier to make a function callable only when the contract is not paused.
    */
    modifier whenNotPaused() {
        require(!_paused, "Must not be paused");
        _;
    }

    /**
    * @dev Modifier to make a function callable only when the contract is paused.
    */
    modifier whenPaused() {
        require(_paused, "Must be paused");
        _;
    }

    /**
    * @dev called by the owner to pause, triggers stopped state
    */
    function pause() public onlyPauser whenNotPaused {
        _paused = true;
        emit Paused(msg.sender);
    }

    /**
    * @dev called by the owner to unpause, returns to normal state
    */
    function unpause() public onlyPauser whenPaused {
        _paused = false;
        emit Unpaused(msg.sender);
    }
}

To add circuit breaking mechanisms to your contract, simply inherit Pausable and apply the modifiers to any relevant functions. For example:

contract MyContract is Pausable {
  function deposit() public whenNotPaused {
      ...
  }

  function withdraw() public whenPaused {
      ...
  }  
}

In the above example, deposit is only callable when the contract is not paused whereas withdraw is only callable when the contract is.

You can also give certain parties roles that allow them to trigger the circuit breaker. We don’t want any stranger to stop our contracts from operating!

Alternatively, you can also have programmatic rules that automatically trigger the certain breaker when certain conditions are met, such as after a certain block number is reached.

Below is a PauserRole contract that adds a role-based authorization system for our circuit breaker:

pragma solidity 0.4.25;

import "openzeppelin-solidity/contracts/access/Roles.sol";

contract PauserRole {
    using Roles for Roles.Role;

    event PauserAdded(address indexed account);
    event PauserRemoved(address indexed account);

    Roles.Role private pausers;

    modifier onlyPauser() {
        require(isPauser(msg.sender), "Only Pausers can execute this function.");
        _;
    }

    function isPauser(address account) public view returns (bool) {
        return pausers.has(account);
    }

    function addPauser(address account) public onlyPauser {
        _addPauser(account);
    }

    function renouncePauser() public {
        _removePauser(msg.sender);
    }

    function _addPauser(address account) internal {
        pausers.add(account);
        emit PauserAdded(account);
    }

    function _removePauser(address account) internal {
        pausers.remove(account);
        emit PauserRemoved(account);
    }
}

Depending on your use case, code changes may need to be approved by a single trusted party, a group of members, or a vote of the full set of stakeholders. You can use the above roles contract as the foundation for more complex upgrade authorization schemes.

Smart Contract Upgrade Mechanisms

Designing an effective upgrade system for smart contracts is an area of active research. Here are three approaches that are most commonly used.

  1. The simplest approach is to store a mutable reference to an external subcomponent in your main contract.

  2. Another simple approach is to have a registry contract that holds the address of the latest version of the contract.

  3. A more seamless approach for contract users is to have a contract that forwards calls and data onto the latest version of the contract.

In general, it’s important to have modularization and good separation between components, so that code changes do not break functionality, orphan data, or require substantial costs to port.

Let’s have a look at each approach in detail.

1. Upgradability with Hub-and-Spoke

You can decompose your contract system into a hub-and-spoke model consisting of several external subcomponent contracts. The main contract knows the addresses and interfaces of these subcomponents and is able to make external calls to them. For example:

contract Hub {
  Spoke spoke public;

  constructor(Spoke _spoke) {
    spoke = _spoke;
  }

  function doSomething() public {
    spoke.run();
  }

  function setSpoke(Spoke _newSpoke) external {
    spoke = _newSpoke;
  }
}

interface Spoke {
  function run() returns (bool);
}

contract SpokeV0 is Spoke {
  function run() returns (bool) {
    return true;
  }
}

The primary Hub contract contains a reference to an external Spoke contract in the state variable spoke. It makes an external contract call spoke.run() in the definition of doSomething().

We can upgrade the spoke contract by first deploying a new fresh contract that implements the Spoke interface. Then, we call Hub.setSpoke() and supply the address of the newly deployed version. The Hub contract will now make external calls to the Spoke contract deployed at the new address.

In production use, you’ll want to restrict who can call setSpoke() to a set of trusted users.

The drawback with this approach is any state variables within the old Spoke contract is unavailable to the newly deployed version. The old and new versions of Spoke are distinct contracts with distinct storage and addresses. When the new version is deployed, it will have its storage initialized to empty.

The only way for the new version to access the old state is to have the new version make external getter calls to the old version, which makes the overall system more brittle and cost additional gas.

Once you’ve upgraded to the new implementation, you can remove the deprecated contract with selfdestruct when it is no longer used.

2. Upgradability with Registries

A variation of the hub-and-spoke model is to have a dedicated Registry contract to store a mutable directory of subcomponent contract addresses.

contract Registry {
  mapping (bytes32 => address) public components;

  function set(bytes32 name, address contractAddress);
  function get(bytes32 name);
  function delete(bytes32 name);
}

In this approach, our main contract will make an external call to the registry contract to find the address of a component’s current implementation. The registry address is fixed and only set during the initial contract creation step.

All address state changes and role-based authorization checks are isolated within the registry contract.

3. Upgradability with Proxies

A major drawback of the upgrade mechanisms we’ve seen so far is the fact that state is not preserved during the move from an old to new implementation.

An alternate state-preserving upgrade mechanism is to use a low-level delegatecall to forward functionality and data to another contract whilst operating in the state of the current contract.

Although it is not possible to upgrade the code of your already deployed smart contract, it is possible to set-up a proxy contract architecture that will allow you to use new deployed contracts as if your main logic had been upgraded.

From here on out, we will examine in detail ZeppelinOS’ unstructured storage smart contract upgrade mechanism.

Pre-requisites

Before we go into delegatecall, there are a few concepts that you need to understand:

  • When a function call to a contract is made that it does not support, the fallback function will be called. You can write a custom fallback function to handle such scenarios. The proxy contract uses a custom fallback function to redirect calls to other contract implementations.

  • There exists a special variant of a Solidity message call, named delegatecall which is identical to a call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values. This means that 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.

  • In other words, whenever a contract A delegates a call to another contract B, it executes the code of contract B in the state and context of contract A. This means that msg.value and msg.sender values will be kept and every storage modification will impact the storage of contract A instead of B.

  • There is a tradeoff between upgradability and security. Upgradability adds complexity and new attack vectors. Foregoing upgradability gives you no tools to tackle vulnerabilities.

Proxy

This diagram illustrates our final proxy contract structure:

Here is our Proxy contract with a fallback function:

pragma solidity 0.4.25;

/**
 * @title Proxy
 * @dev Implements delegation of calls to other contracts, with proper
 * forwarding of return values and bubbling of failures.
 * It defines a fallback function that delegates all calls to the address
 * returned by the abstract _implementation() internal function.
 */
contract Proxy {
    /**
    * @dev Fallback function.
    * Implemented entirely in `_fallback`.
    */
    function () external payable {
        _fallback();
    }

    /**
    * @return The Address of the implementation.
    */
    function _implementation() internal view returns (address);    

    /**
    * @dev Delegates execution to an implementation contract.
    * This is a low level function that doesn't return to its internal call site.
    * It will return to the external caller whatever the implementation returns.
    * @param implementation Address to delegate.
    */
    function _delegate(address implementation) internal {
        assembly {
            // Copy msg.data. We take full control of memory in this inline assembly
            // block because it will not return to Solidity code. We overwrite the
            // Solidity scratch pad at memory position 0.
            calldatacopy(0, 0, calldatasize)

            // Call the implementation.
            // out and outsize are 0 because we don't know the size yet.
            let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0)

            // Copy the returned data.
            returndatacopy(0, 0, returndatasize)

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

    /**
    * @dev Function that is run as the first thing in the fallback function.
    * Can be redefined in derived contracts to add functionality.
    * Redefinitions must call super._willFallback().
    */
    function _willFallback() internal {
    }

    /**
    * @dev fallback implementation.
    * Extracted to enable manual triggering.
    */
    function _fallback() internal {
        _willFallback();
        _delegate(_implementation());
    }
}

Within the contract’s fallback function, the assembly block contains logic that passes received input data to another implementation address.

Here is the key delegatecall:

let result := delegatecall(gas, implementation, 0, calldatasize, 0, 0)

The parameters are as follows:

  • gas : we pass in the gas needed to execute the function
  • _impl : the address of the logic contract we’re calling
  • 0 : the memory pointer for where data starts
  • calldatasize : the size of the data we’re passing.
  • 0 : for data out representing the returned value from calling the logic contract. This is unused because we do not yet know the size of data out and therefore cannot assign it to a variable. We can still access this information using returndata opcode later
  • 0 : for size out. This is unused because we didn’t get a chance to create a temp variable to store data out, since we didn’t know the size of it prior to calling the other contract. We can get this value using an alternative way by calling the returndatasize opcode later.

UpgradeabilityProxy

We can extend the Proxy contract into an UpgradeabilityProxy contract with state variables and functions to point to a new implementation address:

pragma solidity 0.4.25;

import "./Proxy.sol";
import "openzeppelin-solidity/contracts/utils/Address.sol";


/**
 * @title UpgradeabilityProxy
 * @dev This contract implements a proxy that allows to change the
 * implementation address to which it will delegate.
 * Such a change is called an implementation upgrade.
 */
contract UpgradeabilityProxy is Proxy {
    /**
    * @dev Emitted when the implementation is upgraded.
    * @param implementation Address of the new implementation.
    */
    event Upgraded(address indexed implementation);

    /**
    * @dev Storage slot with the address of the current implementation.
    * This is the keccak-256 hash of "org.myapp.proxy.implementation", and is
    * validated in the constructor.
    */
    bytes32 private constant IMPLEMENTATION_SLOT = <keccak256 hash>;

    /**
    * @dev Contract constructor.
    * @param _implementation Address of the initial implementation.
    */
    constructor(address _implementation) public payable {
        assert(IMPLEMENTATION_SLOT == keccak256("org.myapp.proxy.implementation"));
        _setImplementation(_implementation);
    }

    /**
    * @dev Returns the current implementation.
    * @return Address of the current implementation
    */
    function _implementation() internal view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            impl := sload(slot)
        }
    }

    /**
    * @dev Upgrades the proxy to a new implementation.
    * @param newImplementation Address of the new implementation.
    */
    function _upgradeTo(address newImplementation) internal {
        _setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }    

    /**
    * @dev Sets the implementation address of the proxy.
    * @param newImplementation Address of the new implementation.
    */
    function _setImplementation(address newImplementation) private {
        require(Address.isContract(newImplementation), "Cannot set a proxy implementation to a non-contract address");
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            sstore(slot, newImplementation)
        }
    }
}

Key to this ‘unstructured storage’ approach is this line:

bytes32 private constant IMPLEMENTATION_SLOT = <hash equal to keccak256("org.myapp.proxy.implementation")>;

To store data related to upgradability, we use an unstructured storage slot in the proxy contract. In the proxy contract we define a constant variable that, when hashed, should give a random enough storage position to store the address of the implementation contract that the proxy should call to.

In short, this line frees us from having to think about Solidity storage memory layouts when preserving state variables across contract versions.

Since constant state variables do not occupy storage slots, there’s no concern of the implementationPosition being accidentally overwritten by the implementation contract. Due to how Solidity lays out its state variables in storage there is extremely little chance of collision of this storage slot being used by something else defined in the implementation contract.

By using this pattern, none of the implementation contract versions have to know about the storage structure of the proxy, however all future implementation contracts must inherit the storage variables declared by their ancestor versions. Future upgraded token implementation contracts can override existing functions as well as introduce new functions and new storage variables.

AdminUpgradeabilityProxy

Going further, we can define AdminUpgradeabilityProxy with an upgradeTo() function as well as proxy owners and metadata. A proxy owner is the only address that can upgrade a proxy to point to a new logic contract, and the only address that can transfer ownership.

pragma solidity 0.4.25;

import "./UpgradeabilityProxy.sol";


/**
 * @title AdminUpgradeabilityProxy
 * @dev This contract combines an upgradeability proxy with an authorization
 * mechanism for administrative tasks.
 * All external functions in this contract must be guarded by the
 * `ifAdmin` modifier. See ethereum/solidity#3864 for a Solidity
 * feature proposal that would enable this to be done automatically.
 */
contract AdminUpgradeabilityProxy is UpgradeabilityProxy {
    /**
    * @dev Emitted when the administration has been transferred.
    * @param previousAdmin Address of the previous admin.
    * @param newAdmin Address of the new admin.
    */
    event AdminChanged(address previousAdmin, address newAdmin);

    /**
    * @dev Storage slot with the admin of the proxy contract.
    * This is the keccak-256 hash of "org.myapp.proxy.admin", and is
    * validated in the constructor.
    */
    bytes32 private constant ADMIN_SLOT = <keccak256 hash>;

    /**
    * @dev Storage slot with the name of the proxy contract.
    * This is the keccak-256 hash of "org.myapp.proxy.name", and is
    * validated in the constructor.
    */
    bytes32 private constant NAME_SLOT = <keccak256 hash>;

    /**
    * @dev Modifier to check whether the `msg.sender` is the admin.
    * If it is, it will run the function. Otherwise, it will delegate the call
    * to the implementation.
    */
    modifier ifAdmin() {
        if (msg.sender == _admin()) {
            _;
        } else {
            _fallback();
        }
    }

    /**
    * Contract constructor.
    * It sets the `msg.sender` as the proxy administrator.
    * @param _implementation address of the initial implementation.
    * @param _name name of the proxy contract.
    */
    constructor(string _name, address _implementation) public UpgradeabilityProxy(_implementation) payable {
        assert(ADMIN_SLOT == keccak256("org.myapp.proxy.admin"));
        _setName(_name);        
        _setAdmin(msg.sender);
    }

    /**
    * @dev Changes the admin of the proxy.
    * Only the current admin can call this function.
    * @param newAdmin Address to transfer proxy administration to.
    */
    function changeAdmin(address newAdmin) external ifAdmin {
        require(newAdmin != address(0), "Cannot change the admin of a proxy to the zero address");
        emit AdminChanged(_admin(), newAdmin);
        _setAdmin(newAdmin);
    }

    /**
    * @dev Upgrade the backing implementation of the proxy.
    * Only the admin can call this function.
    * @param newImplementation Address of the new implementation.
    */
    function upgradeTo(address newImplementation) external ifAdmin {
        _upgradeTo(newImplementation);
    }

    /**
    * @dev Upgrade the backing implementation of the proxy and call a function
    * on the new implementation.
    * This is useful to initialize the proxied contract.
    * @param newImplementation Address of the new implementation.
    * @param data Data to send as msg.data in the low level call.
    * It should include the signature and the parameters of the function to be called, as described in
    * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding.
    */
    function upgradeToAndCall(address newImplementation, bytes data) external payable ifAdmin {
        _upgradeTo(newImplementation);
        require(newImplementation.delegatecall(data), "Delegate call failed.");
    }

    /**
    * @return The address of the proxy admin.
    */
    function admin() external view ifAdmin returns (address) {
        return _admin();
    }

    /**
    * @return The string of the proxy name.
    */
    function name() external view ifAdmin returns (string) {
        return _name();
    }    

    /**
    * @return The address of the implementation.
    */
    function implementation() external view ifAdmin returns (address) {
        return _implementation();
    }    

    /**
    * @return The admin slot.
    */
    function _admin() internal view returns (address adm) {
        bytes32 slot = ADMIN_SLOT;
        assembly {
            adm := sload(slot)
        }
    }

    /**
    * @dev Sets the address of the proxy admin.
    * @param newAdmin Address of the new proxy admin.
*/
    function _setAdmin(address newAdmin) internal {
        bytes32 slot = ADMIN_SLOT;

        assembly {
            sstore(slot, newAdmin)
        }
    }

    /**
    * @return The admin slot.
    */
    function _name() internal view returns (string nm) {
        bytes32 slot = NAME_SLOT;
        assembly {
            nm := sload(slot)
        }
    }

    /**
    * @dev Sets the name of the proxy contract.
    * @param newName Name of the proxy contract.
    */
    function _setName(string newName) internal {
        bytes32 slot = NAME_SLOT;

        assembly {
            sstore(slot, newName)
        }
    }    

    /**
    * @dev Only fall back when the sender is not the admin.
    *
    function _willFallback() internal {
        require(msg.sender != _admin(), "Cannot call fallback function from the proxy admin");
        super._willFallback();
    }
}

Initializer Functions vs. Constructors

To deploy a contract we wish to upgrade, we need to deploy both the implementation contract and the proxy contract:

// Deploy both implementation and proxy
const implementation = await deployer.deploy(MyContract);
const proxy = await deployer.deploy(AdminUpgradeabilityProxy, "MyImplementationProxy", implementation.address);

// Set proxy implementation
await proxy.upgradeTo(implementation.address);

// Load implementation ABI, but point to proxy address
const upgradable = await MyContract.at(proxy.address);

However, in order to initialize your contract’s state you will need to change one more thing: to use an initializer function instead of your contract constructor.

Let’s have a look. Deploying contracts with Truffle normally looks like this:

module.exports = async (deployer) => {
  await deployer.deploy(MyContract);
}

Deploying a contract will automatically execute any constructors logic defined in the contract as well as the contracts it inherits from. However, due to the nature of the proxy pattern the constructor logic is executed in the wrong context.

A contract’s constructor logic is executed within its own context. when it is deployed. The proxy has its own storage and its own address. We need to find a way to initialize that instead of the logic contract. Because the state lives in the proxy contract instead of the implementation, the initialization needs to be executed at the proxy context and not depend on the constructor.

If your logic contract relies on its constructor to set up some initial state, this has to be initialized in a separate initialize() function after the proxy upgrades to your logic contract. For example:

contract MyContract is Pausable, PauserRole, Initializable {
    function initialize() external initializer {
        _paused = false; // Pausable
        _addPauser(msg.sender); // PauserRole
    }
}

The initializer function should mimic everything you would traditionally put in a constructor. Care must be taken to protect the function so that it can only run once for a given instance—otherwise our contract runs the risk of being initialized twice, potentially by an attacker.

ZeppelinOS’ Initializable contract helps you write initializer functions:

pragma solidity 0.4.25;

/**
 * @title Initializable
 *
 * @dev Helper contract to support initializer functions. To use it, replace
 * the constructor with a function that has the `initializer` modifier.
 * WARNING: Unlike constructors, initializer functions must be manually
 * invoked. This applies both to deploying an Initializable contract, as well
 * as extending an Initializable contract via inheritance.
 * WARNING: When used with inheritance, manual care must be taken to not invoke
 * a parent initializer twice, or ensure that all initializers are idempotent,
 * because this is not dealt with automatically as with constructors.
 */
contract Initializable {

    /**
    * @dev Indicates that the contract has been initialized.
    */
    bool private initialized;

    /**
    * @dev Indicates that the contract is in the process of being initialized.
    */
    bool private initializing;

    /**
    * @dev Modifier to use in the initializer function of a contract.
    */
    modifier initializer() {
        require(initializing || isConstructor() || !initialized, "Contract instance has already been initialized");

        bool wasInitializing = initializing;
        initializing = true;
        initialized = true;

        _;

        initializing = wasInitializing;
    }

    /// @dev Returns true if and only if the function is running in the constructor
    function isConstructor() private view returns (bool) {
        // extcodesize checks the size of the code stored in an address, and
        // address returns the current address. Since the code is still not
        // deployed when running a constructor, any checks on its code size will
        // yield zero, making it an effective way to detect if a contract is
        // under construction or not.
        uint256 cs;
        assembly { cs := extcodesize(address) }
        return cs == 0;
    }

    // Reserved storage space to allow for layout changes in the future.
    uint256[50] private ______gap;
}

Your contract needs to inherit from the Initializable contract and annotate its initializer function with the initializer modifier:

contract MyContract is Initializable {
    function initialize(address myToken) external initializer {
        _token = myToken;
    }
}

During a deployment, right after your proxy is deployed, you will need to call the initialize() to perform any constructor logic:

const implementation = await MyContract.new();
await implementation.initialize(); // performs constructor logic

Your upgradable contracts will then be properly initialized.

Upgradable Helper

To simplify the process of deploying an upgradable contract, I’ve written an upgradable helper function:

module.exports = async (deployer, contract, proxyContract, proxyAdminAddress) => {
  const implementation = await deployer.deploy(contract);
  const proxy = await deployer.deploy(proxyContract, implementation.address);
  const upgradable = await contract.at(proxy.address);
  await proxy.changeAdmin(proxyAdminAddress); // Needed because proxy admins can't initialize
  return {
    proxy,
    implementation: upgradable,
  };
};

const { implementation } = await upgradable(deployer, MyContract, MyProxy);
await implementation.initialize();

Re-initialization of new contract versions

Sometimes, you want to call a new initialization function alongside a contract upgrade. AdminUpgradeabilityProxy offers the upgradeToAndCall function:

/**
* @dev Upgrade the backing implementation of the proxy and call a function
* on the new implementation.
* This is useful to initialize the proxied contract.
* @param newImplementation Address of the new implementation.
* @param data Data to send as msg.data in the low level call.
* It should include the signature and the parameters of the function to be called, as described in
* https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding.
*/
function upgradeToAndCall(address newImplementation, bytes data) external payable ifAdmin {
    _upgradeTo(newImplementation);
    require(newImplementation.delegatecall(data), "Delegate call failed.");
}

To use it, you need to pass in bytes data that include the signature and the parameters of the function to be called, encoded in Ethereum’s encoding format. We can define a helper function to help us perform the encode step:

const abi = require('ethereumjs-abi');
const BN = require('bn.js');

function formatValue(value) {
  if (typeof (value) === 'number' || BN.isBigNumber(value)) {
    return value.toString();
  } else if (typeof (value) === 'string' && value.match(/\d+(\.\d+)?e(\+)?\d+/)) {
    return (new BN(value)).toString(10);
  }
  return value;
}

function encodeCall(name, args = [], rawValues = []) {
  const values = rawValues.map(formatValue);
  const methodId = abi.methodID(name, args).toString('hex');
  const params = abi.rawEncode(args, values).toString('hex');
  return `0x${methodId}${params}`;
}
module.exports = encodeCall;

The encodeCall transforms a function name, args, and rawValues into the right format. We can then call upgradeToAndCall by passing in the encoded initializer function signature and arguments:

const initializeData = encodeCall(
  "initialize",
  ['address', 'string', 'string', 'uint8', 'address'],
  [owner, name, symbol, decimals, exampleToken.address]
);

await proxy.upgradeToAndCall(logicContract.address, initializeData, { from: proxyOwner }

Writing New Contract Versions

In order for new versions of your contract to be able to access old state, you will need to inherit the state variables of the old contract - this can be done via simple inheritance:

// Old implementation
contract MyContract {
  bool internal _isChristmas;

  function setChristmas(bool status) {
    _isChristmas = status;
  }
}

// New implementation
contract MyContractV2 is MyContract {
  uint256 internal _newStateVariable;

  function setChristmas(bool status) {
    ... new function implementation
  }

  function newFunction(){
    ...
  }  
}

Then, deploy the new version and set it as the proxy’s current implementation:

const newImplementation = await deployer.deploy(MyContractV2);
await proxy.upgradeTo(newImplementation.address);

And that’s it! You’ve deployed a new version of your contract which is able to access state variables used in previous versions.

With this upgrade mechanism, minimal modifications are required to make your contracts upgradeable! Your smart contracts don’t even know that they are part of a proxy system. This is ZeppelinOS’ currently favoured approach to smart contract upgradability, and it’s easy to see why.

Summary

Software has an inherent need for evolvability in response to changing requirements, and smart contract systems are no different.

In this article, we examined three different upgradability approaches available to smart contract systems: the hub-and-spoke model, the registry model, and the proxy model.

Thank you for reading and I hope this was useful or otherwise interesting.

Author

Yos is a software craftsman based in Singapore.

📬 Subscribe to my newsletter

Get notified of my latest articles by providing your email below.


Going Serverless book

Interested to find out more about serverless? Going Serverless teaches you how to build scalable applications with the Serverless framework and AWS Lambda. You'll learn how to design, develop, test, deploy, and secure Serverless applications from planning to production.

Learn More →