One of the biggest goals for Enzyme v2 is to have upgradable funds. Finding an appropriate pattern was a challenging task because:
We have different types of contracts that comprise and are shared by funds, e.g., fund libraries, plugins (fees, policies, and DeFi adapters), infrastructure that can persist between releases, etc,
It is important to our philosophy for both fund managers and investors to have the opportunity to opt-in or opt-out of every update (i.e., we can't change how a PerformanceFee works for every fund that is already using it)
The pattern that we settled on is a user-initiated partial migration (essential state only) from an old release to the current release.
The essential state for a fund is:
its holdings (i.e., token balances)
its accounting of investor shares in the fund
access control for ownership and state-changing interactions
This migration pattern is accomplished through two persistent contracts: a
Dispatcher and per-fund
The "essential state" described above lives in per-fund
VaultProxy contract instances, which are upgradable contracts following the EIP-1822 proxy pattern.
VaultProxy specifies a
VaultLib as its target logic, and these logic contracts are deployed per release, and swapped out during a migration.
VaultLibBaseCore contract defines the absolutely essential state and logic for every VaultProxy. This includes:
a standard ERC20 implementation called
the functions required of a
IProxiableVault called by the
core access control roles:
owner is the fund's owner.
migrator is an optional role to allow a non-owner to migrate the fund
creator is the
Dispatcher contract, and only this role is allowed to update the
accessor is the primary account that can make state-changing calls to the
VaultProxy . In practice, this is the release-level contract that interacts with a vault's assets, updates shares balances, etc.
This extremely abstract interface - in which a
VaultProxy needs no knowledge about a release other than which caller can write state - allows for nearly limitless possibilities for release-level architecture.
VaultLibBaseCore contract can be extended with new storage and events by implementing a new
VaultLibBaseN that extends the previous base. For example, this first release implements
abstract contract VaultLibBase1 is VaultLibBaseCore . The next release that adds to state or events would continue this pattern with
abstract contract VaultLibBase2 is VaultLibBase1 , and so on.
The release itself will provide a
VaultLib that extends the latest
VaultLibBaseN implementation with the required logic of that release.
An overarching, non-upgradable
Dispatcher contract is charged with:
deploying new instances of
VaultProxy from an old release to the current release
maintaining global state such as the current release, the global owner (i.e., the Enzyme Council) and the default
symbol value for fund shares tokens
Dispatcher stores the
currentFundDeployer (a generic reference to the latest release's contract that is responsible for deploying and migrating funds), and only a
msg.sender with that value is allowed to call functions to deploy or migrate a
FundDeployer can optionally implement migration hooks provided by
IMigrationHookHandler, which give the release an opportunity to run arbitrary logic as the
Dispatcher invokes those hooks at every step of the migration process.
As was the case described above with the
VaultLibBaseCore , this abstracted notion of a
FundDeployer - in which the
Dispatcher only cares about its identity for access and for optional callbacks - is totally unrestrictive to the shape of the release-level protocol.
Release-level contracts, then, are mostly arbitrary from the standpoint of these persistent contracts, offering maximum flexibility for future iterations and changes.