From e3ddc2c1cf94323ba3999f23f121e686725c40f4 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:35:19 +0100 Subject: [PATCH 1/5] chore: add natspec, add operator data view methods --- .../src/contracts/BoltManagerV3.sol | 353 ++++++++++++++++++ .../src/interfaces/IBoltManagerv3.sol | 65 ++++ 2 files changed, 418 insertions(+) create mode 100644 bolt-contracts/src/contracts/BoltManagerV3.sol create mode 100644 bolt-contracts/src/interfaces/IBoltManagerv3.sol diff --git a/bolt-contracts/src/contracts/BoltManagerV3.sol b/bolt-contracts/src/contracts/BoltManagerV3.sol new file mode 100644 index 000000000..d0ced4dd5 --- /dev/null +++ b/bolt-contracts/src/contracts/BoltManagerV3.sol @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +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 {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {OperatorMapWithTimeV2} from "../lib/OperatorMapWithTimeV2.sol"; +import {EnumerableMapV2} from "../lib/EnumerableMapV2.sol"; +import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol"; +import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol"; +import {IBoltValidatorsV2} from "../interfaces/IBoltValidatorsV2.sol"; +import {IBoltManagerV3} from "../interfaces/IBoltManagerV3.sol"; + +/// @title Bolt Manager +/// @notice The Bolt Manager contract is responsible for managing operators & restaking middlewares, and is the +/// entrypoint contract for all Bolt-related queries for off-chain consumers. +/// @dev This contract is upgradeable using the UUPSProxy pattern. Storage layout remains fixed across upgrades +/// with the use of storage gaps. +/// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps +/// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit. +/// You can also validate manually with forge: forge inspect storage-layout --pretty +contract BoltManagerV2 is IBoltManagerV2, OwnableUpgradeable, UUPSUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableMapV2 for EnumerableMapV2.OperatorMap; + using OperatorMapWithTimeV2 for EnumerableMapV2.OperatorMap; + + // ========= STORAGE ========= + /// @notice Start timestamp of the first epoch. + uint48 public START_TIMESTAMP; + + /// @notice Bolt Parameters contract. + IBoltParametersV1 public parameters; + + /// @notice Validators registry, where validators are registered via their + /// BLS pubkey and are assigned a sequence number. + IBoltValidatorsV2 public validators; + + /// @notice Set of operator addresses that have opted in to Bolt Protocol. + EnumerableMapV2.OperatorMap private operators; + + /// @notice Set of restaking protocols supported. Each address corresponds to the + /// associated Bolt Middleware contract. + EnumerableSet.AddressSet private restakingProtocols; + + // --> Storage layout marker: 7 slots + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + * This can be validated with the Openzeppelin Foundry Upgrades toolkit. + * + * Total storage slots: 50 + */ + uint256[43] private __gap; + + /// @notice Reverts if the caller is not a registered middleware contract. + modifier onlyMiddleware() { + if (!restakingProtocols.contains(msg.sender)) { + revert UnauthorizedMiddleware(); + } + _; + } + + // ========= INITIALIZER & PROXY FUNCTIONALITY ========== // + + /// @notice The initializer for the BoltManagerV1 contract. + /// @param _parameters The address of the parameters contract. + /// @param _validators The address of the validators registry. + function initialize(address _owner, address _parameters, address _validators) public initializer { + __Ownable_init(_owner); + + parameters = IBoltParametersV1(_parameters); + validators = IBoltValidatorsV2(_validators); + + START_TIMESTAMP = Time.timestamp(); + } + + /// @notice The reinitializer for the BoltManagerV2 contract. + /// @param _parameters The address of the parameters contract. + /// @param _validators The address of the validators registry. + function initializeV2(address _owner, address _parameters, address _validators) public reinitializer(2) { + __Ownable_init(_owner); + + parameters = IBoltParametersV1(_parameters); + validators = IBoltValidatorsV2(_validators); + + START_TIMESTAMP = Time.timestamp(); + } + + /// @notice The reinitializer V3 for the BoltManagerV3 contract. + /// @param _parameters The address of the parameters contract. + /// @param _validators The address of the validators registry. + function initializeV3(address _owner, address _parameters, address _validators) public reinitializer(3) { + __Ownable_init(_owner); + + parameters = IBoltParametersV1(_parameters); + validators = IBoltValidatorsV2(_validators); + + START_TIMESTAMP = Time.timestamp(); + } + + /// @notice Upgrade the implementation of the contract. + /// @param newImplementation The address of the new implementation. + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + // ========= VIEW FUNCTIONS ========= + + /// @notice Get the start timestamp of a given epoch. + /// @param epoch The epoch to get the start timestamp for. + /// @return timestamp The start timestamp of the given epoch. + function getEpochStartTs( + uint48 epoch + ) public view returns (uint48 timestamp) { + return START_TIMESTAMP + epoch * parameters.EPOCH_DURATION(); + } + + /// @notice Get the epoch at a given timestamp. + /// @param timestamp The timestamp to get the epoch for. + /// @return epoch The epoch at the given timestamp. + function getEpochAtTs( + uint48 timestamp + ) public view returns (uint48 epoch) { + return (timestamp - START_TIMESTAMP) / parameters.EPOCH_DURATION(); + } + + /// @notice Get the current epoch. + /// @return epoch The current epoch. + function getCurrentEpoch() public view returns (uint48 epoch) { + return getEpochAtTs(Time.timestamp()); + } + + /// @notice Check if an operator address is authorized to work for a validator, + /// given the validator's pubkey hash. This function performs a lookup in the + /// validators registry to check if they explicitly authorized the operator. + /// @param operator The operator address to check the authorization for. + /// @param pubkeyHash The pubkey hash of the validator to check the authorization for. + /// @return True if the operator is authorized, false otherwise. + function isOperatorAuthorizedForValidator(address operator, bytes20 pubkeyHash) public view returns (bool) { + if (operator == address(0) || pubkeyHash == bytes20(0)) { + revert InvalidQuery(); + } + + return validators.getValidatorByPubkeyHash(pubkeyHash).authorizedOperator == operator; + } + + /// @notice Returns the addresses of the middleware contracts of restaking protocols supported by Bolt. + /// @return middlewares The addresses of the supported restaking protocol middlewares. + function getSupportedRestakingProtocols() public view returns (address[] memory middlewares) { + return restakingProtocols.values(); + } + + /// @notice Returns whether an operator is registered with Bolt. + /// @param operator The operator address to check the registration for. + /// @return True if the operator is registered, false otherwise. + function isOperator( + address operator + ) public view returns (bool) { + return operators.contains(operator); + } + + /// @notice Get the data of a registered operator. + /// @param operator The operator address to get the data for. + /// @return operatorData The operator data. + function getOperatorData( + address operator + ) public view returns (EnumerableMapV2.Operator memory operatorData) { + return operators.get(operator); + } + + /// @notice Get the data of all registered operators. + /// @return operatorData An array of operator data. + function getAllOperatorsData() public view returns (EnumerableMapV2.Operator[] memory operatorData) { + operatorData = new EnumerableMapV2.Operator[](operators.length()); + for (uint256 i = 0; i < operators.length(); ++i) { + (address operator, EnumerableMapV2.Operator memory data) = operators.at(i); + operatorData[i] = data; + } + } + + /// @notice Get the status of multiple proposers, given their pubkey hashes. + /// @param pubkeyHashes The pubkey hashes of the proposers to get the status for. + /// @return statuses The statuses of the proposers, including their operator and active stake. + function getProposerStatuses( + bytes20[] calldata pubkeyHashes + ) public view returns (ProposerStatus[] memory statuses) { + statuses = new ProposerStatus[](pubkeyHashes.length); + for (uint256 i = 0; i < pubkeyHashes.length; ++i) { + statuses[i] = getProposerStatus(pubkeyHashes[i]); + } + } + + /// @notice Get the status of a proposer, given their pubkey hash. + /// @param pubkeyHash The pubkey hash of the proposer to get the status for. + /// @return status The status of the proposer, including their operator and active stake. + function getProposerStatus( + bytes20 pubkeyHash + ) public view returns (ProposerStatus memory status) { + if (pubkeyHash == bytes20(0)) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); + // NOTE: this will revert when the proposer does not exist. + IBoltValidatorsV2.ValidatorInfo memory validator = validators.getValidatorByPubkeyHash(pubkeyHash); + + EnumerableMapV2.Operator memory operatorData = operators.get(validator.authorizedOperator); + + status.pubkeyHash = pubkeyHash; + status.operator = validator.authorizedOperator; + status.operatorRPC = operatorData.rpc; + + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(validator.authorizedOperator); + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + return status; + } + + (status.collaterals, status.amounts) = + IBoltMiddlewareV1(operatorData.middleware).getOperatorCollaterals(validator.authorizedOperator); + + // NOTE: check if the sum of the collaterals covers the minimum operator stake required. + + uint256 totalOperatorStake = 0; + for (uint256 i = 0; i < status.amounts.length; ++i) { + totalOperatorStake += status.amounts[i]; + } + + if (totalOperatorStake < parameters.MINIMUM_OPERATOR_STAKE()) { + status.active = false; + } else { + status.active = true; + } + + return status; + } + + /// @notice Get the amount staked by an operator for a given collateral asset. + /// @param operator The operator address to get the stake for. + /// @param collateral The address of the collateral asset to get the stake for. + /// @return amount The amount staked by the operator for the given collateral asset. + function getOperatorStake(address operator, address collateral) public view returns (uint256) { + EnumerableMapV2.Operator memory operatorData = operators.get(operator); + + return IBoltMiddlewareV1(operatorData.middleware).getOperatorStake(operator, collateral); + } + + /// @notice Get the total amount staked of a given collateral asset. + /// @param collateral The address of the collateral asset to get the total stake for. + /// @return amount The total amount staked of the given collateral asset. + function getTotalStake( + address collateral + ) public view returns (uint256 amount) { + // Loop over all of the operators, get their middleware, and retrieve their staked amount. + for (uint256 i = 0; i < operators.length(); ++i) { + (address operator, EnumerableMapV2.Operator memory operatorData) = operators.at(i); + amount += IBoltMiddlewareV1(operatorData.middleware).getOperatorStake(operator, collateral); + } + + return amount; + } + + // ========= OPERATOR FUNCTIONS ====== // + + /// @notice Registers an operator with Bolt. Only callable by a supported middleware contract. + /// @param operatorAddr The operator address to register. + /// @param rpc The RPC endpoint of the operator. + function registerOperator(address operatorAddr, string calldata rpc) external onlyMiddleware { + if (operators.contains(operatorAddr)) { + revert OperatorAlreadyRegistered(); + } + + // Create an already enabled operator + EnumerableMapV2.Operator memory operator = EnumerableMapV2.Operator(rpc, msg.sender, Time.timestamp()); + + operators.set(operatorAddr, operator); + } + + /// @notice De-registers an operator from Bolt. Only callable by a supported middleware contract. + /// @param operator The operator address to deregister. + function deregisterOperator( + address operator + ) public onlyMiddleware { + operators.remove(operator); + } + + /// @notice Allow an operator to signal indefinite opt-out from Bolt Protocol. + /// @dev Pausing activity does not prevent the operator from being slashable for + /// the current network epoch until the end of the slashing window. + /// @param operator The operator address to pause. + function pauseOperator( + address operator + ) external onlyMiddleware { + // SAFETY: This will revert if the operator key is not present. + operators.disable(operator); + } + + /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. + /// @param operator The operator address to unpause. + function unpauseOperator( + address operator + ) external onlyMiddleware { + // SAFETY: This will revert if the operator key is not present. + operators.enable(operator); + } + + /// @notice Check if an operator is currently enabled to work in Bolt Protocol. + /// @param operator The operator address to check the enabled status for. + /// @return True if the operator is enabled, false otherwise. + function isOperatorEnabled( + address operator + ) public view returns (bool) { + if (!operators.contains(operator)) { + revert OperatorNotRegistered(); + } + + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + return enabledTime != 0 && disabledTime == 0; + } + + // ========= ADMIN FUNCTIONS ========= // + + /// @notice Add a restaking protocol into Bolt + /// @param protocolMiddleware The address of the restaking protocol Bolt middleware + function addRestakingProtocol( + address protocolMiddleware + ) public onlyOwner { + restakingProtocols.add(protocolMiddleware); + } + + /// @notice Remove a restaking protocol from Bolt + /// @param protocolMiddleware The address of the restaking protocol Bolt middleware + function removeRestakingProtocol( + address protocolMiddleware + ) public onlyOwner { + restakingProtocols.remove(protocolMiddleware); + } + + // ========= HELPER FUNCTIONS ========= + + /// @notice Check if a map entry was active at a given timestamp. + /// @param enabledTime The enabled time of the map entry. + /// @param disabledTime 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 enabledTime, uint48 disabledTime, uint48 timestamp) private pure returns (bool) { + return enabledTime != 0 && enabledTime <= timestamp && (disabledTime == 0 || disabledTime >= timestamp); + } +} diff --git a/bolt-contracts/src/interfaces/IBoltManagerv3.sol b/bolt-contracts/src/interfaces/IBoltManagerv3.sol new file mode 100644 index 000000000..d3d27ea02 --- /dev/null +++ b/bolt-contracts/src/interfaces/IBoltManagerv3.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {EnumerableMapV2} from "../lib/EnumerableMapV2.sol"; + +interface IBoltManagerV2 { + error InvalidQuery(); + error OperatorAlreadyRegistered(); + error OperatorNotRegistered(); + error UnauthorizedMiddleware(); + // TODO: remove in future upgrade (unused) + error InactiveOperator(); + + /// @notice Proposer status info. + struct ProposerStatus { + // The pubkey hash of the validator. + bytes20 pubkeyHash; + // Whether the corresponding operator is active based on collateral requirements. + bool active; + // The operator address that is authorized to make & sign commitments on behalf of the validator. + address operator; + // The operator RPC endpoint. + string operatorRPC; + // The addresses of the collateral tokens. + address[] collaterals; + // The corresponding amounts of the collateral tokens. + uint256[] amounts; + } + + function registerOperator(address operator, string calldata rpc) external; + + function deregisterOperator( + address operator + ) external; + + function pauseOperator( + address operator + ) external; + + function unpauseOperator( + address operator + ) external; + + function isOperator( + address operator + ) external view returns (bool); + + function getOperatorData( + address operator + ) public view returns (EnumerableMapV2.Operator memory operatorData); + + function getAllOperatorsData() public view returns (EnumerableMapV2.Operator[] memory operatorData); + + function getProposerStatus( + bytes20 pubkeyHash + ) external view returns (ProposerStatus memory status); + + function getProposerStatuses( + bytes20[] calldata pubkeyHashes + ) external view returns (ProposerStatus[] memory statuses); + + function isOperatorAuthorizedForValidator(address operator, bytes20 pubkeyHash) external view returns (bool); + + function getSupportedRestakingProtocols() external view returns (address[] memory middlewares); +} From 02b235f61bd6cefea596632d4c6d126918a35b3b Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:56:03 +0100 Subject: [PATCH 2/5] chore: nits --- bolt-contracts/src/contracts/BoltManagerV3.sol | 2 +- bolt-contracts/test/BoltManager.Symbiotic.t.sol | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bolt-contracts/src/contracts/BoltManagerV3.sol b/bolt-contracts/src/contracts/BoltManagerV3.sol index d0ced4dd5..2167cbf57 100644 --- a/bolt-contracts/src/contracts/BoltManagerV3.sol +++ b/bolt-contracts/src/contracts/BoltManagerV3.sol @@ -22,7 +22,7 @@ import {IBoltManagerV3} from "../interfaces/IBoltManagerV3.sol"; /// See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps /// To validate the storage layout, use the Openzeppelin Foundry Upgrades toolkit. /// You can also validate manually with forge: forge inspect storage-layout --pretty -contract BoltManagerV2 is IBoltManagerV2, OwnableUpgradeable, UUPSUpgradeable { +contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableMapV2 for EnumerableMapV2.OperatorMap; using OperatorMapWithTimeV2 for EnumerableMapV2.OperatorMap; diff --git a/bolt-contracts/test/BoltManager.Symbiotic.t.sol b/bolt-contracts/test/BoltManager.Symbiotic.t.sol index 80b22f978..096bf4f56 100644 --- a/bolt-contracts/test/BoltManager.Symbiotic.t.sol +++ b/bolt-contracts/test/BoltManager.Symbiotic.t.sol @@ -375,4 +375,12 @@ contract BoltManagerSymbioticTest is Test { vm.expectRevert(abi.encodeWithSelector(ValidatorsLib.ValidatorDoesNotExist.selector, pubkeyHash)); manager.getProposerStatus(pubkeyHash); } + + function testCalculateSubnetwork() public { + address network_ = 0xb017002D8024d8c8870A5CECeFCc63887650D2a4; + uint96 identifier_ = 0; + + bytes32 subnetwork_ = network_.subnetwork(identifier_); + assertEq(subnetwork_, 0xb017002D8024d8c8870A5CECeFCc63887650D2a4000000000000000000000000); + } } From 91669135cbb407c5b687229d5eda8773e1c95a22 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:10:50 +0100 Subject: [PATCH 3/5] fix: forge tests, external methods --- bolt-contracts/src/contracts/BoltManagerV3.sol | 1 + bolt-contracts/src/interfaces/IBoltManagerv3.sol | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bolt-contracts/src/contracts/BoltManagerV3.sol b/bolt-contracts/src/contracts/BoltManagerV3.sol index 2167cbf57..662bb9aed 100644 --- a/bolt-contracts/src/contracts/BoltManagerV3.sol +++ b/bolt-contracts/src/contracts/BoltManagerV3.sol @@ -28,6 +28,7 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { using OperatorMapWithTimeV2 for EnumerableMapV2.OperatorMap; // ========= STORAGE ========= + /// @notice Start timestamp of the first epoch. uint48 public START_TIMESTAMP; diff --git a/bolt-contracts/src/interfaces/IBoltManagerv3.sol b/bolt-contracts/src/interfaces/IBoltManagerv3.sol index d3d27ea02..f0a9dad9f 100644 --- a/bolt-contracts/src/interfaces/IBoltManagerv3.sol +++ b/bolt-contracts/src/interfaces/IBoltManagerv3.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.25; import {EnumerableMapV2} from "../lib/EnumerableMapV2.sol"; -interface IBoltManagerV2 { +interface IBoltManagerV3 { error InvalidQuery(); error OperatorAlreadyRegistered(); error OperatorNotRegistered(); @@ -47,9 +47,9 @@ interface IBoltManagerV2 { function getOperatorData( address operator - ) public view returns (EnumerableMapV2.Operator memory operatorData); + ) external view returns (EnumerableMapV2.Operator memory operatorData); - function getAllOperatorsData() public view returns (EnumerableMapV2.Operator[] memory operatorData); + function getAllOperatorsData() external view returns (EnumerableMapV2.Operator[] memory operatorData); function getProposerStatus( bytes20 pubkeyHash From b1f10ea3d245e9d54dbcb84ed618f54164f8c124 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:43:05 +0100 Subject: [PATCH 4/5] feat: update rpc function --- .../src/contracts/BoltManagerV3.sol | 12 ++++ bolt-contracts/src/lib/EnumerableMapV3.sol | 71 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 bolt-contracts/src/lib/EnumerableMapV3.sol diff --git a/bolt-contracts/src/contracts/BoltManagerV3.sol b/bolt-contracts/src/contracts/BoltManagerV3.sol index 662bb9aed..10aaf7770 100644 --- a/bolt-contracts/src/contracts/BoltManagerV3.sol +++ b/bolt-contracts/src/contracts/BoltManagerV3.sol @@ -281,6 +281,18 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { operators.set(operatorAddr, operator); } + function updateOperatorRPC(address operatorAddr, string calldata rpc) external onlyMiddleware { + if (!operators.contains(operatorAddr)) { + revert OperatorNotRegistered(); + } + + if (operators.get(operatorAddr).middleware != msg.sender) { + revert UnauthorizedMiddleware(); + } + + operators.get(operatorAddr).rpc = rpc; + } + /// @notice De-registers an operator from Bolt. Only callable by a supported middleware contract. /// @param operator The operator address to deregister. function deregisterOperator( diff --git a/bolt-contracts/src/lib/EnumerableMapV3.sol b/bolt-contracts/src/lib/EnumerableMapV3.sol new file mode 100644 index 000000000..5c9d79343 --- /dev/null +++ b/bolt-contracts/src/lib/EnumerableMapV3.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +library EnumerableMapV2 { + using EnumerableSet for EnumerableSet.Bytes32Set; + + error KeyNotFound(address key); + + struct Operator { + // RPC endpoint + string rpc; + // Middleware contract address + address middleware; + // Timestamp of registration + uint256 timestamp; + } + + struct OperatorMap { + // Storage of keys + EnumerableSet.Bytes32Set _keys; + mapping(bytes32 key => Operator) _values; + } + + function set(OperatorMap storage self, address key, Operator memory value) internal returns (bool) { + bytes32 keyBytes = bytes32(uint256(uint160(key))); + self._values[keyBytes] = value; + return self._keys.add(keyBytes); + } + + function remove(OperatorMap storage self, address key) internal returns (bool) { + bytes32 keyBytes = bytes32(uint256(uint160(key))); + delete self._values[keyBytes]; + return self._keys.remove(keyBytes); + } + + function contains(OperatorMap storage self, address key) internal view returns (bool) { + return self._keys.contains(bytes32(uint256(uint160(key)))); + } + + function length( + OperatorMap storage self + ) internal view returns (uint256) { + return self._keys.length(); + } + + function at(OperatorMap storage self, uint256 index) internal view returns (address, Operator memory) { + bytes32 key = self._keys.at(index); + return (address(uint160(uint256(key))), self._values[key]); + } + + function get(OperatorMap storage self, address key) internal view returns (Operator memory) { + if (!contains(self, key)) { + revert KeyNotFound(); + } + + return self._values[bytes32(uint256(uint160(key)))]; + } + + function keys( + OperatorMap storage self + ) internal view returns (address[] memory) { + address[] memory result = new address[](self._keys.length()); + for (uint256 i = 0; i < self._keys.length(); i++) { + result[i] = address(uint160(uint256(self._keys.at(i)))); + } + + return result; + } +} From ae433030a5119b86c4316986c3d07ec7688a6238 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:51:00 +0100 Subject: [PATCH 5/5] chore: map with time v3 --- .../src/contracts/BoltManagerV3.sol | 28 ++++---- .../src/interfaces/IBoltManagerv3.sol | 8 +-- bolt-contracts/src/lib/EnumerableMapV3.sol | 4 +- .../src/lib/OperatorMapWithTimeV3.sol | 70 +++++++++++++++++++ 4 files changed, 89 insertions(+), 21 deletions(-) create mode 100644 bolt-contracts/src/lib/OperatorMapWithTimeV3.sol diff --git a/bolt-contracts/src/contracts/BoltManagerV3.sol b/bolt-contracts/src/contracts/BoltManagerV3.sol index 10aaf7770..59ff2aa90 100644 --- a/bolt-contracts/src/contracts/BoltManagerV3.sol +++ b/bolt-contracts/src/contracts/BoltManagerV3.sol @@ -7,8 +7,8 @@ import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {OperatorMapWithTimeV2} from "../lib/OperatorMapWithTimeV2.sol"; -import {EnumerableMapV2} from "../lib/EnumerableMapV2.sol"; +import {OperatorMapWithTimeV3} from "../lib/OperatorMapWithTimeV3.sol"; +import {EnumerableMapV3} from "../lib/EnumerableMapV3.sol"; import {IBoltParametersV1} from "../interfaces/IBoltParametersV1.sol"; import {IBoltMiddlewareV1} from "../interfaces/IBoltMiddlewareV1.sol"; import {IBoltValidatorsV2} from "../interfaces/IBoltValidatorsV2.sol"; @@ -24,11 +24,11 @@ import {IBoltManagerV3} from "../interfaces/IBoltManagerV3.sol"; /// You can also validate manually with forge: forge inspect storage-layout --pretty contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { using EnumerableSet for EnumerableSet.AddressSet; - using EnumerableMapV2 for EnumerableMapV2.OperatorMap; - using OperatorMapWithTimeV2 for EnumerableMapV2.OperatorMap; + using EnumerableMapV3 for EnumerableMapV3.OperatorMap; + using OperatorMapWithTimeV3 for EnumerableMapV3.OperatorMap; // ========= STORAGE ========= - + /// @notice Start timestamp of the first epoch. uint48 public START_TIMESTAMP; @@ -40,7 +40,7 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { IBoltValidatorsV2 public validators; /// @notice Set of operator addresses that have opted in to Bolt Protocol. - EnumerableMapV2.OperatorMap private operators; + EnumerableMapV3.OperatorMap private operators; /// @notice Set of restaking protocols supported. Each address corresponds to the /// associated Bolt Middleware contract. @@ -170,16 +170,16 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { /// @return operatorData The operator data. function getOperatorData( address operator - ) public view returns (EnumerableMapV2.Operator memory operatorData) { + ) public view returns (EnumerableMapV3.Operator memory operatorData) { return operators.get(operator); } /// @notice Get the data of all registered operators. /// @return operatorData An array of operator data. - function getAllOperatorsData() public view returns (EnumerableMapV2.Operator[] memory operatorData) { - operatorData = new EnumerableMapV2.Operator[](operators.length()); + function getAllOperatorsData() public view returns (EnumerableMapV3.Operator[] memory operatorData) { + operatorData = new EnumerableMapV3.Operator[](operators.length()); for (uint256 i = 0; i < operators.length(); ++i) { - (address operator, EnumerableMapV2.Operator memory data) = operators.at(i); + (address operator, EnumerableMapV3.Operator memory data) = operators.at(i); operatorData[i] = data; } } @@ -210,7 +210,7 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { // NOTE: this will revert when the proposer does not exist. IBoltValidatorsV2.ValidatorInfo memory validator = validators.getValidatorByPubkeyHash(pubkeyHash); - EnumerableMapV2.Operator memory operatorData = operators.get(validator.authorizedOperator); + EnumerableMapV3.Operator memory operatorData = operators.get(validator.authorizedOperator); status.pubkeyHash = pubkeyHash; status.operator = validator.authorizedOperator; @@ -245,7 +245,7 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { /// @param collateral The address of the collateral asset to get the stake for. /// @return amount The amount staked by the operator for the given collateral asset. function getOperatorStake(address operator, address collateral) public view returns (uint256) { - EnumerableMapV2.Operator memory operatorData = operators.get(operator); + EnumerableMapV3.Operator memory operatorData = operators.get(operator); return IBoltMiddlewareV1(operatorData.middleware).getOperatorStake(operator, collateral); } @@ -258,7 +258,7 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { ) public view returns (uint256 amount) { // Loop over all of the operators, get their middleware, and retrieve their staked amount. for (uint256 i = 0; i < operators.length(); ++i) { - (address operator, EnumerableMapV2.Operator memory operatorData) = operators.at(i); + (address operator, EnumerableMapV3.Operator memory operatorData) = operators.at(i); amount += IBoltMiddlewareV1(operatorData.middleware).getOperatorStake(operator, collateral); } @@ -276,7 +276,7 @@ contract BoltManagerV3 is IBoltManagerV3, OwnableUpgradeable, UUPSUpgradeable { } // Create an already enabled operator - EnumerableMapV2.Operator memory operator = EnumerableMapV2.Operator(rpc, msg.sender, Time.timestamp()); + EnumerableMapV3.Operator memory operator = EnumerableMapV3.Operator(rpc, msg.sender, Time.timestamp()); operators.set(operatorAddr, operator); } diff --git a/bolt-contracts/src/interfaces/IBoltManagerv3.sol b/bolt-contracts/src/interfaces/IBoltManagerv3.sol index f0a9dad9f..9499149f7 100644 --- a/bolt-contracts/src/interfaces/IBoltManagerv3.sol +++ b/bolt-contracts/src/interfaces/IBoltManagerv3.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {EnumerableMapV2} from "../lib/EnumerableMapV2.sol"; +import {EnumerableMapV3} from "../lib/EnumerableMapV3.sol"; interface IBoltManagerV3 { error InvalidQuery(); error OperatorAlreadyRegistered(); error OperatorNotRegistered(); error UnauthorizedMiddleware(); - // TODO: remove in future upgrade (unused) - error InactiveOperator(); /// @notice Proposer status info. struct ProposerStatus { @@ -47,9 +45,9 @@ interface IBoltManagerV3 { function getOperatorData( address operator - ) external view returns (EnumerableMapV2.Operator memory operatorData); + ) external view returns (EnumerableMapV3.Operator memory operatorData); - function getAllOperatorsData() external view returns (EnumerableMapV2.Operator[] memory operatorData); + function getAllOperatorsData() external view returns (EnumerableMapV3.Operator[] memory operatorData); function getProposerStatus( bytes20 pubkeyHash diff --git a/bolt-contracts/src/lib/EnumerableMapV3.sol b/bolt-contracts/src/lib/EnumerableMapV3.sol index 5c9d79343..2985ec087 100644 --- a/bolt-contracts/src/lib/EnumerableMapV3.sol +++ b/bolt-contracts/src/lib/EnumerableMapV3.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.25; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -library EnumerableMapV2 { +library EnumerableMapV3 { using EnumerableSet for EnumerableSet.Bytes32Set; error KeyNotFound(address key); @@ -52,7 +52,7 @@ library EnumerableMapV2 { function get(OperatorMap storage self, address key) internal view returns (Operator memory) { if (!contains(self, key)) { - revert KeyNotFound(); + revert KeyNotFound(key); } return self._values[bytes32(uint256(uint160(key)))]; diff --git a/bolt-contracts/src/lib/OperatorMapWithTimeV3.sol b/bolt-contracts/src/lib/OperatorMapWithTimeV3.sol new file mode 100644 index 000000000..571f41d7f --- /dev/null +++ b/bolt-contracts/src/lib/OperatorMapWithTimeV3.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; + +import {EnumerableMapV3} from "./EnumerableMapV3.sol"; + +library OperatorMapWithTimeV3 { + using EnumerableMapV3 for EnumerableMapV3.OperatorMap; + + error AlreadyAdded(); + error NotEnabled(); + error AlreadyEnabled(); + + uint256 private constant ENABLED_TIME_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFF; + uint256 private constant DISABLED_TIME_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFF << 48; + + function add(EnumerableMapV3.OperatorMap storage self, address addr) internal { + if (!self.set(addr, EnumerableMapV3.Operator("", address(0), 0))) { + revert AlreadyAdded(); + } + } + + function disable(EnumerableMapV3.OperatorMap storage self, address addr) internal { + EnumerableMapV3.Operator memory operator = self.get(addr); + uint256 value = operator.timestamp; + + if (uint48(value) == 0 || uint48(value >> 48) != 0) { + revert NotEnabled(); + } + + value |= uint256(Time.timestamp()) << 48; + operator.timestamp = value; + self.set(addr, operator); + } + + function enable(EnumerableMapV3.OperatorMap storage self, address addr) internal { + EnumerableMapV3.Operator memory operator = self.get(addr); + uint256 value = operator.timestamp; + + if (uint48(value) != 0 && uint48(value >> 48) == 0) { + revert AlreadyEnabled(); + } + + value = uint256(Time.timestamp()); + operator.timestamp = value; + self.set(addr, operator); + } + + function atWithTimes( + EnumerableMapV3.OperatorMap storage self, + uint256 idx + ) internal view returns (address key, uint48 enabledTime, uint48 disabledTime) { + EnumerableMapV3.Operator memory value; + (key, value) = self.at(idx); + uint256 timestamp = value.timestamp; + enabledTime = uint48(timestamp); + disabledTime = uint48(timestamp >> 48); + } + + function getTimes( + EnumerableMapV3.OperatorMap storage self, + address addr + ) internal view returns (uint48 enabledTime, uint48 disabledTime) { + EnumerableMapV3.Operator memory value = self.get(addr); + enabledTime = uint48(value.timestamp); + disabledTime = uint48(value.timestamp >> 48); + } +}