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:

  1. CallerA calls FundDeployer.createMigratedFundConfig(). This deploys a ComptrollerProxy and sets all release-level fund configuration, as described in "ComptrollerProxy Creation".

  2. CallerA calls FundDeployer.signalMigration() with the addresses of the ComptrollerProxy and VaultProxy that should be joined.

  3. The FundDeployer validates that CallerA was the creator of the ComptrollerProxy and is a valid migrator for the VaultProxy .

  4. 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).

  5. 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 .

  6. 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).

  7. The Dispatcher calls setVaultLib() and setAccessor() in order on the VaultProxy, updating the target of the proxy and the accessor role.

  8. The FundDeployer calls 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.

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 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 true via xxxEmergency versions of each function on the FundDeployer , e.g., signalMigrationEmergency()

Last updated