Upgradeable contract naming (versioning)

Trey asked on Telegram:

So I guess I’m just struggling with what the “intended” workflow is here for making contract updates. Say I’ve got a project with:

CustomERC20 is IERC20
PausableCustomERC20 is CustomERC20
MyToken is PausableCustomERC20

Given what has been recommended, if I want to deploy a new version of MyToken, I should make a MyTokenV2 contract that either inherits from MyToken or is just copy of it with a compatible storage structure.

But what if I make a change to CustomERC20 or PausableCustomERC20? If I truly want the migrations to be reproducible, then it seems like going the route of renaming to “v2” is going to result in me having the rename every class in the inheritance chain to V2. That would logically extend to the OpenZeppelin contracts themselves if I upgrade from one version to another, but you guys don’t update the contract names like this when you make updates, so I don’t really have that option short of forking the Openzeppelin code.

So for example “V1” could include contracts/v1/MyToken, contracts/v1/pausableCustomERC20, contracts/v2/CustomERC20.

Then for V2, that made a change to PausableCustomERC20, I would need to create contracts/v2/MyToken.sol and contracts/v2/PausableCustomERC20.sol, and then contracts/v2/PausableCustomERC20.sol could inherit from either from a new contracts/v2/customERC20.sol or still just from contracts/v1/CustomERC20.sol (since the file didn’t change).

If, as suggested, I need to also publicly change my contract name to be clear/transparent that the contract has changed, then I’ll need to also update the code at each level of the hierarchy to refer to the classes as MyTokenV2, PausableCustomERC20V2, etc. instead of just referencing them with the same class names and relative folder structure as before (i.e. if I just create a parallel path under contracts/v1, contracts/v2, etc. so all the code importing other contracts with relative paths doesn’t have to be modified)

As a parallel, when OpenZeppelin releases a new version of it’s contracts, it versions the release, but leaves the contract names the same between versioned releases, which is pretty standard practice in the software world.

I’m willing to go a different route, but the dependency hierarchy of “just make a MyTokenV2” doesn’t seem to me like it works all that great either, as you essentially end up having to fork every contract in the hierarchy every time you make any change to that contract or any parent class, and constantly updating all the code to add “v2”, “v3”, etc. to referenced classes seems strange.

I feel like I must be overthinking this, but I’m just trying to wrap my head around how people make this work in practice. The “create a release commit with any changes and deploy that commit with consistent contract names” approach feels clean, but requires that every release be run sequentially through the release commits to recreate current state.

The “create a full copy of the hierarchy in the main branch the for every release” also seems like it would work pretty well, so long as we can keep the contract names the same and don’t have to then slap a “v2” at the end of every class name.

If I’m going to increment the contract names to append “v2”, “v3”, etc., It feels like it is going to get messy forking every class name and modifying inheritance hierarchies for every change.

Any pointers on how other people managing this process?

Hi Trey,

My thoughts (when talking about an ERC20) are that you would keep all your custom logic inside MyTokenVn and extend from OpenZeppelin Contracts.

You could have custom contracts that you extend from, but depending on the amount of code (and your architecture), it may be easier to keep them all in the one contract.

contract MyTokenV1 is Initializable, ContextUpgradeSafe, AccessControlUpgradeSafe, ERC20BurnableUpgradeSafe, ERC20PausableUpgradeSafe {
   uint256[50] private __gap;
}

Upgrade for new version of MyToken

For a functionality change or a bug you would:

  • Bump the version of MyToken to MyTokenVn
  • Add functionality
  • Unit test MyTokenVn
  • Locally test the upgrade from MyTokenVn-1 to MyTokenVn
  • Deploy MyTokenVn on a testnet
  • Upgrade the proxy to use MyTokenVn on a testnet and test
  • Deploy MyTokenVn on a fork of mainnet
  • Upgrade the proxy to use MyTokenVn on a fork of mainnet and test
  • Deploy MyTokenVn on mainnet
  • Upgrade the proxy to use MyTokenVn on mainnet and check

Upgrade for new version of OpenZeppelin Contracts (upgrade safe)

My thoughts are that you are may be less likely to change the version of OpenZeppelin Contracts that you are using in subsequent upgrades.

This would only be for a feature that you really want to take advantage of. (e.g. a permit option for meta transactions) or in the (ideally rare) case of a bug in the OpenZeppelin Contracts you are extending.

If you did do this, you would:

  • Bump the version of MyToken to MyTokenVn
  • Install the new upgrade safe version of OpenZeppelin Contracts in your project
  • Unit test MyTokenVn
  • Locally test the upgrade from MyTokenVn-1 to MyTokenVn
  • Deploy MyTokenVn on a testnet
  • Upgrade the proxy to use MyTokenVn on a testnet and test
  • Deploy MyTokenVn on a fork of mainnet
  • Upgrade the proxy to use MyTokenVn on a fork of mainnet and test
  • Deploy MyTokenVn on mainnet
  • Upgrade the proxy to use MyTokenVn on mainnet and check

I will be interested to hear more thoughts on this from the community.

This is a really interesting topic!

I personally don't have a strong opinion on whether projects should version the contracts (on top of the underlyling version control system such as git). So if in your particular use case you prefer to progressively change the same contract, that's fine, and the Upgrades Plugins should support that just as well!

It's true that following the versioning approach gives a kind of reproducibility that is interesting and valuable. If this is important to you, then maybe it would be best to program with less deep contract hierarchies so you don't have to reversion so many contracts every time.

Regarding versioning of OpenZeppelin Contracts itself, if you wanted to be as strict with versioning about it, you would have to pin the dependency. If you want to upgrade to a new version, you can use npm aliases to install two versions side by side. See npm-install where it says

npm install <alias>@npm:<name> : Install a package under a custom alias.

This allows you to have multiple versions installed:

npm install oz-contracts-3.1@npm:@openzeppelin/contracts@3.1.0
npm install oz-contracts-3.2@npm:@openzeppelin/contracts@3.2.0
import "oz-contracts-3.1/token/...";

I have never done this or seen anyone do it, but I think it can work. You have to be careful not to end up with multiple versions of an OZ contract in the same Solidity file because you will run into name clashes. However, if you follow the strict versioning approach I think you'd be unlike to run into that.

1 Like