Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/contracts/Adapters/EigenAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/contracts/Adapters/SymbioticAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/contracts/OraclePriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
_;
Expand Down
13 changes: 13 additions & 0 deletions src/contracts/RewardsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/contracts/SSPRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/contracts/SlashingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
59 changes: 54 additions & 5 deletions src/contracts/StakeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,34 @@ 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
mapping(uint96 => address) private _committeeOperator;
/// @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.
Expand Down Expand Up @@ -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;
Expand Down
76 changes: 66 additions & 10 deletions src/contracts/extensions/RoleActivationTimelock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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");
}
}
10 changes: 10 additions & 0 deletions src/interfaces/IStakeManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading
Loading