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:
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.
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.
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?