Upgrading your project

At the end of the previous guide we deployed an OpenZeppelin SDK project with one contract. Here is the code of the contract, to keep it fresh on our minds:

pragma solidity ^0.5.0;

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

contract MyContract is Initializable {

  uint256 public x;
  string public s;

  function initialize(uint256 _x, string memory _s) initializer public {
    x = _x;
    s = _s;
  }
}

This is a traditional immutable contract that will remain frozen for ever on the blockchain, with mistakes, limited functionalities and everything. The OpenZeppelin SDK lets us opt-in to allow upgrades on our contracts, and open the doors to a more sustainable process for developing our projects. With upgrades we can make iterative releases, quickly adding small pieces of functionalities that we can adjust according to the always changing goals of our users; and of course, we can add fixes for any bugs we introduced on previous iterations. And just as with any other contract on the blockchain, we can define governance mechanisms to decide when and how to upgrade the contracts, that can be manual, automated, or any combination of both that will earn the trust of our users.

If any of the following commands fail with an A network name must be provided to execute the requested action error, it means our session has expired. In that case, renew it by running the command npx openzeppelin session --network local --from 0x1df62f291b2e969fb0849d99d9ce41e2f137006e --expires 3600 again.

Now let’s create an upgradeable instance of this contract so you can experiment with what this is all about:

npx openzeppelin create MyContract --init initialize --args 42,hitchhiker

The npx openzeppelin create command receives an optional --init [function-name] parameter to call the initialization function after creating the contract, and the --args parameter allows you to pass arguments to it. This way, you are initializing your contract with 42 as the value of the x state variable and hitchhiker as the value of the s state variable.

This command will print the address of your contract, and it will update the .openzeppelin/dev-<network_id>.json file.

When calling an initializer with many variables, these should be passed as a comma-separated list, with no spaces in between.

We can start a console to interact with our contract and check that it has been properly initialized:

npx truffle console --network local

Once in the Truffle console, execute the following instructions to test that our instance is working as expected:

Make sure you replace with the address returned by the create command we ran above.
truffle(local)> myContract = await MyContract.at('<your-contract-address>')
truffle(local)> (await myContract.x()).toString()
'42'

truffle(local)> myContract.s()
"hitchhiker"

You can now exit the Truffle console and continue with the following steps.

Upgrading the contract

Remember that for this guide we are using a ganache local development network. Do not stop the ganache-cli command that we ran before, or you will lose your previous deployment!

Now, let’s say we found an issue on our contract, or maybe we just want to extend its functionalities.

Open contracts/MyContract.sol, and add a new function:

pragma solidity ^0.5.0;

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

contract MyContract is Initializable {

  uint256 public x;
  string public s;

  function initialize(uint256 _x, string memory _s) initializer public {
    x = _x;
    s = _s;
  }

  function increment() public {
    x += 1;
  }
}
While the OpenZeppelin SDK supports arbitrary changes regarding functionality, you will need to preserve all the variables that appeared in previous versions of your contracts, declaring any new variables below the already existing ones. All the considerations and some recommendations for your upgrades are explained in the Writing upgradeable contracts page.

Once you have saved these changes, push the new code to the network:

npx openzeppelin push

Finally, let’s update the already deployed contract with the new code:

npx openzeppelin update MyContract

You will see that this command prints the same contract address as before, and a logic contract address that is new. This is all the magic behind upgrades: we have two contracts, one is the contract address that we will never change, but it just serves as a proxy to the logic contract that we can replace with new versions.

We can start a new Truffle console to interact with our contract and check that it has been properly upgraded:

npx truffle console --network local

Once in the Truffle console, execute the following instructions to try the new functionality we’ve just added:

Make sure you replace with the address of the upgradeable instance your created of MyContract. This address was returned by the create command we ran in the previous section, which is the same as the one returned by the update command we ran above.
truffle(local)> myContract = await MyContract.at('<your-contract-address>')
truffle(local)> myContract.increment()
truffle(local)> (await myContract.x()).toString()
43

Now let’s imagine that instead of just adding a new function to the contract (a change to functionality), we wanted to add a new variable t to our contract. But how do we set the initial value of t? The variables x and s were initialized with the initialize function, which was called when the proxy was created via the openzeppelin create MyContract --init initialize --args …​ command. Naturally, the solution would be to add the initialization of t to the end of the initialize function:

function initialize(uint256 _x, string memory _s, uint256 _t) initializer public {
  x = _x;
  s = _s;
  t = _t;
}

That would be fine for newly deployed instances of MyContract, but it wouldn’t work for one that has allready been deployed, and is instead being updated. We cannot call the same initialize function, because the Initializable modifier guards it against being called more than once. We need a new function.

The update command also accepts --init and --args parameters, so we can use a function with it to initialize the new variable. A good name for the new function could be something like initializeT or initializeVersion2. This function would simply set the initial value of t and check that it has not yet been initialized. It should be called with openzeppelin update MyContract --init initializeT --args 99.

function initializeT(uint256 _t) public {
  require(t == 0);
  t = _t;
}

This initialization validation for t, of course, would only make sense if t cannot be zero.

The resulting code would be:

pragma solidity ^0.5.0;

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

contract MyContract is Initializable {

  uint256 public x;
  string public s;
  uint256 public t;

  function initialize(uint256 _x, string memory _s, uint256 _t) initializer public {
    x = _x;
    s = _s;
    t = _t;
  }

  function initializeT(uint256 _t) public {
    require(t == 0);
    t = _t;
  }

  function increment() public {
    x += 1;
  }
}

Upgrades are only one of the features of the OpenZeppelin SDK. Next, we will see another very interesting feature, because it allows us to reuse packages that have been already deployed to the blockchain.