Upgrading Smart Contracts

Smart contracts deployed with the OpenZeppelin CLI can be upgraded to modify their code, while preserving their address, state, and balance. This allows you to iteratively add new features to your project, or fix any bugs you may find in in production.

Throughout this guide, we will learn:

What’s in an Upgrade

Smart contracts in Ethereum are immutable by default. Once you create them there is no way to alter them, effectively acting as an unbreakable contract among participants.

However, for some scenarios, it is desirable to be able to modify them. Think of a traditional contract between two parties: if they both agreed to change it, they would be able to do so. On Ethereum, they may desire to alter a smart contract to fix a bug they found (which might even lead to a hacker stealing their funds!), to add additional features, or simply to change the rules enforced by it.

Here’s what you’d need to do to fix a bug in a contract you cannot upgrade:

  1. Deploy a new version of the contract

  2. Manually migrate all state from the old one contract to the new one (which can be very expensive in terms of gas fees!)

  3. Update all contracts that interacted with the old contract to use the address of the new one

  4. Reach out to all your users and convince them to start using the new deployment (and handle both contracts being used simultaneously, as users are slow to migrate)

To avoid going through this mess, we have built contract upgrades directly into our tools. This allows us to change the contract code, while preserving the state, balance, and address. And all by running a single command. Let’s see it in action.

Upgrading a Contract using the CLI

Whenever you deploy a new contract using the CLI via openzeppelin create, that contract instance can be upgraded later. By default, only the address that originally deployed the contract has the rights to upgrade it.

If you’re unfamiliar with the OpenZeppelin CLI, head to our introductory guide first!

Let’s see how it works, by upgrading the Box contract we deployed earlier:

// contracts/Box.sol
pragma solidity ^0.5.0;

contract Box {
    uint256 private value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}
If you had stopped and restarted ganache between the deployment guide and this one, you will need to first create a new Box instance, so you can now upgrade it.

For the sake of the example, let’s say we want to add a new feature: a function that increments the value stored in the Box.

// contracts/Box.sol
contract Box {
    // ...

    // Increments the stored value by 1
    function increment() public {
        value = value + 1;
        emit ValueChanged(value);
    }
}

After changing the Solidity file, we can now just upgrade the instance we had deployed earlier by running the openzeppelin upgrade command.

$ npx openzeppelin upgrade
? Pick a network: development
✓ Compiled contracts with solc 0.5.9
✓ Contract Box deployed
? Which instances would you like to upgrade?: All instances
Instance upgraded at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601.

Done! Our Box instance has been upgraded to the latest version of the code, while keeping its state and the same address as before. We didn’t need to deploy a new one at a new address, nor manually copy the value from the old Box to the new one.

Let’s try it out by invoking the new increment function, and checking the value afterwards:

$ npx openzeppelin send-tx
? Pick a network: development
? Pick an instance: Box at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
? Select which function: increment()
✓ Transaction successful: 0x9c84faf32a87a33f517b424518712f1dc5ba0bdac4eae3a67ca80a393c555ece
Events emitted:
- ValueChanged(6)

$ npx openzeppelin call
? Pick a network: development
? Pick an instance: Box at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
? Select which function: retrieve()
✓ Method 'retrieve()' returned: 6

That’s it! Notice how the value of the Box was preserved throughout the upgrade, as well as its address. And this process is the same regardless of whether you are working on a local blockchain, a testnet, or the main network. Let’s see how the OpenZeppelin CLI accomplishes this.

How Upgrades Work

This section will be more theory-heavy than others: feel free to skip over it and return later if you are curious.

When you create a new upgradeble contract instance, the OpenZeppelin CLI actually deploys two contracts:

  1. The contract you have written, which is known as the implementation contract or logic contract.

  2. A proxy to that contract, which is the contract that you actually interact with.

Here, the proxy is a simple contract that just delegates all calls to an implementation contract. A delegate call is similar to a regular call, except that all code is executed in the context of the caller, not of the callee. Because of this, a transfer in the implementation contract’s code will actually transfer the proxy’s balance, and any reads or writes to the contract storage will read or write from the proxy’s own storage.

This allows us to decouple a contract’s state and code: the proxy holds the state, while the logic contract provides the code. And it also allows us to change the code by just having the proxy delegate to a different implementation contract.

An upgrade then involves the following steps:

  1. Deploy the new implementation contract.

  2. Send a transaction to the proxy that updates its implementation address to the new one.

You can have multiple proxies using the same implementation contract, so you can save gas using this pattern if you plan to deploy multiple copies of the same contract.

Any user of the smart contract always interacts with the proxy, which never changes its address. This allows you to roll out an upgrade or fix a bug without requesting your users to change anything on their end - they just keep interacting with the same address as always.

If you want to learn more about how OpenZeppelin proxies work, check out OpenZeppelin Upgrades.

Upgrading Contracts Programmatically

If you want to create and upgrade contracts from your JavaScript code instead of via the command line, you can use the OpenZeppelin Upgrades library instead of the CLI.

The CLI does not just manage contract upgrades, but also compilation, interaction, and source code verification. The Upgrades library only takes care of creating and upgrading. The library also does not keep track of the contracts you have already deployed, nor runs any initializer or storage layout validations, as the CLI does. Nevertheless, these capabilities may be added to the Upgrades library in the near future.

Your first step will be to install the library in your project, and you will also probably want to install web3:

$ npm install @openzeppelin/upgrades web3

As in our previous guide on programmatically interacting with contracts, we will start with some boilerplate code to initialize a provider, as well as the Upgrades library.

const Web3 = require('web3');
const Upgrades = require('@openzeppelin/upgrades')

async function main() {
  // Set up web3 object, connected to the local development network, initialize the Upgrades library
  const web3 = new Web3('http://localhost:8545');
  Upgrades.ZWeb3.initialize(web3.currentProvider)
}

main();
You can check out a full version of the code in this section in the upgrades-library example of the SDK repository.

All our code from now on will be part of the main function. Let’s begin by creating a new project, to manage our upgradeable contracts.

const [from] = await ZWeb3.accounts();
const project = new ProxyAdminProject('MyProject', null, null, { from, gas: 1e6, gasPrice: 1e9 });
The Upgrades library ships with three different flavours of projects: SimpleProject, ProxyAdminProject, and AppProject. We recommend using the ProxyAdmin one to begin with. You can learn more in the Upgrades documentation.

Using this project, we can now create an instance of any contract. The project will take care of deploying it in such a way it can be upgraded later.

const MyContractV0 = Upgrades.Contracts.getFromLocal('MyContractV0');
const instance = await project.createProxy(MyContractV0);

After deploying the contract, you can upgrade it to a new version of the code using the upgradeProxy method, and providing the instance address.

const address = instance.options.address;
const MyContractV1 = Upgrades.Contracts.getFromLocal('MyContractV1');
await project.upgradeProxy(address, MyContractV1);

That’s it! You can now programmatically manage your contracts upgrades from your JavaScript code.

Limitations of Contract Upgrades

While any smart contract can be made upgradeable, some restrictions of the Solidity language need to be worked around. These come up when writing both the initial contract and the version we’ll upgrade it to.

Initialization

Upgradeable contracts cannot have a constructor. To help you run initialization code, OpenZeppelin Upgrades provides the Initializable base contract that allows you to tag a method as initializer, ensuring it can be run only once.

As an example, let’s write a new version of the Box contract with an initializer, storing the address of an admin who will be the only one allowed to change its contents.

// contracts/AdminBox.sol
pragma solidity ^0.5.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";

contract AdminBox is Initializable {
    uint256 private value;
    address private admin;

    function initialize(address _admin) public initializer {
        admin = _admin;
    }

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        require(msg.sender == admin);
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }
}

When deploying this contract, the CLI will prompt us to execute the initializer and ask us to provide the admin address.

$ npx oz create
✓ Compiled contracts with solc 0.5.9
? Pick a contract to instantiate: AdminBox
? Pick a network: development
✓ Contract AdminBox deployed
? Call a function to initialize the instance after creating it? Yes
? Select which function: initialize(_admin: address)
? _admin (address): 0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1
✓ Setting everything up to create contract instances
✓ Instance created at 0x2612Af3A521c2df9EAF28422Ca335b04AdF3ac66

For all practical purposes, the initializer acts as a constructor. However, keep in mind that since it’s a regular function, you will need to manually call the initializers of all base contracts (if any).

In future versions of the OpenZeppelin CLI, it will take care of automatically converting constructors into initializers, so you won’t need to worry about this.

To learn more about this and other caveats when writing upgradeable contracts, check out our Writing Upgradeable Contracts guide.

Upgrading

Due to technical limitations, when you upgrade a contract to a new version you cannot change the storage layout of that contract.

This means that, if you have already declared a state variable in your contract, you cannot remove it, change its type, or declare another variable before it. In our Box example, it means that we can only add new state variables after value.

// contracts/Box.sol
contract Box {
    uint256 private value;

    // We can safely add a new variable after the ones we had declared
    address private owner;

    // ...
}

Fortunately, this limitation only affects state variables. You can change the contract’s functions and events as you wish.

If you accidentally mess up with your contract’s storage layout, the CLI will warn you when you try to upgrade.

To learn more about this limitation, head over to the Modifying Your Contracts guide.

Next Steps

Now that you know how to upgrade your smart contracts, and can iteratively develop your project, it’s time to take your project to testnet and to production! You can rest with the confidence that, should a bug appear, you have the tools to modify your contract and change it.