How would you manage non-upgradeable contracts on ZeppelinOS?

An issue we have been long discussing is how ZeppelinOS should handle non-upgradeable deployments. Whenever you deploy a contract using zos create a new upgradeable proxy is created behind the scenes. If you want to make a contract non-upgradeable, your only options are to set the upgradeability admin of the contract to the null address (so no one can actually upgrade it), or use another tool altogether. None of these options are good.

We have identified three different options to implement this:

  1. Creating a non-upgradeable proxy (ie minimal proxy) to the logic contract. This is the less disruptive option, since we just work with different “flavors” of proxies. This also plays nicely with EVM packages, and supports the use case of facilitating the deployment of multiple copies of the same contract at reduced gas cost.

  2. Simply deploying the contract from the local artifacts. This is the most direct approach: instead of creating a logic contract and a proxy, we just deploy a contract, period. It allows users to keep working with constructors, and does not require trust in the proxy pattern.

  3. Duplicating the code of the logic contract into a new instance. Instead of creating a proxy, we actually copy the code of a logic contract into a new address. This is the most costly option, since it requires at least two deployments per contract (one of the logic one, which goes unused, and another of the actual instance). It’s main upside is that it is partly implemented already in #32.

I think the minimal proxies approach (1) is the one that best fits with our current solution. We require all contracts to be written with initializers (eventually we can automate this process), deploy them as logic contracts, and then set up different proxies to them (upgradeable or not). This makes the code as homogeneous as possible, while removing upgrades from the equation when needed. It also adds support for an interesting use case, which is cheap deployment of multiple copies of a contract.

However, plain deployments (2) have an advantage: they do not require “trust” in the proxy pattern. If a project wants non-upgradeable contracts because they don’t want to use an extraneous pattern in the core of their system, then proxies (minimal or not) are not an option. On the other hand, this may lead to confusion when working with zOS: some contracts could be written with constructors (the ones deployed using this pattern), while others would require initializers (the ones that use proxies), and they would all be mixed up on the same project.

What are your thoughts on this? Can you share use cases or reasons for having a core of non-upgradeable contracts in your project, and which option would better suit you?


I can see how someone could make the case for partial upgradeability: an underlying governance system may want to be left untouched, but details of how said system works could be upgradeable by calling into upgradeable contracts.

1 Like

Perhaps it makes sense if users want a “non upgradable” contract that also does not require “trust” in the proxy pattern, that there be a command like, “deploy boring contract” which just deploys a regular contract.

I like the idea of minimal proxies personally, when the discussion with developers inevitably turns to “governance” over upgradability, or wanting to deploy non-upgradable contracts. I need to explain that it can be “turned off” by setting the owner to ‘0x0’ but I don’t think that is a satisfying answer.

It might be worthwhile to consider that some projects might want to build projects that mix and match, upgradable, non-upgradable, etc… so maybe there is also the ability for users to specifically declare this:

zos create MyWallet -proxy-type upgradable
zos create MyWallet -proxy-type skinny
zos create MyWallet -proxy-type none
1 Like

That is exactly the info I want to gather :slight_smile:

If there are such projects that want to mix and match, what is the rationale behind that decision, and why are minimal proxies not good enough? Is it just a matter of trust, convenience, gas, security…?

1 Like

Off the top of my head, I would say probably just convention. People are on-boarded into solidity with the process of creating and deploying a contract in the ‘normal’ way. It’s easy to conceptualize and easy to test (I think plenty of people just pop the address into remix to test by hand).

There is a strange element of cognitive load that comes along with using proxies. There is something else, more code, somewhere… For some developers, I think this just feels like a strange inconvenience when they don’t need it. Also, given how difficult it can sometimes be to test solidity code, I think some portion of dev’s might come away from a cryptic revert wondering if it’s REALLY their code, or instead the proxy.

Also, some projects do want to use selfDestruct() in their code, but with proxies, I think it becomes a little bit unclear for developers what exactly might be the expected behavior and how to both use/misuse this feature as it could involve thinking “how do we protect the proxy versus, how do we protect the logic?”.


In thinking more about this, it might be nice to have a CLI command that “burns ownership” of the proxy. So transfers it to 0x000… something simple like that could be nice for developers who want to just quickly lock their contract.


I like option 2. deploy non-upgradeable
I want it to be easy, with plenty of messaging that I am deploying a non-upgradeable contract, that I can’t upgrade using ZeppelinOS.

That being said, for the types of projects I have worked on (ERC721 NFTs), other than simplicity and having to think about the governance of upgrades, why wouldn’t I deploy a ZeppelinOS upgradeable contract?


I think for something you might want to not have an upgradable proxy attached. For example if you do have a standard ERC721, or ERC20 as a part of your project, perhaps you want your users to know that this code can’t be changed, so that might be non-upgradable.