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 out contract factory.

// contracts/Product.sol
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
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
✓ Added contract Product
$ npx oz push
✓ Contract Product deployed
All contracts have been deployed
$ npx oz publish
✓ Project structure deployed
✓ Registering Product at 0x3064A4B40fcf4E68bCc5E2EBF44421A325C78b00 in directory
✓ Published to dev-1578608628898!

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-1578608628898.json
  ...
  "app": {
    "address": "0x61F31Be60e7EA755cc034cD38FdE02bA4e3ecd47"
  },
  ...

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

$ npx oz create
? Pick a contract to instantiate Factory
✓ Added contract Factory
✓ Contract Factory deployed
All contracts have been deployed
? Call a function to initialize the instance after creating it? Yes
? Select which function * initialize(_appContractAddress: address)
? _appContractAddress (address): 0x61F31Be60e7EA755cc034cD38FdE02bA4e3ecd47
✓ Setting everything up to create contract instances
✓ creating-from-solidity Factory instance created at 0xA7b8Fb47F9114278ece7d2d8161533d06B9203a4
0xA7b8Fb47F9114278ece7d2d8161533d06B9203a4

Encoding Call Data

Our Factory is ready for its createInstace 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 an instance Factory at 0xA7b8Fb47F9114278ece7d2d8161533d06B9203a4
? Select which function createInstance(_data: bytes)
? _data (bytes): 0xfe4b84df000000000000000000000000000000000000000000000000000000000000002a
✓ Transaction successful. Transaction hash: 0xc39b59dc10e1c68c681648d30d042b2b8c8a912839912533a349628c299ec619
Events emitted:
 - InstanceCreated(0x37838554CEb544A849cD4e5867AD0a9F7d4fB779)

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.