The most important, uncompromisable tenet of our migration pattern:
One bad release must never be able to render a
VaultProxy un-migratable to any future release.
This is achieved through a series of mitigations, chief among them:
Calls to migrate always come from the inbound
FundDeployer , rather than vice-versa.
Hooks that call down to the inbound and outbound
FundDeployer instances must be able to be bypassed in the case of failure.
In order to migrate a fund from a previous release to the current release:
FundDeployer.createMigratedFundConfig(). This deploys a
ComptrollerProxy and sets all release-level fund configuration, as described in "ComptrollerProxy Creation".
FundDeployer.signalMigration() with the addresses of the
VaultProxy that should be joined.
FundDeployer validates that CallerA was the creator of the
ComptrollerProxy and is a valid migrator for the
FundDeployer calls up to
Dispatcher.signalMigration() , which stores a
MigrationRequest with the passed values along with the
executableTimestamp (the timestamp at which the migration will be allowed to be executed, based on the
migrationTimelock value set on
Dispatcher at the time migration is signaled).
After the current block's timestamp is greater than or equal to the
executableTimestamp , CallerA can call
FundDeployer.executeMigration(), which calls the mirroring function on the
Dispatcher validates whether the
migrationTimelock has elapsed for the
MigrationRequest , and whether the calling
FundDeployer is still the
currentFundDeployer (migrations to stale releases are not allowed).
setAccessor() in order on the
VaultProxy, updating the target of the proxy and the
ComptrollerProxy.activate() to set the migrated
VaultProxy on the
ComptrollerProxy and to give extensions a final chance to update state before the the fund can start taking investments.
As stated in the pattern above, there is a
migrationTimelock, which defines the minimum time that must elapse between signaling and executing a migration. This gives investors the opportunity to opt-out of a fund if they do not agree to the upgrade, or to the new fund configuration.
Dispatcher invokes two types of hooks that call down to the outbound and inbound
FundDeployer instances during the migration process, giving them the chance to execute arbitrary code at the release-level:
invokeMigrationOutHook is called on the outbound
FundDeployer instance before and after each action in the migration pipeline: signal, migrate, and cancel (only post-cancellation)
invokeMigrationInCancelHook is called on the inbound
FundDeployer instance post-cancellation. This is necessary because while the inbound
FundDeployer is the caller in all other cases, if an approved migrator calls
FundDeployer.cancelMigration() directly, the inbound
FundDeployer should be given the opportunity to react.
These hooks are not guaranteed to succeed, but - as stated above - they must never block a migration.
This is why each migration function has a
bool _bypassFailure param on the
Dispatcher, which is set to
xxxEmergency versions of each function on the
FundDeployer , e.g.,