Skip to content

Commit

Permalink
feat: refactor middleware to use strategy timestamps
Browse files Browse the repository at this point in the history
  • Loading branch information
merklefruit committed Jan 21, 2025
1 parent 2ed71ec commit be9c436
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 41 deletions.
120 changes: 99 additions & 21 deletions smart-contracts/src/holesky/contracts/BoltEigenLayerMiddlewareV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ pragma solidity ^0.8.27;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";

import {PauseableEnumerableSet} from "@symbiotic/middleware-sdk/libraries/PauseableEnumerableSet.sol";

import {
IAllocationManager, IAllocationManagerTypes
Expand All @@ -28,7 +30,7 @@ contract BoltEigenLayerMiddlewareV1 is
IAVSRegistrar,
IBoltRestakingMiddlewareV1
{
using EnumerableSet for EnumerableSet.AddressSet;
using PauseableEnumerableSet for PauseableEnumerableSet.AddressSet;

/// @notice Address of the EigenLayer Allocation Manager contract.
IAllocationManager public ALLOCATION_MANAGER;
Expand All @@ -46,7 +48,7 @@ contract BoltEigenLayerMiddlewareV1 is
bytes32 public NAME_HASH;

/// @notice The list of whitelisted strategies for this AVS
EnumerableSet.AddressSet internal whitelistedStrategies;
PauseableEnumerableSet.AddressSet internal whitelistedStrategies;

/**
* @dev This empty reserved space is put in place to allow future versions to add new
Expand All @@ -63,6 +65,12 @@ contract BoltEigenLayerMiddlewareV1 is
/// @notice Emitted when a strategy is whitelisted
event StrategyAddedToWhitelist(address strategy);

/// @notice Emitted when a strategy is paused
event StrategyPaused(address strategy);

/// @notice Emitted when a strategy is unpaused
event StrategyUnpaused(address strategy);

/// @notice Emitted when a strategy is removed from the whitelist
event StrategyRemovedFromWhitelist(address strategy);

Expand Down Expand Up @@ -105,22 +113,21 @@ contract BoltEigenLayerMiddlewareV1 is
function getOperatorCollaterals(
address operator
) public view returns (address[] memory, uint256[] memory) {
address[] memory collateralTokens = new address[](whitelistedStrategies.length());
uint256[] memory amounts = new uint256[](whitelistedStrategies.length());
// Use the beginning of the current epoch to check which strategies were enabled at that time.
// Only the strategies enabled at the beginning of the epoch are considered for the operator's collateral.
uint48 timestamp = OPERATORS_REGISTRY.getCurrentEpochStartTimestamp();
IStrategy[] memory activeStrategies = _getActiveStrategiesAt(timestamp);

// cast strategies to IStrategy; this should be zero-cost but Solidity doesn't allow it directly
IStrategy[] memory strategies = new IStrategy[](whitelistedStrategies.length());
for (uint256 i = 0; i < whitelistedStrategies.length(); i++) {
strategies[i] = IStrategy(whitelistedStrategies.at(i));
}
address[] memory collateralTokens = new address[](activeStrategies.length);
uint256[] memory amounts = new uint256[](activeStrategies.length);

// get the shares of the operator across all strategies
uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, strategies);
uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, activeStrategies);

// get the collateral tokens and amounts for the operator across all strategies
for (uint256 i = 0; i < strategies.length; i++) {
collateralTokens[i] = address(strategies[i].underlyingToken());
amounts[i] = strategies[i].sharesToUnderlyingView(shares[i]);
for (uint256 i = 0; i < activeStrategies.length; i++) {
collateralTokens[i] = address(activeStrategies[i].underlyingToken());
amounts[i] = activeStrategies[i].sharesToUnderlyingView(shares[i]);
}

return (collateralTokens, amounts);
Expand All @@ -136,10 +143,23 @@ contract BoltEigenLayerMiddlewareV1 is
return 0;
}

/// @notice Get the list of whitelisted strategies for this AVS
/// @notice Get the list of whitelisted strategies for this AVS and whether they are enabled
/// @return The list of whitelisted strategies
function getWhitelistedStrategies() public view returns (address[] memory) {
return whitelistedStrategies.values();
function getWhitelistedStrategies() public view returns (address[] memory, bool[] memory) {
address[] memory strategies = new address[](whitelistedStrategies.length());
bool[] memory enabled = new bool[](whitelistedStrategies.length());

// Use the beginning of the current epoch to check which strategies were enabled at that time.
uint48 timestamp = OPERATORS_REGISTRY.getCurrentEpochStartTimestamp();

for (uint256 i = 0; i < whitelistedStrategies.length(); i++) {
(address strategy, uint48 enabledAt, uint48 disabledAt) = whitelistedStrategies.at(i);

strategies[i] = strategy;
enabled[i] = _wasEnabledAt(enabledAt, disabledAt, timestamp);
}

return (strategies, enabled);
}

// ========= AVS Registrar functions ========= //
Expand All @@ -165,8 +185,10 @@ contract BoltEigenLayerMiddlewareV1 is
// called by operators when deregistering from this AVS.
// Failure does nothing here: if this call reverts the deregistration will still go through.

// We forward the call to the OperatorsRegistry to deregister the operator from its storage.
OPERATORS_REGISTRY.deregisterOperator(operator);
// We forward the call to the OperatorsRegistry to pause the operator from its storage.
// In order to be fully removed, the operator must call OPERATORS_REGISTRY.deregisterOperator()
// after waiting for the required delay.
OPERATORS_REGISTRY.pauseOperator(operator);
}

// ========= Admin functions ========= //
Expand All @@ -180,18 +202,41 @@ contract BoltEigenLayerMiddlewareV1 is
require(!whitelistedStrategies.contains(strategy), "Strategy already whitelisted");
require(STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy)), "Strategy not allowed");

whitelistedStrategies.add(strategy);
whitelistedStrategies.register(Time.timestamp(), strategy);
emit StrategyAddedToWhitelist(strategy);
}

/// @notice Pause a strategy, preventing its collateral from being active in the AVS
/// @param strategy The strategy to pause
function pauseStrategy(
address strategy
) public onlyOwner {
require(whitelistedStrategies.contains(strategy), "Strategy not whitelisted");

whitelistedStrategies.pause(Time.timestamp(), strategy);
emit StrategyPaused(strategy);
}

/// @notice Unpause a strategy, allowing its collateral to be active in the AVS
/// @param strategy The strategy to unpause
function unpauseStrategy(
address strategy
) public onlyOwner {
require(whitelistedStrategies.contains(strategy), "Strategy not whitelisted");

whitelistedStrategies.unpause(Time.timestamp(), OPERATORS_REGISTRY.EPOCH_DURATION(), strategy);
emit StrategyUnpaused(strategy);
}

/// @notice Remove a strategy from the whitelist
/// @param strategy The strategy to remove
/// @dev Strategies must be paused for an EPOCH_DURATION before they can be removed
function removeStrategyFromWhitelist(
address strategy
) public onlyOwner {
require(whitelistedStrategies.contains(strategy), "Strategy not whitelisted");

whitelistedStrategies.remove(strategy);
whitelistedStrategies.unregister(Time.timestamp(), OPERATORS_REGISTRY.EPOCH_DURATION(), strategy);
emit StrategyRemovedFromWhitelist(strategy);
}

Expand Down Expand Up @@ -243,4 +288,37 @@ contract BoltEigenLayerMiddlewareV1 is
require(whitelistedStrategies.contains(address(strategies[i])), "Strategy not whitelisted");
}
}

/// @notice Get all the active strategies at a given timestamp
/// @param timestamp The timestamp to get the active strategies at
/// @return The array of active strategies
function _getActiveStrategiesAt(
uint48 timestamp
) internal view returns (IStrategy[] memory) {
uint256 activeCount = 0;
IStrategy[] memory activeStrategies = new IStrategy[](whitelistedStrategies.length());
for (uint256 i = 0; i < whitelistedStrategies.length(); i++) {
(address strategy, uint48 enabledAt, uint48 disabledAt) = whitelistedStrategies.at(i);

if (_wasEnabledAt(enabledAt, disabledAt, timestamp)) {
activeStrategies[activeCount] = IStrategy(strategy);
activeCount++;
}
}

// Resize the array to the actual number of active strategies
IStrategy[] memory result = new IStrategy[](activeCount);
for (uint256 i = 0; i < activeCount; i++) {
result[i] = activeStrategies[i];
}
}

/// @notice Check if a map entry was active at a given timestamp.
/// @param enabledAt The enabled time of the map entry.
/// @param disabledAt The disabled time of the map entry.
/// @param timestamp The timestamp to check the map entry status at.
/// @return True if the map entry was active at the given timestamp, false otherwise.
function _wasEnabledAt(uint48 enabledAt, uint48 disabledAt, uint48 timestamp) internal pure returns (bool) {
return enabledAt != 0 && enabledAt <= timestamp && (disabledAt == 0 || disabledAt >= timestamp);
}
}
44 changes: 32 additions & 12 deletions smart-contracts/src/holesky/contracts/OperatorsRegistryV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ pragma solidity ^0.8.27;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {Time} from "@openzeppelin/contracts/utils/types/Time.sol";

import {IOperatorsRegistryV1} from "../interfaces/IOperatorsRegistryV1.sol";
import {IBoltRestakingMiddlewareV1} from "../interfaces/IBoltRestakingMiddlewareV1.sol";
import {OperatorsLibV1} from "../lib/OperatorsLibV1.sol";

/// @title OperatorsRegistryV1
/// @notice A smart contract to store and manage Bolt operators
contract OperatorsRegistryV1 is OwnableUpgradeable, UUPSUpgradeable, IOperatorsRegistryV1 {
using OperatorsLibV1 for OperatorsLibV1.OperatorMap;

/// @notice The start timestamp of the contract, used as reference for time-based operations
uint48 public START_TIMESTAMP;

/// @notice The duration of an epoch in seconds, used for delaying opt-in/out operations
uint48 public EPOCH_DURATION;

/// @notice The set of bolt operators, indexed by their signer address
OperatorsLibV1.OperatorMap private OPERATORS;

/// @notice the address of the EigenLayer restaking middleware
address public EIGENLAYER_RESTAKING_MIDDLEWARE;
IBoltRestakingMiddlewareV1 public EIGENLAYER_RESTAKING_MIDDLEWARE;

/// @notice The address of the Symbiotic restaking middleware
address public SYMBIOTIC_RESTAKING_MIDDLEWARE;
IBoltRestakingMiddlewareV1 public SYMBIOTIC_RESTAKING_MIDDLEWARE;

/**
* @dev This empty reserved space is put in place to allow future versions to add new
Expand All @@ -29,16 +37,17 @@ contract OperatorsRegistryV1 is OwnableUpgradeable, UUPSUpgradeable, IOperatorsR
*
* Total storage slots: 50
*/
uint256[45] private __gap;
uint256[43] private __gap;

// ========= Initializer & Proxy functionality ========= //

/// @notice Initialize the contract
/// @param owner The address of the owner
function initialize(
address owner
) public initializer {
function initialize(address owner, uint48 epochDuration) public initializer {
__Ownable_init(owner);

START_TIMESTAMP = uint48(block.timestamp);
EPOCH_DURATION = epochDuration;
}

/// @notice Upgrade the contract
Expand All @@ -59,15 +68,23 @@ contract OperatorsRegistryV1 is OwnableUpgradeable, UUPSUpgradeable, IOperatorsR
_;
}

// ========= Public helpers ========= //

/// @notice Returns the timestamp of when the current epoch started
function getCurrentEpochStartTimestamp() public view returns (uint48) {
uint48 currentEpoch = (Time.timestamp() - START_TIMESTAMP) / EPOCH_DURATION;
return START_TIMESTAMP + currentEpoch * EPOCH_DURATION;
}

// ========= Operators functions ========= //
//
// The operator lifecycle looks as follows:
// 1. Register, and become active immediately. The operator can then manage their
// restaking positions through the EL AllocationManager contract.
// 2. Pause, and become inactive (with a delay). The operator won't be slashable anymore,
// restaking positions through the restaking protocol.
// 2. Pause, and become inactive. After a delay, the operator won't be slashable anymore,
// but they can still manage and rebalance their positions.
// 3. Unpause, and become active again (with a delay). The operator can be slashed again.
// 4. Deregister, and become inactive (with a delay). The operator won't be part of the AVS anymore.
// 3. Unpause, and become active again. After a delay, the operator can be slashed again.
// 4. Deregister, and become inactive. After a delay, the operator won't be part of the AVS anymore.

/// @notice Register an operator in the registry
/// @param signer The address of the operator
Expand Down Expand Up @@ -151,8 +168,11 @@ contract OperatorsRegistryV1 is OwnableUpgradeable, UUPSUpgradeable, IOperatorsR
/// @notice Update the address of a restaking middleware contract address
/// @param restakingProtocol The name of the restaking protocol
/// @param newMiddleware The address of the new restaking middleware
function updateRestakingMiddleware(string calldata restakingProtocol, address newMiddleware) public onlyOwner {
require(newMiddleware != address(0), "Invalid middleware address");
function updateRestakingMiddleware(
string calldata restakingProtocol,
IBoltRestakingMiddlewareV1 newMiddleware
) public onlyOwner {
require(address(newMiddleware) != address(0), "Invalid middleware address");

bytes32 protocolNameHash = keccak256(abi.encodePacked(restakingProtocol));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity ^0.8.27;
/// @title IBoltRestakingMiddlewareV1
/// @notice An interface for generalized restaking protocol middlewares in Bolt
interface IBoltRestakingMiddlewareV1 {
function NAME_HASH() external view returns (bytes32);

function getOperatorCollaterals(
address operator
) external view returns (address[] memory, uint256[] memory);
Expand Down
29 changes: 29 additions & 0 deletions smart-contracts/src/holesky/interfaces/IOperatorsRegistryV1.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {IBoltRestakingMiddlewareV1} from "./IBoltRestakingMiddlewareV1.sol";

/// @title IOperatorsRegistryV1
/// @notice An interface for the OperatorsRegistryV1 contract
interface IOperatorsRegistryV1 {
Expand All @@ -25,6 +27,18 @@ interface IOperatorsRegistryV1 {
/// @param restakingMiddleware The address of the restaking middleware
event OperatorUnpaused(address signer, address restakingMiddleware);

/// @notice Returns the start timestamp of the registry contract
function START_TIMESTAMP() external view returns (uint48);

/// @notice Returns the duration of an epoch in seconds
function EPOCH_DURATION() external view returns (uint48);

/// @notice Returns the address of the EigenLayer restaking middleware
function EIGENLAYER_RESTAKING_MIDDLEWARE() external view returns (IBoltRestakingMiddlewareV1);

/// @notice Returns the address of the Symbiotic restaking middleware
function SYMBIOTIC_RESTAKING_MIDDLEWARE() external view returns (IBoltRestakingMiddlewareV1);

/// @notice Register an operator in the registry
/// @param signer The address of the operator
/// @param rpcEndpoint The rpc endpoint of the operator
Expand All @@ -35,4 +49,19 @@ interface IOperatorsRegistryV1 {
function deregisterOperator(
address signer
) external;

/// @notice Pause an operator in the registry
/// @param signer The address of the operator
function pauseOperator(
address signer
) external;

/// @notice Unpause an operator in the registry, marking them as "active"
/// @param signer The address of the operator
function unpauseOperator(
address signer
) external;

/// @notice Returns the timestamp of when the current epoch started
function getCurrentEpochStartTimestamp() external view returns (uint48);
}
Loading

0 comments on commit be9c436

Please sign in to comment.