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 Gives the possibility to delegate any call to a foreign implementation.
 */
contract Proxy {
    /**
    * @dev Fallback function allowing to perform a delegatecall to the given implementation.
    * This function will return whatever the implementation call returns
    */
    function () public payable {
        address _impl = implementation();
        require(_impl != address(0), "Implementation cannot be zero address.");

        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize)
            let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
            let size := returndatasize
            returndatacopy(ptr, 0, size)

            switch result
            case 0 { revert(ptr, size) }
            default { return(ptr, size) }
        }
    }

    /**
    * @dev Tells the address of the implementation where every call will be delegated.
    * @return address of the implementation to which it will be delegated
    */
    function implementation() public view returns (address);
}

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, _impl, ptr, 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
  • ptr : 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 with state variables and functions to point to a new implementation address:

pragma solidity 0.4.25;

import "./Proxy.sol";
import "../lib/Address.sol";


/**
 * @title UpgradeabilityProxy
 * @dev This contract represents a proxy where the implementation address to which it will delegate can be upgraded
 */
contract UpgradeabilityProxy is Proxy {
    /**
    * @dev This event will be emitted every time the implementation gets upgraded
    * @param implementation representing the address of the upgraded implementation
    */
    event Upgraded(address indexed implementation);

    // Storage position of the address of the current implementation
    bytes32 private constant IMPLEMENTATION_POSITION = keccak256("org.zeppelinos.proxy.implementation");

    /**
    * @dev Tells the address of the current implementation
    * @return address of the current implementation
    */
    function implementation() public view returns (address impl) {
        bytes32 position = IMPLEMENTATION_POSITION;
        assembly {
            impl := sload(position)
        }
    }

    /**
    * @dev Sets the address of the current implementation
    * @param newImplementation address representing the new implementation to be set
    */
    function setImplementation(address newImplementation) internal {
        bytes32 position = IMPLEMENTATION_POSITION;
        assembly {
            sstore(position, newImplementation)
        }
    }

    /**
    * @dev Upgrades the implementation address
    * @param newImplementation representing the address of the new implementation to be set
    */
    function _upgradeTo(address newImplementation) internal {
        require(Address.isContract(newImplementation), "Cannot set a proxy implementation to a non-contract address");
        address currentImplementation = implementation();
        require(currentImplementation != newImplementation, "Upgraded implementation must be distinct from old implementation.");
        setImplementation(newImplementation);
        emit Upgraded(newImplementation);
    }
}

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

bytes32 private constant implementationPosition =               keccak256("org.zeppelinos.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.

OwnedUpgradeabilityProxy

Going further, we can define 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 OwnedUpgradeabilityProxy
 * @dev This contract combines an upgradeability proxy with basic authorization control functionalities
 * Based on https://github.com/zeppelinos/labs/tree/master/upgradeability_using_unstructured_storage
 */
contract OwnedUpgradeabilityProxy is UpgradeabilityProxy {
    /**
    * @dev Event to show ownership has been transferred
    * @param previousOwner representing the address of the previous owner
    * @param newOwner representing the address of the new owner
    */
    event ProxyOwnershipTransferred(address previousOwner, address newOwner);

    // Storage position of the owner of the contract
    bytes32 private constant PROXY_OWNER_POSITION = keccak256("org.zeppelinos.proxy.owner");

    // Proxy name metadata
    bytes32 private constant PROXY_NAME_POSITION = keccak256("org.zeppelinos.proxy.name");

    /**
    * @dev the constructor sets the original owner of the contract to the sender account.
    */
    constructor(string name) public {
        setUpgradeabilityOwner(msg.sender);
        setProxyName(name);
    }

    /**
    * @dev Throws if called by any account other than the owner.
    */
    modifier onlyProxyOwner() {
        require(msg.sender == proxyOwner(), "Only the proxy owner can execute this function.");
        _;
    }

    /**
    * @dev Tells the address of the owner
    * @return the address of the owner
    */
    function proxyOwner() public view returns (address owner) {
        bytes32 position = PROXY_OWNER_POSITION;
        assembly {
            owner := sload(position)
        }
    }

    /**
    * @dev Tells the name of the proxy
    * @return the name of the proxy
    */
    function proxyName() public view returns (address owner) {
        bytes32 position = PROXY_NAME_POSITION;
        assembly {
            owner := sload(position)
        }
    }    

    /**
    * @dev Allows the current owner to transfer control of the contract to a newOwner.
    * @param newOwner The address to transfer ownership to.
    */
    function transferProxyOwnership(address newOwner) public onlyProxyOwner {
        require(newOwner != address(0), "Owner must not be zero address.");
        emit ProxyOwnershipTransferred(proxyOwner(), newOwner);
        setUpgradeabilityOwner(newOwner);
    }

    /**
    * @dev Allows the proxy owner to upgrade the current version of the proxy.
    * @param implementation representing the address of the new implementation to be set.
    */
    function upgradeTo(address implementation) public onlyProxyOwner {
        _upgradeTo(implementation);
    }

    /**
    * @dev Allows the proxy owner to upgrade the current version of the proxy and call the new implementation
    * to initialize whatever is needed through a low level call.
    * @param implementation representing the address of the new implementation to be set.
    * @param data represents the msg.data to bet sent in the low level call. This parameter may include the function
    * signature of the implementation to be called with the needed payload
    */
    function upgradeToAndCall(address implementation, bytes data) public payable onlyProxyOwner {
        upgradeTo(implementation);
         // solhint-disable-next-line avoid-call-value
        require(address(this).call.value(msg.value)(data), "External call Must succeed.");
    }

    /**
    * @dev Sets the address of the owner
    */
    function setUpgradeabilityOwner(address newProxyOwner) internal {
        bytes32 position = PROXY_OWNER_POSITION;
        assembly {
            sstore(position, newProxyOwner)
        }
    }

    /**
    * @dev Sets the name of the proxy
    */
    function setProxyName(string newProxyName) internal {
        bytes32 position = PROXY_NAME_POSITION;
        assembly {
            sstore(position, newProxyName)
        }
    }        
}

Initializer Functions vs. Constructors

To deploy a contract we wish to upgrade, we can do the following:

// Deploy both implementation and proxy
const implementation = await deployer.deploy(MyContract);
const proxy = await deployer.deploy(OwnedUpgradeabilityProxy);

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

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

In order to make this work, you will need to change one more thing: your contract constructors.

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. Your contract needs to inherit from the Initializable contract and annotate its initializer function with the initializer modifier.

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;
}

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

// Helper for deploying upgradable contracts
const upgradable = async function(deployer, proxyContract, contract) {
  const implementation = await deployer.deploy(contract);
  const proxy = await deployer.deploy(proxyContract);
  await proxy.upgradeTo(implementation.address);
  const upgradable = await contract.at(proxy.address);
  return {
    proxy,
    implementation: upgradable,
  };
};

const { implementation: myContract } = await upgradable(deployer, MyProxy, MyContract);
await myContract.initialize(); // performs constructor logic

Your upgradable contracts will then be properly initialized.

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 →