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
.