Together with @jcarpanelli we are starting work on a new feature to overcome some of the usability challenges that come up around the usage of transparent proxies, and we wanted to open the discussion on how this new feature would fit within the ZeppelinOS client and libraries.
https://github.com/zeppelinos/zos/issues/419
The story so far
But first, a quick reminder on the two different modes we have for running a ZeppelinOS project.
Current default mode
By default, the ZeppelinOS CLI keeps track of all the implementation contracts that are deployed when running zos push
in per-network json files. This way, when you create a proxy, the CLI just looks up the address for the implementation on the json file, and creates a proxy that is backed by it. Upgrades are issued directly to the proxy by the sender account.
zos add MyContract
// Adds the contract to the project
zos push --network mainnet
// Deploys MyContract to mainnet
// Updates zos.mainnet.json with its address
zos create MyContract --network mainnet
// Looks up the address of MyContract in zos.mainnet.json
// Creates a new proxy backed by that address
// Saves the address of the new proxy on zos.mainnet.json
In this mode, the proxies are created and owned by the sender account, which is problematic when the same accounts wants to interact with the created instance.
MyContract.deployed().myMethod({ from: accounts[0] })
// Fails since the default account is the owner of the proxy
Published mode
Alternatively, a ZeppelinOS project can be published via the zos publish
command. This deploys a set of contracts (App, Package, and Directory) that keep the structure of your project on-chain. The Directory keeps track by name of all the implementation contracts deployed on the current version, the Package keeps one Directory per version, and the App keeps track of all the Packages used (you'll have more than one if you link
ed an EVM package as a dependency).
App 1--n Package 1--n Directory 1--n Implementation
Publishing your project is only mandatory if you want to share it as an EVM package. Otherwise, ZeppelinOS works simply on the default mode.
In this mode, the App contract not only keeps track of the Packages that comprise the project, but also acts as the owner of all proxies created. It provides methods to create
a new proxy and upgrade
an existing one.
zos add MyContract
// Adds the contract to the project
zos push --network mainnet --publish
// Creates App, Package, and Directory contracts
// Deploys MyContract to mainnet
// Registers MyContract in Directory
// Updates zos.mainnet.json with all deployed addresses
zos create MyContract --network mainnet
// Requests the App contract to create a new "MyContract"
// Saves the address of the new proxy on zos.mainnet.json
Note that, since the App is the admin of the proxies, the user's default account can safely interact with the created proxies. Whenever the user needs to manage the proxies, they do it via the App.
zos update MyContract --network mainnet
// Looks up the address of MyContract proxy in zos.mainnet.json
// Sends a request to App to update that address to the latest "MyContract"
Summing up
- By default, the user's account is the admin of all proxies created, and manages them directly.
- When published, the App contract is the admin of the proxies, and the user's account is the owner of the App.
SimpleProject and AppProject
As a side note, both these modes are accessible via the programmatic interface as well. The default mode can be replicated in javascript using the SimpleProject
class.
const project = new SimpleProject('MyProject', { from: accounts[0] });
// Setup new project
await project.setImplementation(MyContract);
// Deploy an instance of MyContract implementation contract and track its address
const proxy = await project.createProxy(MyContract);
// Creates a new proxy backed by the deployed MyContract instance
// The proxy is owned by the sender address, account[0]
While the published mode is handled by the AppProject
class:
const project = AppProject.deploy('MyProject', '0.1', { from: accounts[0] });
// Deploys new App, Package, and Provider contracts
await project.setImplementation(MyContract);
// Deploy an instance of MyContract implementation contract
// Registers it in the Provider contract
const proxy = await project.createProxy(MyContract);
// Sends a tx to App.create('MyContract'), to create a new proxy
// The proxy is owned by the App contract
The new ProxyAdmin
The idea of the new feature we are working on is to have a small contract ProxyAdmin
that takes over the responsibility of owning proxies from the user's account and from the App contract. This contract would only have a method to upgrade
a proxy given its address, and would be owned by the user's account.
This way, the user goes through the ProxyAdmin contract for managing their proxies, instead of interacting directly with them. This allows the user account to interact directly with the proxies, since it is not its admin.
Question: Do we like the name for the ProxyAdmin?
Default and published modes revisited
This new component would now be included on both the default and the published mode:
- In default mode, the user account no longer is the admin of the proxies. The user account is now the owner of the ProxyAdmin, who in turn is the admin of the proxies
- In published mode, the App no longer is the admin of the proxies. The user account is now the owner of the ProxyAdmin, who in turn is the admin of the proxies. This allows us to remove the upgrade methods from the App.
This means that there is no way from the CLI to manage the proxies directly, since a ProxyAdmin will always be deployed.
Question: are we fine with this? Do we want to keep a non-default super-lightweight mode in which the users can still manage the proxies directly, and deal themselves with using different accounts for proxy management and interaction? I'd say no, to avoid cluttering the CLI with too many different modes.
ProxyAdminProject
For low-level programmatic usage, the SimpleProject would keep behaving exactly the same way as before. The AppProject, on the other hand, would incorporate the new ProxyAdmin for being the admin the proxies, instead of having the App contract in that role.
We would also introduce a new ProxyAdminProject
that has the same behaviour as the new CLI default mode: it keeps track of all implementation contracts off-chain, but delegates the admin management of the proxies to a ProxyAdmin instance.
This would allow users of the programmatic interface to manage proxies themselves via the SimpleProject, and also to be able to achieve the same behaviour as in the CLI via the new ProxyAdminProject. The rationale for this decision is that users that rely on the programmatic interface have more knowledge of the system, and can easier manage the quirks of using two different accounts for interacting with their proxies.
Question: Are we fine with keeping 3 different projects, even if one is not used from the CLI? Do we like the new name for the new project?
Migrations
The new ProxyAdmin implies a breaking change with respect to the old architecture, both for default and published mode. This means that we'd need to migrate the old projects to the new format, which could be easily detected via the zosversion
entry on the json files.
The migration would entail the same for both kind of projects: deploying a new ProxyAdmin, and transferring the administration of them from the user account or the App (depending on the project kind) to it.
Instead of adding a new command for the migration, whenever the user attempts to perform a network-related operation (such as create or upgrade), he's notified by the CLI that the structure is incompatible and needs to migrate. If the user accepts, the migration proceeds; if not, the command execution is aborted.
$ zos create MyContract --network mainnet
> An old version of the ZeppelinOS project is detected on mainnet, and needs to be migrated to the latest one.
> Do you want to proceed? [y/n/h]
// if yes
> Migrating...
> Deploying new ProxyAdmin
> Transferring ownership of 0x123 to proxy admin at 0x456
> Transferring ownership of 0x124 to proxy admin at 0x456
> Migration successful
> Proceeding with original command
> Creating instance of MyContract
> Instance of MyContract created at 0x125
// if no
> Migration cancelled. Aborting.
// if help
> The new version uses a thin ProxyAdmin contract to manage all upgradeable contract instances. This avoids issues with using the same account for managing the upgradeable contracts and interacting with them. All proxies currently managed by your account 0x1234 [or your App 0x12345] will now be managed by the ProxyAdmin, which will be owned only by you.
> Do you want to proceed? [y/n/h]
Question: Are we fine with forcing users to migrate? An alternative would be to maintain support for the old modes, but this means much more complexity to maintain in the project. As for the method for enforcing the migration, are we fine with it?
Wrapping up
The restriction to avoid using the same account for managing a proxy and interacting with it has been the source of much confusion so far. The ProxyAdmin acts as a very small forwarding agent to prevent this issue, by taking over the responsibility of being the admin in all scenarios. We'd appreciate your feedback on this new feature!