We’ve decided to release a new major version of the SDK with significant improvements. This is a living document of the changes we’re planning. Most of it has come out of conversations with others in the team, but a few things are my own ideas that haven’t been discussed yet. This is a work in progress so please share your thoughts!

Goals

I believe the main goal of 3.0 should be to iron out the quirks that have accumulated since the initial prototype of ZeppelinOS.

Being a major version we will feel free to make breaking changes. However, it is also a goal that there should be a migration path for current 2.x users. We want everyone to benefit from the improved experience.

Automatic Initializers

So far users have had to write their upgradeable contracts in a special way with initializer functions. The conversion from constructors to initializers is completely mechanical, though, so we want to automate it for the user. We will do this behind the new deploy command.

The special thing about deploy is its --kind option, which can have different values: regular , upgradeable , minimal , and potentially more in the future, such as deterministic (using create2 ).

Transpiler

The conversion to initializers is done by a transpiler that will process Solidity files and generate other Solidity files. The transpiled contracts are renamed to …Upgradeable , for example Foo results in FooUpgradeable .

These files are placed next to the user’s contracts in the directory contracts/__upgradeable__ . The reasoning behind placing them there is that it will interoperate better with other tooling that expects contracts to be under the contracts directory.

It will be possible to disable the transpiler for a project, and users migrating from 2.x might want to do this since their contracts will already be converted to initializers.

Removing Aliases

Since day 1 the CLI has had the concept of aliases. The idea is that you can assign an alternative name to a contract and its instances. We don’t expect a lot of people to know about this feature because it’s nowadays hidden in the oz create workflow, and contracts are almost always referred to by their name in the source code.

We want to fully remove aliases now, since we don’t think they really contribute to a good experience, and they add a lot of complexity internally. We do recognize that aliases as a building block can be used for many purposes, but in my opinion they are probably never the right abstraction. Here are some examples and alternative abstractions, that we may implement in the future.

Example Use Case #1

Aliases can be used when keeping multiple versions of the same contract side by side. For example, you may have FooV1 and FooV2 among your contracts in order to test the correctness of an upgrade. Initially you may have aliased FooV1 as Foo , but once you’re confident in the upgrade you can change the alias Foo to refer to FooV2 , and run oz upgrade Foo .

A better abstraction for this use case would be the concept of snapshots. We could provide a command to store a named snapshot of a contract source: oz snapshot Foo v1 . Later this snapshot could be used wherever we need to refer to a contract implementation, for example oz deploy Foo@v1 .

In the meantime, we will add as an alternative a command that can be used to change a proxy’s implementation to another contract, such as oz upgrade-to FooV2 .

Example Use Case #2

You can alias ERC20 as MyERC20 and deploy an upgradeable instance of MyERC20 . If you later change the source for ERC20 and run oz upgrade MyERC20 , this will upgrade only the MyERC20 instance.

This doesn’t work so well for other kinds of commands. For example, it would not be reasonable to run oz call --to <alias> --method totalSupply , because the alias potentially refers to multiple contracts, and it’s unlikely that the user intends to call a function on multiple contracts.

A better abstraction for this use case would be assigning names to instances. Similarly, we’ve seen the need to assign names to arbitrary addresses. Both of these things may be served by the same feature.

Removing App/Package/ImplementationDirectory

These are the contracts that support on-chain Ethereum Packages (initially called EVM Packages). Package and ImplementationDirectory were created as a way to store an on-chain versioned registry of contract implementations for a package, and App as a way to connect to multiple packages and act as a factory of proxies for those packages’ implementations.

Nowadays, they are mostly unused because of the overhead they add. The CLI normally works in a mode where all of that information is simply stored locally in JSON files, and proxies are created directly rather than through the App as a factory. The local JSON files work well or even better, so we want to remove support for these contracts, which contribute a lot of code and complexity internally.

There is one use case for which the contracts do serve a purpose, though, which is dynamically creating proxies on-chain. Before removing them, we need an alternative offering for this use case. We considered many options, a lot of them involving using the transpiler to convert a Solidity new statement automatically into a proxy creation. One central issue that kept coming up was how to decide who should be the admin of the newly created proxy. Ultimately, we decided that such an automatic conversion is too magic and hides important detail. We came to the conclusion that if a contract creates other proxies, then proxies are part of its domain, and therefore it is better to make it an explicit concern.

As an alternative to App , we will give users the tools to create their own proxy factories, in a Solidity library of proxy factories, and to hook in to the transpiler to get hold of the upgradeable source code for the contract they want to deploy dynamically.

// This import lets the CLI know that it needs to have Foo.sol transpiled // and up to date. It puts FooUpgradeable in scope. import "./__upgradeable__/Foo.sol"; contract MyFactory { using Proxies for Proxies.ProxyFactory; // This struct contains the address of the implementation that is // used for the proxies it creates. struct Proxies.ProxyFactory factory; constructor() public { // Initializes the factory struct by deploying the implementation // and storing its address. factory = Proxies.newFactory(FooUpgradeable.creationCode); } // initializerData is the encoded call to the initializer function. function deployInstance(address admin, bytes initializerData) public { factory.deploy(admin, initializerData); } }

Argument Conventions

Commands that receive arguments for functions (including initializers) currently do so through an option --args whose value is the arguments separated by commas. For example:

oz create --args 1,2,a_string Foo oz call --to <address> --method bar --args 1,2,a_string

This feels out of place in a shell interface where arguments are normally separated by spaces and the shell does the splitting into words. The new deploy command follows this convention:

oz deploy Foo 1 2 a_string

We should strive for a similar interface for all commands, where the primary arguments are specified as positional arguments. oz call , for example, should look like:

oz call <address> bar 1 2 a_string

Implicit Initialization

Users of the CLI currently have to initialize a project with oz init before doing anything (except compiling). This step basically doesn’t serve any purpose for the user. They have to provide a name and a version which are not really used for anything (other than the Package contract mentioned above that we intend to remove). Additionally, they can specify options that will be stored in the config, but we really shouldn’t expect users to know at this stage what options they will want for their project.

Internally the command is necessary because it initializes the .openzeppelin directory. This can and should be done implicitly when needed.