Contract Upgrades

Migratability refers to the process by which a core contract can be upgraded to a different version with more functionality. Migrations are completed via the MezzMigrator.

Mezz Migrator

The Mezz Migrator is the contract responsible for upgrading core contracts to new or patched versions. For an implementation to be set in the Mezz Migrator, it must inherit from the Credentialed base contract, which requires inheritors to implement a core identifier and version, which should return a bytes32 and uint256, respectively. Upgradeable core contracts also inherit from MezzUUPSUpgradeable, which overrides UUPSUpgradeable's _authorizeUpgrade internal function such that only the Mezz Migrator can call upgradeToAndCall, which atomically upgrades a proxy's implementation and executes an initialization function via a delegate call. Contracts that inherit from UUPSUpgradeable are deployed via ERC1967 proxies.

Core Identifiers and Versioning

Each core identifier in Mezzanine is associated with a set of implementations in the Mezz Migrator, which are versioned from one. For example, the Treasury will have a version of 1 when the Mezzanine protocol is first deployed.

Versions in Mezzanine follow the X.Y format, similar to that found in the Semantic Versioning standards. X is incremented when the version is not backward compatible with prior versions. Y is incremented when a new implementation is set in the Mezz Migrator. Therefore, the version function in MezzUUPSUpgradeable contracts refers to Y, while X is defined in the calculation of the core identifier.

Core identifiers are calculated for each contract as follows:

bytes32 internal constant CORE_ID = keccak256(abi.encodePacked("mezzanine.coreId.CONTRACT_NAME.vX"))

where CONTRACT_NAME is the name of the contract and X corresponds to X.Y in the semantic versioning standards.

The return value of a contract's version will be off by a factor of one from the semantic versioning standards since versions are indexed at one. For example, a treasury implementation of version 2 and a core identifier of the hash of "mezzanine.coreId.Treasury.v1" would correspond to V1.1.

The Mezz Migrator is responsible for versioning contracts even when they are not upgradeable. For example, the Common Shares and Preferred Shares contracts are non-upgradeable. They are deployed via the Mezz Deployer. The latest versions for these contracts will always be queried from the Mezz Migrator and subsequently deployed. Older versions will never be used.

The latest version of an implementation can be patched if the core contract is upgradeable. Specifically, the latest version of a core identifier would be updated to a new implementation. The protocol or the exploitable implementation can then be paused or frozen to reset to the contract to the patched implementation.

Upgrading Core Contracts

MezzUUPSUpgradeable contracts store the address of the MezzMigrator as an immutable variable that is set in their constructor. Behavior from UUPSUpgradable has been overridden such that the MezzMigrator, itself, must be the caller to upgrade a contract. A core contract can upgrade itself by calling the MezzMigrator's upgradeToNewerVersion and resetToPatchedLatestVersion functions. Calling these functions will perform a callback on the caller's upgradeToAndCall function. Arbitrary calldata can be provided if needed to call a re-init function, which acts similarly to a constructor for the new version. The methods by which core contracts call these functions are bespoke.

In the event of a route for an exploit, backdoors to key core contracts have been implemented such that they can be upgraded to newer versions by the owner of the Mezz Hub, even if a company has not requested an upgrade. This behavior can be decremented in future versions. However, it was implemented this way as a means to allow the Mezz Team to quickly patch potential routes for exploits for inactive companies.

Steps for Patching an Exploitable Version of a Core Contract

The Mezzanine may push a version with a potential route for an exploit. To patch exploitable contracts, the Mezzanine team should take the following steps:

  1. Freeze the protocol

  2. If undiscovered, identify where and how the exploit is taking place. Otherwise, skip to step 3

  3. Freeze the relevant implementations and/or pause the protocol

  4. Reset the latest version’s implementation to a patched version

  5. Notify users to upgrade their core implementation contracts to the patched version or have the Mezz team patch on behalf of users

  6. Once all core implementations have been patched, change the protocol state back to active

Minor bugs can be patched following a much more lenient process:

  1. Set a new version of the core contract with the patched implementation

  2. Request that users upgrade to this newest version

Risk Management

Core contracts can only be upgraded via the Mezz Migrator when a callback is performed on the core contract's upgradeToAndCall function. upgradeToAndCall makes an internal call to _authorizeUpgrade, which can be overridden to add access control. MezzUUPSUpgradeable contracts override this function such that the Mezz Migrator must be the caller when upgrading contracts.

upgradeToAndCall atomically upgrades a proxy's implementation and executes an arbitrary delegate call onto the upgrading contract's implementation. Calldata is validated such that it cannot be another call to upgradeToAndCall, forgoing Mezzanine's strict version control. This arbitrary call is meant to be towards a contract's init or re-init function in case the new implementation needs to initialize or set state to function properly. Doing so improperly may lead to a potential route for exploitation in the upgraded contract. The provision and validation of calldata provided during an upgrade should be provided on Mezzanine's interface, and incorrect or the lack of calldata should be flagged. Mezzanine's smart contract developers should also design any re-initialization functions to minimize any route for exploitation.

Last updated