diff --git a/src/contracts/core/ReleaseManager.sol b/src/contracts/core/ReleaseManager.sol new file mode 100644 index 000000000..87a8c60fb --- /dev/null +++ b/src/contracts/core/ReleaseManager.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "../mixins/PermissionControllerMixin.sol"; +import "../mixins/SemVerMixin.sol"; +import "./ReleaseManagerStorage.sol"; + +contract ReleaseManager is Initializable, ReleaseManagerStorage, PermissionControllerMixin, SemVerMixin { + using EnumerableSet for EnumerableSet.Bytes32Set; + using OperatorSetLib for OperatorSet; + using Strings for uint16; + + /** + * + * INITIALIZING FUNCTIONS + * + */ + constructor( + IAllocationManager _allocationManager, + IPermissionController _permissionController, + string memory _version + ) + ReleaseManagerStorage(_allocationManager) + PermissionControllerMixin(_permissionController) + SemVerMixin(_version) + { + _disableInitializers(); + } + + /** + * + * EXTERNAL FUNCTIONS + * + */ + + /// @inheritdoc IReleaseManager + function publishRelease( + OperatorSet calldata operatorSet, + Artifact[] calldata artifacts, + uint32 upgradeWindow, + ReleaseType releaseType + ) external checkCanCall(operatorSet.avs) returns (Version memory) { + Version memory version = getLatestVersion(operatorSet); + + uint32 upgradeByTime = uint32(block.timestamp + upgradeWindow); + + if (releaseType == ReleaseType.MAJOR) { + ++version.major; + version.minor = 0; + version.patch = 0; + _upgradeByTimes[operatorSet.key()][version.major] = upgradeByTime; + } else if (releaseType == ReleaseType.MINOR) { + ++version.minor; + version.patch = 0; + } else if (releaseType == ReleaseType.PATCH) { + ++version.patch; + } + + bytes32 versionKey = _encodeVersion(version); + + // Append the version to the operator set's version history. + _versions[operatorSet.key()].add(versionKey); + // Map the version to the release artifacts and deprecation time. + Release storage release = _releases[operatorSet.key()][versionKey]; + for (uint256 i = 0; i < artifacts.length; i++) { + release.artifacts.push(artifacts[i]); + } + + emit ReleasePublished(operatorSet.key(), version, releaseType, upgradeByTime, artifacts); + + return version; + } + + /// @inheritdoc IReleaseManager + function deprecateRelease( + OperatorSet calldata operatorSet, + Version calldata version, + uint32 deprecationDelay + ) public checkCanCall(operatorSet.avs) { + bytes32 versionKey = _encodeVersion(version); + + // Check that the release is not already deprecated and modify state. + Release storage release = _releases[operatorSet.key()][versionKey]; + require(release.deprecateAtTime == 0, ReleaseAlreadyDeprecated()); + uint32 deprecateAtTime = uint32(block.timestamp + deprecationDelay); + release.deprecateAtTime = deprecateAtTime; + + // Checked that there is at least one stable version after deprecating this release. + Version memory lastStableVersion = getLatestStableVersion(operatorSet); + require(lastStableVersion.major != type(uint16).max, MustHaveAtLeastOneStableVersion()); + + emit ReleaseDeprecated(operatorSet.key(), version, deprecateAtTime); + } + + /// @inheritdoc IReleaseManager + function extendUpgradeWindow( + OperatorSet calldata operatorSet, + Version calldata version, + uint32 upgradeWindow + ) public checkCanCall(operatorSet.avs) { + _upgradeByTimes[operatorSet.key()][version.major] = uint32(block.timestamp + upgradeWindow); + + emit UpgradeWindowExtended(operatorSet.key(), version, upgradeWindow); + } + + /** + * + * INTERNAL FUNCTIONS + * + */ + /// @notice Encodes a Version struct into a bytes32 value for storage. + /// @dev Needed to store versions in an EnumerableSet. + /// @dev Packs the major, minor, and patch versions into a single bytes32 value. + /// The encoding format is: [major(16 bits)][minor(16 bits)][patch(16 bits)][padding(224 bits)] + function _encodeVersion( + Version memory version + ) internal pure returns (bytes32) { + return bytes32(abi.encodePacked(version.major, version.minor, version.patch)); + } + + /// @notice Decodes a bytes32 value back into a Version struct. + /// @dev Unpacks the major, minor, and patch versions from the encoded bytes32 value. + /// The decoding format is: [major(16 bits)][minor(16 bits)][patch(16 bits)][padding(224 bits)] + function _decodeVersion( + bytes32 encoded + ) internal pure returns (Version memory) { + return Version({ + major: uint16((uint256(encoded) >> 208) & type(uint16).max), + minor: uint16((uint256(encoded) >> 192) & type(uint16).max), + patch: uint16((uint256(encoded) >> 176) & type(uint16).max) + }); + } + + /** + * + * VIEW FUNCTIONS + * + */ + + /// @inheritdoc IReleaseManager + function getTotalVersions( + OperatorSet memory operatorSet + ) external view returns (uint256) { + return _versions[operatorSet.key()].length(); + } + + /// @inheritdoc IReleaseManager + function getVersion(OperatorSet memory operatorSet, uint256 index) external view returns (Version memory) { + return _decodeVersion(_versions[operatorSet.key()].at(index)); + } + + /// @inheritdoc IReleaseManager + function getRelease( + OperatorSet memory operatorSet, + Version memory version + ) external view returns (Release memory) { + return _releases[operatorSet.key()][_encodeVersion(version)]; + } + + /// @inheritdoc IReleaseManager + function getReleaseStatus( + OperatorSet memory operatorSet, + Version memory version + ) external view returns (ReleaseStatus) { + // First, check whether the version exists. + bytes32 versionKey = _encodeVersion(version); + bool exists = _versions[operatorSet.key()].contains(versionKey); + + // If the version does not exist, it is not valid. + if (!exists) return ReleaseStatus.NONEXISTENT; + + // Second, check whether the version is deprecated by a force deprecation. + uint32 deprecateAtTime = _releases[operatorSet.key()][versionKey].deprecateAtTime; + if (deprecateAtTime != 0 && block.timestamp >= deprecateAtTime) return (ReleaseStatus.DEPRECATED); + + // Third, check whether the version is deprecated by a major version upgrade. + uint32 lastUpgradeByTime = _upgradeByTimes[operatorSet.key()][getLatestStableVersion(operatorSet).major]; + if (lastUpgradeByTime != 0 && block.timestamp >= lastUpgradeByTime) return (ReleaseStatus.OUTDATED); + + // Otherwise, the version is live. + return (ReleaseStatus.LIVE); + } + + /// @inheritdoc IReleaseManager + function getLatestVersion( + OperatorSet memory operatorSet + ) public view returns (Version memory) { + EnumerableSet.Bytes32Set storage versions = _versions[operatorSet.key()]; + return _decodeVersion(versions.at(versions.length() - 1)); + } + + /// @inheritdoc IReleaseManager + function getLatestStableVersion( + OperatorSet memory operatorSet + ) public view returns (Version memory) { + EnumerableSet.Bytes32Set storage versions = _versions[operatorSet.key()]; + uint256 versionCount = versions.length(); + + // Linear search backwards for the latest stable version. + for (uint256 i = versionCount - 1; i >= 0; i--) { + bytes32 versionKey = versions.at(i); + uint32 deprecateAtTime = _releases[operatorSet.key()][versionKey].deprecateAtTime; + + // If the release is deprecated, skip it. + if (block.timestamp >= deprecateAtTime) { + continue; + } + + // Othersise, return the release version. + return _decodeVersion(versionKey); + } + + // No version has been published. + return Version({major: type(uint16).max, minor: type(uint16).max, patch: type(uint16).max}); + } +} diff --git a/src/contracts/core/ReleaseManagerStorage.sol b/src/contracts/core/ReleaseManagerStorage.sol new file mode 100644 index 000000000..649ccbbff --- /dev/null +++ b/src/contracts/core/ReleaseManagerStorage.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "../interfaces/IReleaseManager.sol"; +import "../interfaces/IAllocationManager.sol"; + +abstract contract ReleaseManagerStorage is IReleaseManager { + // Immutables + + /// @notice The EigenLayer AllocationManager contract. + IAllocationManager public immutable allocationManager; + + // Mutables + + /// @notice Maps operator set keys to their version history. + /// @param operatorSetKey The key identifying the operator set. + /// @return versions The set of version keys for this operator set. + mapping(bytes32 operatorSetKey => EnumerableSet.Bytes32Set versions) internal _versions; + + /// @notice Maps operator set and version keys to their release details. + /// @param operatorSetKey The key identifying the operator set. + /// @param versionKey The key identifying the version. + /// @return release The release details including artifacts and deprecation time. + mapping(bytes32 operatorSetKey => mapping(bytes32 versionKey => Release release)) internal _releases; + + /// @notice Maps operator set keys and major versions to their upgrade deadlines. + /// @param operatorSetKey The key identifying the operator set. + /// @param major The major version number. + /// @return upgradeByTime The timestamp by which operators must upgrade to this major version. + mapping(bytes32 operatorSetKey => mapping(uint16 major => uint32 upgradeByTime)) internal _upgradeByTimes; + + constructor( + IAllocationManager _allocationManager + ) { + allocationManager = _allocationManager; + } + + uint256[47] private __gap; +} diff --git a/src/contracts/interfaces/IReleaseManager.sol b/src/contracts/interfaces/IReleaseManager.sol new file mode 100644 index 000000000..371248b40 --- /dev/null +++ b/src/contracts/interfaces/IReleaseManager.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.27; + +import "../libraries/OperatorSetLib.sol"; + +interface IReleaseManagerErrors { + /// @notice Thrown when a release with the same digest is already deprecated. + error ReleaseAlreadyDeprecated(); + /// @notice Thrown when a deprecation is attempted that leaves an operator + /// set without a stable version. + error MustHaveAtLeastOneStableVersion(); +} + +interface IReleaseManagerTypes { + /// @notice The type of release, following semantic versioning principles. + /// @custom:constant MAJOR A breaking change that requires operators to upgrade. + /// @custom:constant MINOR A backward-compatible feature addition. + /// @custom:constant PATCH A backward-compatible bug fix. + enum ReleaseType { + MAJOR, + MINOR, + PATCH + } + + /// @notice The current status of a release in the system. + /// @custom:constant NONEXISTENT The release has not been published. + /// @custom:constant DEPRECATED The release has been explicitly deprecated. + /// @custom:constant OUTDATED A newer major version has been released. + /// @custom:constant LIVE The release is currently active and valid. + enum ReleaseStatus { + NONEXISTENT, + DEPRECATED, + OUTDATED, + LIVE + } + + /// @notice A semantic version. + /// @param major The major version. + /// @param minor The minor version. + /// @param patch The patch version. + struct Version { + uint16 major; + uint16 minor; + uint16 patch; + } + + /// @notice An artifact. + /// @param digest The digest of the artifact. + /// @param registryUrl The registry URL of the artifact. + struct Artifact { + bytes32 digest; + string registryUrl; + } + + /// @notice A release. + /// @param artifacts The artifacts of the release. + /// @param deprecateAtTime The time at which the release is deprecated. + struct Release { + Artifact[] artifacts; + uint32 deprecateAtTime; + } +} + +interface IReleaseManagerEvents is IReleaseManagerTypes { + /// @notice Emitted when a new release is published for an operator set. + /// @param operatorSetKey The key identifying the operator set. + /// @param version The version number of the published release. + /// @param releaseType The type of release (MAJOR, MINOR, or PATCH). + /// @param upgradeByTime The timestamp by which operators must upgrade to this release. + /// @param artifacts The artifacts included in this release. + event ReleasePublished( + bytes32 indexed operatorSetKey, + Version indexed version, + ReleaseType indexed releaseType, + uint32 upgradeByTime, + Artifact[] artifacts + ); + + /// @notice Emitted when a release is deprecated. + /// @param operatorSetKey The key identifying the operator set. + /// @param version The version number of the deprecated release. + /// @param deprecateAtTime The timestamp at which the release becomes deprecated. + event ReleaseDeprecated(bytes32 indexed operatorSetKey, Version indexed version, uint32 deprecateAtTime); + + /// @notice Emitted when the upgrade window for a major version is extended. + /// @param operatorSetKey The key identifying the operator set. + /// @param version The version number of the release. + /// @param upgradeWindow The new upgrade window duration in seconds. + event UpgradeWindowExtended(bytes32 indexed operatorSetKey, Version indexed version, uint32 upgradeWindow); +} + +interface IReleaseManager is IReleaseManagerErrors, IReleaseManagerEvents { + // WRITE + + /// @notice Publishes a new release for an operator set with the specified artifacts and upgrade window. + /// @param operatorSet The operator set to publish the release for. + /// @param artifacts The artifacts included in this release. + /// @param upgradeWindow The time window (in seconds) for operators to upgrade to this release. + /// @param releaseType The type of release (MAJOR, MINOR, or PATCH). + /// @return The version number of the published release. + function publishRelease( + OperatorSet calldata operatorSet, + Artifact[] calldata artifacts, + uint32 upgradeWindow, + ReleaseType releaseType + ) external returns (Version memory); + + /// @notice Deprecates a specific release version after a given delay. + /// @param operatorSet The operator set containing the release to deprecate. + /// @param version The version to deprecate. + /// @param deprecationDelay The delay (in seconds) before the release becomes deprecated. + function deprecateRelease( + OperatorSet calldata operatorSet, + Version calldata version, + uint32 deprecationDelay + ) external; + + /// @notice Extends the upgrade window for a major version release. + /// @param operatorSet The operator set containing the version. + /// @param version The version to extend the upgrade window for. + /// @param upgradeWindow The new upgrade window duration (in seconds). + function extendUpgradeWindow( + OperatorSet calldata operatorSet, + Version calldata version, + uint32 upgradeWindow + ) external; + + // READ + + /// @notice Returns the total number of releases published for an operator set. + /// @param operatorSet The operator set to query. + /// @return The total number of releases. + function getTotalVersions( + OperatorSet memory operatorSet + ) external view returns (uint256); + + /// @notice Returns the version at a specific index in the operator set's release history. + /// @param operatorSet The operator set to query. + /// @param index The index of the version to retrieve. + /// @return The version at the specified index. + function getVersion(OperatorSet memory operatorSet, uint256 index) external view returns (Version memory); + + /// @notice Returns the release details for a specific version. + /// @param operatorSet The operator set containing the release. + /// @param version The version to query. + /// @return The release details including artifacts and deprecation time. + function getRelease( + OperatorSet memory operatorSet, + Version memory version + ) external view returns (Release memory); + + /// @notice Returns the current status of a release version. + /// @param operatorSet The operator set containing the release. + /// @param version The version to check. + /// @return The status of the release (NONEXISTENT, DEPRECATED, OUTDATED, or LIVE). + function getReleaseStatus( + OperatorSet memory operatorSet, + Version memory version + ) external view returns (ReleaseStatus); + + /// @notice Returns the latest version published for an operator set. + /// @param operatorSet The operator set to query. + /// @return The latest version. + function getLatestVersion( + OperatorSet memory operatorSet + ) external view returns (Version memory); + + /// @notice Returns the latest stable (non-deprecated) version for an operator set. + /// @param operatorSet The operator set to query. + /// @return The latest stable version, or max values if no stable version exists. + function getLatestStableVersion( + OperatorSet memory operatorSet + ) external view returns (Version memory); +}