Creating Upgradeable Contracts From Solidity

The OpenZeppelin CLI lets you create upgradable contracts from the command line, and you can also use Upgrades directly from JavaScript, but there is a third use case: creating an upgradable contract directly from another contract.

In this guide, we will learn how to create a contract factory where the resulting contracts are themselves upgradeable.

This guide features advanced usage of OpenZeppelin tools, and requires familiarity with Solidity, development blockchains and the OpenZeppelin CLI.

For a refresher on the topics, head to Deploying and Interacting With Smart Contracts.

Setting Up

Start by initializing an OpenZeppelin project using the CLI:

$ npx oz init creating-from-solidity 1.0.0

The project name is important: we’ll be using it later from the Solidity code.

We can now write the code for Product, a simple contract that stores a value. This is the contract that will be created by our contract factory.

// contracts/Product.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

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


contract Product is Initializable {
    uint256 public value;

    function initialize(uint256 _value) public initializer {
        value = _value;
    }
}

The Factory is a bit more involved: we’ll provide it with our project’s App contract, and use it to create new upgradeable instances of Product. This is where your project’s name comes into play: you’ll need to provide this information to App:

// contracts/Factory.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;

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


contract Factory is Initializable {
    App private app;

    event InstanceCreated(address);

    function initialize(App _app) public initializer {
        app = _app;
    }

    function createInstance(bytes memory _data) public {
        string memory packageName = "creating-from-solidity";
        string memory contractName = "Product";
        address admin = msg.sender;

        address product = address(
            app.create(packageName, contractName, admin, _data)
        );

        emit InstanceCreated(product);
    }
}

Connecting Our Factory to the App

The Factory 's initialize method requires that we provide the address of our App. The App is an on-chain representation of your project, and must be published explicitly for us to use it this way.

To do this, we’ll first need to add the Product contract to our App (so that it can deploy new ones), push or deploy the underlying contracts, and finally publish the whole project to the blockchain

$ npx oz add Product
✓ Compiled contracts with solc 0.5.17 (commit.d19bba13)
✓ Added contract Product
$ npx oz push
Nothing to compile, all contracts are up to date.
? Pick a network development
✓ Contract Product deployed
All implementations have been deployed
$ npx oz publish
? Pick a network development
✓ Project structure deployed
✓ Registering Product at 0xe78A0F7E598Cc8b0Bb87894B0F60dD2a88d6a8Ab in directory
✓ Published to dev-1591071938836!

Now that the App has been published and Product registered inside it, we’re ready to use it! Head to your project’s network configuration file and look for the app entry:

// .openzeppelin/dev-1591071938836.json
  ...
  "app": {
    "address": "0x5b1869D9A4C187F2EAa108f3062412ecf0526b24"
  },
  ...

With the App address at hand, we can deploy Factory using oz deploy as usual:

$ npx oz deploy
Nothing to compile, all contracts are up to date.
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy Factory
✓ Added contract Factory
✓ Contract Factory deployed
All implementations have been deployed
? Call a function to initialize the instance after creating it? Yes
? Select which function initialize(_app: address)
? _app: address: 0x5b1869D9A4C187F2EAa108f3062412ecf0526b24
✓ Setting everything up to create contract instances
✓ creating-from-solidity Factory instance created at 0x9b1f7F645351AF3631a656421eD2e40f2802E6c0
To upgrade this instance run 'oz upgrade'
0x9b1f7F645351AF3631a656421eD2e40f2802E6c0

Encoding Call Data

Our Factory is ready for its createInstance method to be called, but there’s still something missing: Product 's initialization data.

Recall that Product has an initialize method: when creating a new one, we need to make sure it is called with the right arguments.

// contracts/Product.sol
    ...
    function initialize(uint256 _value) public initializer {
        value = _value;
    }
    ...

OpenZeppelin Upgrades provides a JavaScript utility function just for this sort of thing: encodeCall. It receives a method name, an array of argument types and an array of argument values, and outputs the call data that corresponds to that method invocation.

Let’s generate the call data for an initialization with the number 42:

$ node
> const { encodeCall } = require('@openzeppelin/upgrades');
> encodeCall('initialize', ['uint256'], [42]);
'0xfe4b84df000000000000000000000000000000000000000000000000000000000000002a'

Creating the Instance contract

With the call data we just generated we’re finally ready to use Factory to create a new Product.

$ npx oz send-tx
? Pick a network development
? Pick an instance Factory at 0x9b1f7F645351AF3631a656421eD2e40f2802E6c0
? Select which function createInstance(_data: bytes)
? _data: bytes: 0xfe4b84df000000000000000000000000000000000000000000000000000000000000002a
✓ Transaction successful. Transaction hash: 0x803969c85bb93058ae7deecfaab53ba78b79161bde4fb168c174e949a8698e71
Events emitted:
 - InstanceCreated(0x3c63250aFA2470359482d98749f2d60D2971c818)

We have now created a new upgradeable Product contract from our Factory contract! Note that the data provided to createInstance is the one we generated using encodeCall.