Fund Migration
Philosophy
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.
Step-by-step
In order to migrate a fund from a previous release to the current release:
CallerA calls
FundDeployer.createMigratedFundConfig()
. This deploys aComptrollerProxy
and sets all release-level fund configuration, as described in "ComptrollerProxy Creation".CallerA calls
FundDeployer.signalMigration()
with the addresses of theComptrollerProxy
andVaultProxy
that should be joined.The
FundDeployer
validates that CallerA was the creator of theComptrollerProxy
and is a valid migrator for theVaultProxy
.The
FundDeployer
calls up toDispatcher.signalMigration()
, which stores aMigrationRequest
with the passed values along with theexecutableTimestamp
(the timestamp at which the migration will be allowed to be executed, based on themigrationTimelock
value set onDispatcher
at the time migration is signaled).After the current block's timestamp is greater than or equal to the
executableTimestamp
, CallerA can callFundDeployer.executeMigration()
, which calls the mirroring function on theDispatcher
.The
Dispatcher
validates whether themigrationTimelock
has elapsed for theMigrationRequest
, and whether the callingFundDeployer
is still thecurrentFundDeployer
(migrations to stale releases are not allowed).The
Dispatcher
callssetVaultLib()
andsetAccessor()
in order on theVaultProxy
, updating the target of the proxy and theaccessor
role.The
FundDeployer
callsComptrollerProxy.activate()
to set the migratedVaultProxy
on theComptrollerProxy
and to give extensions a final chance to update state before the the fund can start taking investments.
Migration Timelock
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.
Hooks and "emergency" functions
The 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 outboundFundDeployer
instance before and after each action in the migration pipeline: signal, migrate, and cancel (only post-cancellation)invokeMigrationInCancelHook
is called on the inboundFundDeployer
instance post-cancellation. This is necessary because while the inboundFundDeployer
is the caller in all other cases, if an approved migrator callsFundDeployer.cancelMigration()
directly, the inboundFundDeployer
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 true
via xxxEmergency
versions of each function on the FundDeployer
, e.g., signalMigrationEmergency()
Last updated