From 691932096281a64ac34971dde1494db6380e381f Mon Sep 17 00:00:00 2001 From: Serge <2901744+evercoinx@users.noreply.github.com> Date: Thu, 7 May 2026 11:48:40 +0200 Subject: [PATCH] fix: implement CYS3-02 fix --- src/contracts/Adapters/EigenAdapter.sol | 3 + src/contracts/Adapters/SymbioticAdapter.sol | 3 + src/contracts/OraclePriceFeed.sol | 3 + src/contracts/RewardsManager.sol | 13 ++ src/contracts/SSPRouter.sol | 8 + src/contracts/SlashingManager.sol | 8 + src/contracts/StakeManager.sol | 59 ++++++- .../extensions/RoleActivationTimelock.sol | 76 ++++++-- src/interfaces/IStakeManager.sol | 10 ++ src/script/MigrateOperatorCommittees.s.sol | 163 ++++++++++++++++++ src/script/UpgradeAllContracts.s.sol | 11 +- src/upgrades/baselines/RewardsManagerV0.sol | 37 ++++ src/upgrades/baselines/SSPRouterV0.sol | 52 ++++++ src/upgrades/baselines/SlashingManagerV0.sol | 41 +++++ src/upgrades/baselines/StakeManagerV0.sol | 44 +++++ 15 files changed, 515 insertions(+), 16 deletions(-) create mode 100644 src/script/MigrateOperatorCommittees.s.sol create mode 100644 src/upgrades/baselines/RewardsManagerV0.sol create mode 100644 src/upgrades/baselines/SSPRouterV0.sol create mode 100644 src/upgrades/baselines/SlashingManagerV0.sol create mode 100644 src/upgrades/baselines/StakeManagerV0.sol diff --git a/src/contracts/Adapters/EigenAdapter.sol b/src/contracts/Adapters/EigenAdapter.sol index dc2ccb59..7e0cb7d0 100644 --- a/src/contracts/Adapters/EigenAdapter.sol +++ b/src/contracts/Adapters/EigenAdapter.sol @@ -65,6 +65,9 @@ contract EigenAdapter is BaseAdapter, ReentrancyGuardUpgradeable, IEigenAdapter, IStrategyManager public strategyManager; IStrategyFactory public strategyFactory; + /// @dev Reserved storage gap for future upgrades. + uint256[50] private __gap; + /** * @notice Initialize the contract with admin and Eigen protocol addresses * @param _admin The address that will be granted admin role diff --git a/src/contracts/Adapters/SymbioticAdapter.sol b/src/contracts/Adapters/SymbioticAdapter.sol index 76cd2c34..9d4234f2 100644 --- a/src/contracts/Adapters/SymbioticAdapter.sol +++ b/src/contracts/Adapters/SymbioticAdapter.sol @@ -46,6 +46,9 @@ contract SymbioticAdapter is BaseAdapter, ReentrancyGuardUpgradeable, ISymbiotic /// @dev Each vault has its own DefaultStakerRewards contract that manages rewards for that vault mapping(address => address) private vaultToRewardsContract; + /// @dev Reserved storage gap for future upgrades. + uint256[50] private __gap; + /// @notice Initialize the contract with admin and Symbiotic protocol addresses /// @param _admin The address that will be granted admin role /// @param _operatorRegistry The Symbiotic operator registry address diff --git a/src/contracts/OraclePriceFeed.sol b/src/contracts/OraclePriceFeed.sol index 85dbfc7e..4f10c93a 100644 --- a/src/contracts/OraclePriceFeed.sol +++ b/src/contracts/OraclePriceFeed.sol @@ -21,6 +21,9 @@ contract OraclePriceFeed is IOraclePriceFeed, RoleActivationTimelock, UUPSUpgrad /// @notice Mapping from token address to staleness threshold (in seconds) mapping(address => uint256) private _stalenessThresholds; + /// @dev Reserved storage gap for future upgrades. + uint256[50] private __gap; + modifier onlyRegisteredToken(address token) { if (!hasPriceFeed(token)) revert PriceFeedNotFound(token); _; diff --git a/src/contracts/RewardsManager.sol b/src/contracts/RewardsManager.sol index 500f8e9d..65a6d459 100644 --- a/src/contracts/RewardsManager.sol +++ b/src/contracts/RewardsManager.sol @@ -37,6 +37,19 @@ contract RewardsManager is IRewardsManager, RoleActivationTimelock, ReentrancyGu /// @notice Mapping to track if rewards have been distributed for a specific task instance mapping(bytes32 => bool) private rewardsDistributed; + /// @custom:oz-renamed-from totalRewardsPerNetworkInstance + /// @dev Deprecated. Slot retained for upgrade layout compatibility; data is stale and + /// must not be read by new logic. Future re-use only via ERC-7201 namespacing. + mapping(address => mapping(bytes32 => uint256)) private _deprecated_totalRewardsPerNetworkInstance; + + /// @custom:oz-renamed-from rewardDistributionCounter + /// @dev Deprecated. Slot retained for upgrade layout compatibility; value is stale and + /// must not be read by new logic. Future re-use only via ERC-7201 namespacing. + uint256 private _deprecated_rewardDistributionCounter; + + /// @dev Reserved storage gap for future upgrades. + uint256[50] private __gap; + /// @custom:oz-upgrades-unsafe-allow constructor /// @notice Prevents the implementation contract from being initialized directly. /// @dev This locks the implementation and ensures initialization can only occur through a proxy. diff --git a/src/contracts/SSPRouter.sol b/src/contracts/SSPRouter.sol index 2acd2aaf..dbc4dc8c 100644 --- a/src/contracts/SSPRouter.sol +++ b/src/contracts/SSPRouter.sol @@ -60,6 +60,11 @@ contract SSPRouter is ISSPRouter, RoleActivationTimelock, ReentrancyGuardUpgrade /// @notice Mapping: committeeId => moduleType => set of vault addresses for efficient filtering mapping(uint96 => mapping(ISSPRouter.SSPModuleType => EnumerableSet.AddressSet)) private committeeModuleVaults; + /// @custom:oz-renamed-from committeeStakeRequirement + /// @dev Deprecated. Slot retained for upgrade layout compatibility; data is stale and + /// must not be read by new logic. Future re-use only via ERC-7201 namespacing. + mapping(uint96 => uint256) private _deprecated_committeeStakeRequirement; + /// @notice Mapping from committee ID to duration for the committee mapping(uint96 => uint32) public committeeToDuration; @@ -69,6 +74,9 @@ contract SSPRouter is ISSPRouter, RoleActivationTimelock, ReentrancyGuardUpgrade /// @notice Address of the Chainlink price feed contract address public oraclePriceFeed; + /// @dev Reserved storage gap for future upgrades. + uint256[50] private __gap; + /// @custom:oz-upgrades-unsafe-allow constructor /// @notice Prevents the implementation contract from being initialized directly. /// @dev This locks the implementation and ensures initialization can only occur through a proxy. diff --git a/src/contracts/SlashingManager.sol b/src/contracts/SlashingManager.sol index fe802544..0725784a 100644 --- a/src/contracts/SlashingManager.sol +++ b/src/contracts/SlashingManager.sol @@ -43,6 +43,14 @@ contract SlashingManager is /// @notice Mapping to track if slashing has been executed for a specific task instance mapping(bytes32 => bool) private slashingExecuted; + /// @custom:oz-renamed-from slashingCounter + /// @dev Deprecated. Slot retained for upgrade layout compatibility; value is stale and + /// must not be read by new logic. Future re-use only via ERC-7201 namespacing. + uint256 private _deprecated_slashingCounter; + + /// @dev Reserved storage gap for future upgrades. + uint256[50] private __gap; + /// @custom:oz-upgrades-unsafe-allow constructor /// @notice Prevents the implementation contract from being initialized directly. /// @dev This locks the implementation and ensures initialization can only occur through a proxy. diff --git a/src/contracts/StakeManager.sol b/src/contracts/StakeManager.sol index 1a46906a..8c88146a 100644 --- a/src/contracts/StakeManager.sol +++ b/src/contracts/StakeManager.sol @@ -28,15 +28,17 @@ contract StakeManager is IStakeManager, RoleActivationTimelock, UUPSUpgradeable address public router; address public coverPoolFactory; - // Track all committee IDs - EnumerableSet.UintSet private _committeeIds; - /// @notice Mapping of committee ID to set of vaults mapping(uint96 => EnumerableSet.AddressSet) private _committeeVaults; /// @notice Mapping to track if an operator is registered mapping(address => bool) private _isOperator; - /// @notice Mapping: operator => set of committee IDs the operator belongs to - mapping(address => EnumerableSet.UintSet) private _operatorCommitteeIds; + + /// @custom:oz-renamed-from _operatorCommitteeId + /// @dev Deprecated. Replaced by _operatorCommitteeIds for multi-committee support. + /// Slot retained so existing per-operator records remain decodable until + /// migrateOperatorCommittees() backfills the new set. Must not be written by new logic. + mapping(address => uint96) private _deprecated_operatorCommitteeId; + // operator associated with vaults mapping(address => EnumerableSet.AddressSet) private _vaultToOperators; /// @notice Mapping: committeeId => operator address @@ -44,6 +46,16 @@ contract StakeManager is IStakeManager, RoleActivationTimelock, UUPSUpgradeable /// @notice Mapping: committeeId => authorized cover pool address mapping(uint96 => address) private _committeeToAuthorizedCoverPool; + // Appended after audit baseline + + /// @notice Track all committee IDs + EnumerableSet.UintSet private _committeeIds; + /// @notice Mapping: operator => set of committee IDs the operator belongs to + mapping(address => EnumerableSet.UintSet) private _operatorCommitteeIds; + + /// @dev Reserved storage gap for future upgrades. + uint256[47] private __gap; + /// @custom:oz-upgrades-unsafe-allow constructor /// @notice Prevents the implementation contract from being initialized directly. /// @dev This locks the implementation and ensures initialization can only occur through a proxy. @@ -281,6 +293,43 @@ contract StakeManager is IStakeManager, RoleActivationTimelock, UUPSUpgradeable return _committeeOperator[committeeId] == operator; } + // @inheritdoc IStakeManager + /// @notice Idempotent migration: copies the old single-committee-per-operator mapping into the + /// new multi-committee set. Safe to call multiple times; EnumerableSet.add is a no-op on + /// duplicates. Call via MigrateOperatorCommittees.s.sol after upgrading the proxy. + function migrateOperatorCommittees(address[] calldata operators) + external + onlyActiveRole(DEFAULT_ADMIN_ROLE) + { + uint256 len = operators.length; + for (uint256 i = 0; i < len; i++) { + address op = operators[i]; + uint96 committeeId = _deprecated_operatorCommitteeId[op]; + if (committeeId != 0) { + _operatorCommitteeIds[op].add(uint256(committeeId)); + if (!_isOperator[op]) { + _isOperator[op] = true; + } + } + } + } + + /// @notice Idempotent migration: backfills _committeeIds from existing + /// _committeeToAuthorizedCoverPool entries. Safe to call multiple times. + /// Call via MigrateOperatorCommittees.s.sol after upgrading the proxy. + function backfillCommitteeIds(uint96[] calldata committeeIds) + external + onlyActiveRole(DEFAULT_ADMIN_ROLE) + { + uint256 len = committeeIds.length; + for (uint256 i = 0; i < len; i++) { + uint96 id = committeeIds[i]; + if (_committeeToAuthorizedCoverPool[id] != address(0)) { + _committeeIds.add(uint256(id)); + } + } + } + function _addOperatorToCommittee(address operator, uint96 committeeId) internal { address[] memory vaults = _committeeVaults[committeeId].values(); uint256 vaultsLength = vaults.length; diff --git a/src/contracts/extensions/RoleActivationTimelock.sol b/src/contracts/extensions/RoleActivationTimelock.sol index 7d8e3f88..92fee916 100644 --- a/src/contracts/extensions/RoleActivationTimelock.sol +++ b/src/contracts/extensions/RoleActivationTimelock.sol @@ -30,16 +30,54 @@ import {AccessControlUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/a /// /// Inheriting contracts must replace onlyRole(X) with onlyActiveRole(X) on functions /// that should enforce the cooldown on the caller. +/// +/// @dev State is stored in ERC-7201 namespaced storage so that inheriting contracts see zero +/// additional sequential storage slots, preserving the storage layout of any previously +/// deployed proxy implementations that did not have this base class. abstract contract RoleActivationTimelock is AccessControlUpgradeable { /// @notice Emitted when the global activation delay is updated /// @param delay The new delay value in seconds event DelaySet(uint256 delay); - /// @notice Global activation delay applied to every role grant - uint256 public delay; + // ------------------------------------------------------------------------- + // ERC-7201 namespaced storage + // ------------------------------------------------------------------------- + + /// @custom:storage-location erc7201:catalysis.storage.RoleActivationTimelock + struct RoleActivationTimelockStorage { + /// @notice Global activation delay applied to every role grant + uint256 delay; + /// @notice role => account => timestamp when the role was granted (0 = init-time grant, treat as active) + mapping(bytes32 => mapping(address => uint256)) roleGrantedAt; + } + + // keccak256(abi.encode(uint256(keccak256("catalysis.storage.RoleActivationTimelock")) - 1)) + // & ~bytes32(uint256(0xff)) + bytes32 private constant _ROLE_ACTIVATION_TIMELOCK_STORAGE_LOCATION = + 0x6ca78a7182eeacaae9d958ef0ac90e179bbb6030a902ec93ea55d8b1994f9800; + + function _getRoleActivationTimelockStorage() + private + pure + returns (RoleActivationTimelockStorage storage $) + { + assembly { + $.slot := _ROLE_ACTIVATION_TIMELOCK_STORAGE_LOCATION + } + } + + // ------------------------------------------------------------------------- + // Public view + // ------------------------------------------------------------------------- + + /// @notice Global activation delay applied to every role grant (in seconds). + function delay() public view returns (uint256) { + return _getRoleActivationTimelockStorage().delay; + } - /// @notice role => account => timestamp when the role was granted (0 = init-time grant, treat as active) - mapping(bytes32 => mapping(address => uint256)) private _roleGrantedAt; + // ------------------------------------------------------------------------- + // Modifiers + // ------------------------------------------------------------------------- /// @notice Blocks execution until the caller's role has passed its activation delay modifier onlyActiveRole(bytes32 role) { @@ -48,18 +86,30 @@ abstract contract RoleActivationTimelock is AccessControlUpgradeable { _; } + // ------------------------------------------------------------------------- + // Initializer + // ------------------------------------------------------------------------- + /// @notice Must be called inside the inheriting contract's initialize() /// @param _delay Activation delay in seconds function __RoleActivationTimelock_init(uint256 _delay) internal onlyInitializing { - delay = _delay; + _getRoleActivationTimelockStorage().delay = _delay; } + // ------------------------------------------------------------------------- + // Admin functions + // ------------------------------------------------------------------------- + /// @notice Update the global activation delay; only callable by a matured DEFAULT_ADMIN_ROLE function setDelay(uint256 _delay) external onlyActiveRole(DEFAULT_ADMIN_ROLE) { - delay = _delay; + _getRoleActivationTimelockStorage().delay = _delay; emit DelaySet(_delay); } + // ------------------------------------------------------------------------- + // AccessControl overrides + // ------------------------------------------------------------------------- + /// @notice Enforces caller-maturity before granting: the caller's own admin role must have /// passed its cooldown. Records the grantee's timestamp only on a fresh grant. function grantRole(bytes32 role, address account) public virtual override { @@ -74,7 +124,7 @@ abstract contract RoleActivationTimelock is AccessControlUpgradeable { function _grantRole(bytes32 role, address account) internal virtual override returns (bool) { if (super._grantRole(role, account)) { if (!_isInitializing()) { - _roleGrantedAt[role][account] = block.timestamp; + _getRoleActivationTimelockStorage().roleGrantedAt[role][account] = block.timestamp; } return true; } @@ -95,20 +145,26 @@ abstract contract RoleActivationTimelock is AccessControlUpgradeable { function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) { if (super._revokeRole(role, account)) { if (!_isInitializing()) { - delete _roleGrantedAt[role][account]; + delete _getRoleActivationTimelockStorage().roleGrantedAt[role][account]; } return true; } return false; } + // ------------------------------------------------------------------------- + // Internal helpers + // ------------------------------------------------------------------------- + /// @notice Reverts if the caller's adminRole has not yet passed its activation delay. function _requireCallerMatured(bytes32 adminRole) internal view { - require(block.timestamp >= _roleGrantedAt[adminRole][msg.sender] + delay, "Caller role not yet active"); + RoleActivationTimelockStorage storage $ = _getRoleActivationTimelockStorage(); + require(block.timestamp >= $.roleGrantedAt[adminRole][msg.sender] + $.delay, "Caller role not yet active"); } /// @notice Reverts if account's role has not yet passed its activation delay. function _requireRoleActive(bytes32 role, address account) internal view { - require(block.timestamp >= _roleGrantedAt[role][account] + delay, "Role not yet active"); + RoleActivationTimelockStorage storage $ = _getRoleActivationTimelockStorage(); + require(block.timestamp >= $.roleGrantedAt[role][account] + $.delay, "Role not yet active"); } } diff --git a/src/interfaces/IStakeManager.sol b/src/interfaces/IStakeManager.sol index 59dbda52..b12e6c90 100644 --- a/src/interfaces/IStakeManager.sol +++ b/src/interfaces/IStakeManager.sol @@ -154,4 +154,14 @@ interface IStakeManager { /// @param operator The address of the operator /// @return Array of committee IDs the operator is a member of function getOperatorCommitteeIds(address operator) external view returns (uint256[] memory); + + /// @notice One-time idempotent migration: copies each operator's old single-committee mapping + /// into the new multi-committee set. Safe to call multiple times. + /// @param operators Array of operator addresses to migrate. + function migrateOperatorCommittees(address[] calldata operators) external; + + /// @notice One-time idempotent migration: backfills _committeeIds from existing + /// _committeeToAuthorizedCoverPool entries. Safe to call multiple times. + /// @param committeeIds Array of committee IDs to add to the set. + function backfillCommitteeIds(uint96[] calldata committeeIds) external; } diff --git a/src/script/MigrateOperatorCommittees.s.sol b/src/script/MigrateOperatorCommittees.s.sol new file mode 100644 index 00000000..0fba4677 --- /dev/null +++ b/src/script/MigrateOperatorCommittees.s.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Script, console2} from "forge-std/Script.sol"; +import {IStakeManager} from "../interfaces/IStakeManager.sol"; + +/** + * @title MigrateOperatorCommittees + * @notice Post-upgrade migration script for StakeManager. + * + * After upgrading StakeManager from the audit-baseline implementation to the new + * multi-committee layout, two idempotent on-chain migrations must be run: + * + * 1. backfillCommitteeIds — populates the new _committeeIds EnumerableSet from + * the existing _committeeToAuthorizedCoverPool entries. + * + * 2. migrateOperatorCommittees — copies each operator's old single-committee mapping + * (_deprecated_operatorCommitteeId) into the new _operatorCommitteeIds set. + * + * Both functions are safe to run multiple times (EnumerableSet.add is a no-op on + * duplicates). Run in sequence: committeeIds first so that _isOperator flags are + * populated before operator iteration. + * + * Required environment variables: + * - DEPLOYER_ADDRESS: Address of the DEFAULT_ADMIN_ROLE holder (or a Safe / relayer). + * - STAKE_MANAGER_PROXY: Address of the StakeManager UUPS proxy. + * + * Provide operator and committee arrays as comma-separated env vars: + * - COMMITTEE_IDS: comma-separated uint96 committee IDs (e.g. "1,2,3") + * - OPERATOR_ADDRESSES: comma-separated operator addresses + * + * Usage (simulate, no broadcast): + * forge script src/script/MigrateOperatorCommittees.s.sol \ + * --rpc-url $RPC_URL + * + * Usage (broadcast): + * forge script src/script/MigrateOperatorCommittees.s.sol \ + * --rpc-url $RPC_URL --broadcast [--keystore ...] [--ledger] + * + * @dev Both migration functions are guarded by DEFAULT_ADMIN_ROLE on StakeManager. + * Enumerate operator addresses and committee IDs from historical event logs: + * cast logs --rpc-url $RPC_URL --address $STAKE_MANAGER_PROXY \ + * "OperatorAddedToCommittee(address,uint96)" + * cast logs --rpc-url $RPC_URL --address $STAKE_MANAGER_PROXY \ + * "CommitteeCreated(uint96)" + */ +contract MigrateOperatorCommittees is Script { + address internal constant DEFAULT_DEPLOYER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + + function run() external { + address deployer = vm.envOr("DEPLOYER_ADDRESS", DEFAULT_DEPLOYER); + address stakeManagerProxy = vm.envAddress("STAKE_MANAGER_PROXY"); + + IStakeManager sm = IStakeManager(stakeManagerProxy); + + // Parse committee IDs from env + uint96[] memory committeeIds = _parseCommitteeIds(vm.envOr("COMMITTEE_IDS", string(""))); + // Parse operator addresses from env + address[] memory operators = _parseAddresses(vm.envOr("OPERATOR_ADDRESSES", string(""))); + + console2.log("=== StakeManager Post-Upgrade Migration ==="); + console2.log("StakeManager proxy:", stakeManagerProxy); + console2.log("Admin:", deployer); + console2.log("Committee IDs to backfill:", committeeIds.length); + console2.log("Operators to migrate:", operators.length); + + vm.startBroadcast(deployer); + + if (committeeIds.length > 0) { + sm.backfillCommitteeIds(committeeIds); + console2.log(" backfillCommitteeIds() called."); + } else { + console2.log(" [skip] COMMITTEE_IDS not set; backfillCommitteeIds() skipped."); + } + + if (operators.length > 0) { + sm.migrateOperatorCommittees(operators); + console2.log(" migrateOperatorCommittees() called."); + } else { + console2.log(" [skip] OPERATOR_ADDRESSES not set; migrateOperatorCommittees() skipped."); + } + + vm.stopBroadcast(); + + console2.log("=== Migration complete. Re-run to verify idempotency. ==="); + } + + /// @dev Parse a comma-separated list of uint96 values from a string env var. + function _parseCommitteeIds(string memory raw) internal pure returns (uint96[] memory ids) { + if (bytes(raw).length == 0) return new uint96[](0); + + // Count commas to determine array length + uint256 count = 1; + bytes memory b = bytes(raw); + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == ",") ++count; + } + + ids = new uint96[](count); + uint256 idx = 0; + uint256 start = 0; + for (uint256 i = 0; i <= b.length; i++) { + if (i == b.length || b[i] == ",") { + bytes memory segment = new bytes(i - start); + for (uint256 j = start; j < i; j++) { + segment[j - start] = b[j]; + } + ids[idx++] = uint96(_parseUint(string(segment))); + start = i + 1; + } + } + } + + /// @dev Parse a comma-separated list of hex addresses from a string env var. + function _parseAddresses(string memory raw) internal pure returns (address[] memory addrs) { + if (bytes(raw).length == 0) return new address[](0); + + uint256 count = 1; + bytes memory b = bytes(raw); + for (uint256 i = 0; i < b.length; i++) { + if (b[i] == ",") ++count; + } + + addrs = new address[](count); + uint256 idx = 0; + uint256 start = 0; + for (uint256 i = 0; i <= b.length; i++) { + if (i == b.length || b[i] == ",") { + bytes memory segment = new bytes(i - start); + for (uint256 j = start; j < i; j++) { + segment[j - start] = b[j]; + } + addrs[idx++] = _parseAddress(string(segment)); + start = i + 1; + } + } + } + + function _parseUint(string memory s) internal pure returns (uint256 result) { + bytes memory b = bytes(s); + for (uint256 i = 0; i < b.length; i++) { + uint8 c = uint8(b[i]); + require(c >= 48 && c <= 57, "Invalid digit"); + result = result * 10 + (c - 48); + } + } + + function _parseAddress(string memory s) internal pure returns (address) { + bytes memory b = bytes(s); + require(b.length == 42, "Address must be 42 chars (0x...)"); + uint160 result = 0; + for (uint256 i = 2; i < 42; i++) { + uint8 c = uint8(b[i]); + uint8 nibble; + if (c >= 48 && c <= 57) nibble = c - 48; + else if (c >= 65 && c <= 70) nibble = c - 55; + else if (c >= 97 && c <= 102) nibble = c - 87; + else revert("Invalid hex char in address"); + result = result * 16 + nibble; + } + return address(result); + } +} diff --git a/src/script/UpgradeAllContracts.s.sol b/src/script/UpgradeAllContracts.s.sol index 4730c509..2b9af86a 100644 --- a/src/script/UpgradeAllContracts.s.sol +++ b/src/script/UpgradeAllContracts.s.sol @@ -188,7 +188,16 @@ contract UpgradeAllContracts is Script { /** * @notice Upgrades all proxy contracts to their new implementations - * @dev Uses upgradeToAndCall with empty data since no re-initialization is needed + * @dev Uses upgradeToAndCall with empty data since no re-initialization is needed. + * + * IMPORTANT: This script bypasses the OZ Upgrades plugin storage-layout check because it + * calls upgradeToAndCall directly. Before broadcasting, manually verify the implementation + * storage layout with `forge inspect storageLayout` and compare it against the + * committed snapshots in test/snapshots/storage/. Do NOT skip this step. + * + * After upgrading proxies that use a multi-committee mapping (StakeManager), run + * MigrateOperatorCommittees.s.sol to backfill _operatorCommitteeIds and _committeeIds + * from the deprecated single-committee mapping. * @param proxies The proxy addresses to upgrade * @param newImpls The new implementation addresses */ diff --git a/src/upgrades/baselines/RewardsManagerV0.sol b/src/upgrades/baselines/RewardsManagerV0.sol new file mode 100644 index 00000000..e696ff7f --- /dev/null +++ b/src/upgrades/baselines/RewardsManagerV0.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {AccessControlUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +/** + * @title RewardsManagerV0 + * @notice Skeleton reference contract capturing the audit-baseline storage layout of RewardsManager + * (commit 9e1c5bc). Used by the OZ Upgrades plugin as referenceContract. + * + * Storage layout (audit baseline 9e1c5bc): + * slot 0: premiumManager (addr 20) + * slot 1: sspRouter (addr 20) + * slot 2: committeeRewardTokenAmount mapping seed + * slot 3: rewardsDistributed mapping seed + * slot 4: totalRewardsPerNetworkInstance mapping seed + * slot 5: rewardDistributionCounter (uint256) + * + * @dev ONLY used as a layout reference. Must never be deployed. + */ +contract RewardsManagerV0 is AccessControlUpgradeable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + address public premiumManager; + address public sspRouter; + mapping(uint96 => mapping(address => uint256)) public committeeRewardTokenAmount; + mapping(bytes32 => bool) private rewardsDistributed; + mapping(address => mapping(bytes32 => uint256)) public totalRewardsPerNetworkInstance; + uint256 public rewardDistributionCounter; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/src/upgrades/baselines/SSPRouterV0.sol b/src/upgrades/baselines/SSPRouterV0.sol new file mode 100644 index 00000000..be2bd93a --- /dev/null +++ b/src/upgrades/baselines/SSPRouterV0.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {AccessControlUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {EnumerableSet} from "@openzeppelin-v5/contracts/utils/structs/EnumerableSet.sol"; +import {ISSPRouter} from "../../interfaces/ISSPRouter.sol"; + +/** + * @title SSPRouterV0 + * @notice Skeleton reference contract capturing the audit-baseline storage layout of SSPRouter + * (commit 9e1c5bc). Used by the OZ Upgrades plugin as referenceContract. + * + * Storage layout (audit baseline 9e1c5bc): + * slot 0: stakeManager + * slot 1: rewardsManager + * slot 2: slashingManager + * slot 3: stakeRecipient + * slot 4: adapters mapping seed + * slot 5: vaultToModule mapping seed + * slot 6: moduleVaults mapping seed + * slot 7: committeeModuleVaults mapping seed + * slot 8: committeeStakeRequirement mapping seed + * slot 9: committeeToDuration mapping seed + * slot 10-11: acceptableTokens (AddressSet, 2 slots) + * slot 12: chainlinkPriceFeed / oraclePriceFeed + * + * @dev ONLY used as a layout reference. Must never be deployed. + */ +contract SSPRouterV0 is AccessControlUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + + address public stakeManager; + address public rewardsManager; + address public slashingManager; + address public stakeRecipient; + mapping(ISSPRouter.SSPModuleType => address) private adapters; + mapping(address => ISSPRouter.SSPModuleType) public vaultToModule; + mapping(ISSPRouter.SSPModuleType => EnumerableSet.AddressSet) private moduleVaults; + mapping(uint96 => mapping(ISSPRouter.SSPModuleType => EnumerableSet.AddressSet)) private committeeModuleVaults; + mapping(uint96 => uint256) public committeeStakeRequirement; + mapping(uint96 => uint32) public committeeToDuration; + EnumerableSet.AddressSet private acceptableTokens; + address public oraclePriceFeed; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/src/upgrades/baselines/SlashingManagerV0.sol b/src/upgrades/baselines/SlashingManagerV0.sol new file mode 100644 index 00000000..ce6746d3 --- /dev/null +++ b/src/upgrades/baselines/SlashingManagerV0.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {AccessControlUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/utils/PausableUpgradeable.sol"; + +/** + * @title SlashingManagerV0 + * @notice Skeleton reference contract capturing the audit-baseline storage layout of SlashingManager + * (commit 9e1c5bc). Used by the OZ Upgrades plugin as referenceContract. + * + * Storage layout (audit baseline 9e1c5bc): + * slot 0: stakeManager (addr 20) + * slot 1: sspRouter (addr 20) + * slot 2: claimManager (addr 20) + * slot 3: slashingExecuted mapping seed + * slot 4: slashingCounter (uint256) + * + * @dev ONLY used as a layout reference. Must never be deployed. + */ +contract SlashingManagerV0 is + AccessControlUpgradeable, + ReentrancyGuardUpgradeable, + UUPSUpgradeable, + PausableUpgradeable +{ + address public stakeManager; + address public sspRouter; + address public claimManager; + mapping(bytes32 => bool) private slashingExecuted; + uint256 public slashingCounter; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +} diff --git a/src/upgrades/baselines/StakeManagerV0.sol b/src/upgrades/baselines/StakeManagerV0.sol new file mode 100644 index 00000000..a4b16d91 --- /dev/null +++ b/src/upgrades/baselines/StakeManagerV0.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {AccessControlUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin-v5/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {EnumerableSet} from "@openzeppelin-v5/contracts/utils/structs/EnumerableSet.sol"; + +/** + * @title StakeManagerV0 + * @notice Skeleton reference contract capturing the audit-baseline storage layout of StakeManager + * (commit 9e1c5bc). Used by the OZ Upgrades plugin as referenceContract. + * + * Storage layout (audit baseline 9e1c5bc, pre-RoleActivationTimelock inheritance): + * slot 0: router (addr 20) + * slot 1: coverPoolFactory (addr 20) + * slot 2: _committeeVaults mapping seed + * slot 3: _isOperator mapping seed + * slot 4: _operatorCommitteeId mapping seed (mapping(address => uint96)) + * slot 5: _vaultToOperators mapping seed + * slot 6: _committeeOperator mapping seed + * slot 7: _committeeToAuthorizedCoverPool mapping seed + * + * @dev ONLY used as a layout reference. Must never be deployed. + */ +contract StakeManagerV0 is AccessControlUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + + address public router; + address public coverPoolFactory; + + mapping(uint96 => EnumerableSet.AddressSet) private _committeeVaults; + mapping(address => bool) private _isOperator; + mapping(address => uint96) private _operatorCommitteeId; + mapping(address => EnumerableSet.AddressSet) private _vaultToOperators; + mapping(uint96 => address) private _committeeOperator; + mapping(uint96 => address) private _committeeToAuthorizedCoverPool; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} +}