diff --git a/bolt-contracts/README.md b/bolt-contracts/README.md index d6a6a7977..bed5a661b 100644 --- a/bolt-contracts/README.md +++ b/bolt-contracts/README.md @@ -48,10 +48,12 @@ coordination of validators, operators, and vaults within the Bolt network. Key features include: -1. Registration of Symbiotic Operators and Vaults / EigenLayer Operators and Strategies -2. Whitelisting of collateral assets used to back commitments -3. Retrieval of operator stake and proposer status from their pubkey -4. Integration with Symbiotic/EigenLayer +1. Retrieval of operator stake and proposer status from their pubkey +2. Integration with Symbiotic +3. Integration with Eigenlayer + +Specific functionalities about the restaking protocols are handled inside +the `IBoltMiddleware` contracts, such as `BoltSymbioticMiddleware` and `BoltEigenlayerMiddleware`. ### Symbiotic Integration guide for Staking Pools @@ -61,9 +63,9 @@ on how to spin up a Vault and start receiving stake from your node operators. Opting into Bolt works as any other Symbiotic middleware integration. Here are the steps: -1. Make sure your vault collateral is whitelisted in `BoltManager` by calling `isSymbioticCollateralWhitelisted`. -2. Register as a vault in `BoltManager` by calling `registerSymbioticVault`. -3. Verify that your vault is active in `BoltManager` by calling `isSymbioticVaultEnabled`. +1. Make sure your vault collateral is whitelisted in `BoltSymbioticMiddleware` by calling `isCollateralWhitelisted`. +2. Register as a vault in `BoltSymbioticMiddleware` by calling `registerVault`. +3. Verify that your vault is active in `BoltSymbioticMiddleware` by calling `isVaultEnabled`. 4. Set the network limit for your vault in Symbiotic with `Vault.delegator().setNetworkLimit()`. 5. You can now start approving operators that opt in to your vault directly in Symbiotic. 6. When you assign shares to operators, they are able to provide commitments on behalf of your collateral. @@ -78,7 +80,7 @@ The opt-in process requires the following steps: 1. register in Symbiotic with `OperatorRegistry.registerOperator()`. 2. opt-in to the Bolt network with `OperatorNetworkOptInService.optIn(networkAddress)`. 3. opt-in to any vault with `OperatorVaultOptInService.optIn(vaultAddress)`. -4. register in Bolt with `BoltManager.registerSymbioticOperator(operatorAddress)`. +4. register in Bolt with `BoltSymbioticMiddleware.registerOperator(operatorAddress)`. 5. get approved by the vault. 6. start providing commitments with the stake provided by the vault. diff --git a/bolt-contracts/script/DeployManager.s.sol b/bolt-contracts/script/Deploy.s.sol similarity index 57% rename from bolt-contracts/script/DeployManager.s.sol rename to bolt-contracts/script/Deploy.s.sol index 1de3932b9..ad54126f1 100644 --- a/bolt-contracts/script/DeployManager.s.sol +++ b/bolt-contracts/script/Deploy.s.sol @@ -5,9 +5,11 @@ import {Script, console} from "forge-std/Script.sol"; import {BoltValidators} from "../src/contracts/BoltValidators.sol"; import {BoltManager} from "../src/contracts/BoltManager.sol"; +import {BoltEigenLayerMiddleware} from "../src/contracts/BoltEigenLayerMiddleware.sol"; +import {BoltSymbioticMiddleware} from "../src/contracts/BoltSymbioticMiddleware.sol"; /// @notice Script to deploy the BoltManager and BoltValidators contracts. -contract DeployBoltManager is Script { +contract DeployBolt is Script { function run( address symbioticNetwork, address symbioticOperatorRegistry, @@ -24,19 +26,22 @@ contract DeployBoltManager is Script { BoltValidators validators = new BoltValidators(sender); console.log("BoltValidators deployed at", address(validators)); - BoltManager manager = new BoltManager( - address(sender), + BoltManager manager = new BoltManager(sender, address(validators)); + console.log("BoltManager deployed at", address(manager)); + + BoltEigenLayerMiddleware eigenLayerMiddleware = new BoltEigenLayerMiddleware( + sender, address(validators), eigenlayerAVSDirectory, eigenlayerDelegationManager, eigenlayerStrategyManager + ); + console.log("BoltEigenLayerMiddleware deployed at", address(eigenLayerMiddleware)); + BoltSymbioticMiddleware symbioticMiddleware = new BoltSymbioticMiddleware( + sender, address(validators), symbioticNetwork, symbioticOperatorRegistry, symbioticOperatorNetOptIn, - symbioticVaultRegistry, - eigenlayerAVSDirectory, - eigenlayerDelegationManager, - eigenlayerStrategyManager + symbioticVaultRegistry ); - console.log("BoltManager deployed at", address(manager)); - + console.log("BoltSymbioticMiddleware deployed at", address(eigenLayerMiddleware)); vm.stopBroadcast(); } } diff --git a/bolt-contracts/src/contracts/BoltEigenLayerMiddleware.sol b/bolt-contracts/src/contracts/BoltEigenLayerMiddleware.sol new file mode 100644 index 000000000..9759355d4 --- /dev/null +++ b/bolt-contracts/src/contracts/BoltEigenLayerMiddleware.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Time} from "lib/openzeppelin-contracts/contracts/utils/types/Time.sol"; +import {EnumerableMap} from "lib/openzeppelin-contracts/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +import {MapWithTimeData} from "../lib/MapWithTimeData.sol"; +import {IBoltValidators} from "../interfaces/IBoltValidators.sol"; +import {IBoltMiddleware} from "../interfaces/IBoltMiddleware.sol"; +import {IBoltManager} from "../interfaces/IBoltManager.sol"; + +import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; +import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; +import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol"; +import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; +import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; +import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectoryStorage.sol"; +import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol"; +import {StrategyManagerStorage} from "@eigenlayer/src/contracts/core/StrategyManagerStorage.sol"; + +contract BoltEigenLayerMiddleware is IBoltMiddleware, Ownable { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + + // ========= STORAGE ========= + + /// @notice Validators registry, where validators are registered via their + /// BLS pubkey and are assigned a sequence number. + IBoltManager public boltManager; + + /// @notice Set of EigenLayer operators addresses that have opted in to Bolt Protocol. + EnumerableMap.AddressToUintMap private operators; + + /// @notice Set of EigenLayer protocol strategies that are used in Bolt Protocol. + EnumerableMap.AddressToUintMap private strategies; + + /// @notice Set of EigenLayer collaterals addresses that are allowed. + EnumerableSet.AddressSet private whitelistedCollaterals; + + // ========= IMMUTABLES ========= + + /// @notice Address of the EigenLayer AVS Directory contract. + AVSDirectoryStorage public immutable AVS_DIRECTORY; + + /// @notice Address of the EigenLayer Delegation Manager contract. + DelegationManagerStorage public immutable DELEGATION_MANAGER; + + /// @notice Address of the EigenLayer Strategy Manager contract. + StrategyManagerStorage public immutable STRATEGY_MANAGER; + + /// @notice Start timestamp of the first epoch. + uint48 public immutable START_TIMESTAMP; + + // ========= CONSTANTS ========= + + /// @notice Duration of an epoch in seconds. + uint48 public constant EPOCH_DURATION = 1 days; + + /// @notice Duration of the slashing window in seconds. + uint48 public constant SLASHING_WINDOW = 7 days; + + /// @notice Name hash of the restaking protocol for identifying the instance of `IBoltMiddleware`. + bytes32 public constant NAME_HASH = keccak256("EIGENLAYER"); + + // ========= ERRORS ========= + + error StrategyNotAllowed(); + error OperatorNotRegisteredToAVS(); + + // ========= CONSTRUCTOR ========= + + /// @notice Constructor for the BoltEigenLayerMiddleware contract. + /// @param _boltManager The address of the Bolt Manager contract. + /// @param _eigenlayerAVSDirectory The address of the EigenLayer AVS Directory contract. + /// @param _eigenlayerDelegationManager The address of the EigenLayer Delegation Manager contract. + /// @param _eigenlayerStrategyManager The address of the EigenLayer Strategy Manager. + constructor( + address _owner, + address _boltManager, + address _eigenlayerAVSDirectory, + address _eigenlayerDelegationManager, + address _eigenlayerStrategyManager + ) Ownable(_owner) { + boltManager = IBoltManager(_boltManager); + START_TIMESTAMP = Time.timestamp(); + + AVS_DIRECTORY = AVSDirectoryStorage(_eigenlayerAVSDirectory); + DELEGATION_MANAGER = DelegationManagerStorage(_eigenlayerDelegationManager); + STRATEGY_MANAGER = StrategyManagerStorage(_eigenlayerStrategyManager); + } + + // ========= VIEW FUNCTIONS ========= + + /// @notice Get the start timestamp of an epoch. + function getEpochStartTs( + uint48 epoch + ) public view returns (uint48 timestamp) { + return START_TIMESTAMP + epoch * EPOCH_DURATION; + } + + /// @notice Get the epoch at a given timestamp. + function getEpochAtTs( + uint48 timestamp + ) public view returns (uint48 epoch) { + return (timestamp - START_TIMESTAMP) / EPOCH_DURATION; + } + + /// @notice Get 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, bytes32 pubkeyHash) public view returns (bool) { + if (operator == address(0) || pubkeyHash == bytes32(0)) { + revert InvalidQuery(); + } + + return boltManager.validators().getValidatorByPubkeyHash(pubkeyHash).authorizedOperator == operator; + } + + /// @notice Get the list of EigenLayer strategies addresses that are allowed. + /// @return _strategies The list of strategies addresses that are allowed. + function getWhitelistedCollaterals() public view returns (address[] memory _strategies) { + return whitelistedCollaterals.values(); + } + + /// @notice Check if an EigenLayer strategy address is allowed. + /// @param strategy The strategy address to check if it is allowed. + /// @return true if the strategy address is allowed, false otherwise. + function isCollateralWhitelisted( + address strategy + ) public view returns (bool) { + return whitelistedCollaterals.contains(strategy); + } + + // ========= ADMIN FUNCTIONS ========= + + /// @notice Add a collateral address to the whitelist. + /// @param collateral The collateral address to add to the whitelist. + function addWhitelistedCollateral( + address collateral + ) public onlyOwner { + whitelistedCollaterals.add(collateral); + } + + /// @notice Remove a collateral address from the whitelist. + /// @param collateral The collateral address to remove from the whitelist. + function removeWhitelistedCollateral( + address collateral + ) public onlyOwner { + whitelistedCollaterals.remove(collateral); + } + + // ========= EIGENLAYER MIDDLEWARE LOGIC ========= + + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// @param operator The operator address to signal opt-in for. + function registerOperator( + address operator + ) public { + if (operators.contains(operator)) { + revert AlreadyRegistered(); + } + + if (!DELEGATION_MANAGER.isOperator(operator)) { + revert NotOperator(); + } + + if (!checkIfOperatorRegisteredToAVS(operator)) { + revert OperatorNotRegisteredToAVS(); + } + + operators.add(operator); + operators.enable(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. + function pauseOperator() public { + if (!operators.contains(msg.sender)) { + revert NotRegistered(); + } + + operators.disable(msg.sender); + } + + /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. + function unpauseOperator() public { + if (!operators.contains(msg.sender)) { + revert NotRegistered(); + } + + operators.enable(msg.sender); + } + + /// @notice Register a strategy to work in Bolt Protocol. + /// @param strategy The EigenLayer strategy address + function registerStrategy( + address strategy + ) public { + if (strategies.contains(strategy)) { + revert AlreadyRegistered(); + } + + if (!STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy))) { + revert StrategyNotAllowed(); + } + + if (!isCollateralWhitelisted(address(IStrategy(strategy).underlyingToken()))) { + revert CollateralNotWhitelisted(); + } + + strategies.add(strategy); + strategies.enable(strategy); + } + + /// @notice Allow a strategy to signal indefinite opt-out from Bolt Protocol. + function pauseStrategy() public { + if (!strategies.contains(msg.sender)) { + revert NotRegistered(); + } + + strategies.disable(msg.sender); + } + + /// @notice Allow a disabled strategy to signal opt-in to Bolt Protocol. + function unpauseStrategy() public { + if (!strategies.contains(msg.sender)) { + revert NotRegistered(); + } + + strategies.enable(msg.sender); + } + + /// @notice Check if a strategy is currently enabled to work in Bolt Protocol. + /// @param strategy The strategy address to check the enabled status for. + /// @return True if the strategy is enabled, false otherwise. + function isStrategyEnabled( + address strategy + ) public view returns (bool) { + (uint48 enabledTime, uint48 disabledTime) = strategies.getTimes(strategy); + return enabledTime != 0 && disabledTime == 0; + } + + /// @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) { + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + return enabledTime != 0 && disabledTime == 0; + } + + /// @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 getProposersStatus( + bytes32[] memory pubkeyHashes + ) public view returns (IBoltValidators.ProposerStatus[] memory statuses) { + statuses = new IBoltValidators.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( + bytes32 pubkeyHash + ) public view returns (IBoltValidators.ProposerStatus memory status) { + if (pubkeyHash == bytes32(0)) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); + IBoltValidators.Validator memory validator = boltManager.validators().getValidatorByPubkeyHash(pubkeyHash); + + address operator = validator.authorizedOperator; + + status.pubkeyHash = pubkeyHash; + status.active = validator.exists; + status.operator = operator; + + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + return status; + } + + status.collaterals = new address[](strategies.length()); + status.amounts = new uint256[](strategies.length()); + + for (uint256 i = 0; i < strategies.length(); ++i) { + (address strategy, uint48 enabledVaultTime, uint48 disabledVaultTime) = strategies.atWithTimes(i); + + address collateral = address(IStrategy(strategy).underlyingToken()); + status.collaterals[i] = collateral; + if (!_wasEnabledAt(enabledVaultTime, disabledVaultTime, epochStartTs)) { + continue; + } + + status.amounts[i] = getOperatorStake(operator, collateral); + } + } + + /// @notice Get the amount of tokens delegated to an operator across the allowed strategies. + /// @param operator The operator address to get the stake for. + /// @param collateral The collateral address to get the stake for. + /// @return amount The amount of tokens delegated to the operator of the specified collateral. + function getOperatorStake(address operator, address collateral) public view returns (uint256 amount) { + uint48 timestamp = Time.timestamp(); + return getOperatorStakeAt(operator, collateral, timestamp); + } + + /// @notice Get the stake of an operator in EigenLayer protocol at a given timestamp. + /// @param operator The operator address to check the stake for. + /// @param collateral The collateral address to check the stake for. + /// @param timestamp The timestamp to check the stake at. + /// @return amount The stake of the operator at the given timestamp, in collateral token. + function getOperatorStakeAt( + address operator, + address collateral, + uint48 timestamp + ) public view returns (uint256 amount) { + if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); + + // NOTE: Can this be done more gas-efficiently? + IStrategy[] memory strategyMem = new IStrategy[](1); + + for (uint256 i = 0; i < strategies.length(); i++) { + (address strategy, uint48 enabledTime, uint48 disabledTime) = strategies.atWithTimes(i); + + if (collateral != address(IStrategy(strategy).underlyingToken())) { + continue; + } + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + strategyMem[0] = IStrategy(strategy); + // NOTE: order is preserved i.e., shares[i] corresponds to strategies[i] + uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, strategyMem); + amount += IStrategy(strategy).sharesToUnderlyingView(shares[0]); + } + + return amount; + } + + /// @notice Get the total stake of all EigenLayer operators at a given epoch for a collateral asset. + /// @param epoch The epoch to check the total stake for. + /// @param collateral The collateral address to check the total stake for. + /// @return totalStake The total stake of all operators at the given epoch, in collateral token. + function getTotalStake(uint48 epoch, address collateral) public view returns (uint256 totalStake) { + uint48 epochStartTs = getEpochStartTs(epoch); + + // for epoch older than SLASHING_WINDOW total stake can be invalidated + // NOTE: not available in EigenLayer yet since slashing is not live + // if ( + // epochStartTs < SLASHING_WINDOW || + // epochStartTs < Time.timestamp() - SLASHING_WINDOW || + // epochStartTs > Time.timestamp() + // ) { + // revert InvalidQuery(); + // } + + for (uint256 i; i < operators.length(); ++i) { + (address operator, uint48 enabledTime, uint48 disabledTime) = operators.atWithTimes(i); + + // just skip operator if it was added after the target epoch or paused + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + totalStake += getOperatorStake(operator, collateral); + } + } + + // ========= EIGENLAYER AVS FUNCTIONS ========= + + /// @notice Register an EigenLayer layer operator to work in Bolt Protocol. + /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator. + /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) public { + AVS_DIRECTORY.registerOperatorToAVS(operator, operatorSignature); + } + + /// @notice Check if an operator is registered to work in Bolt Protocol AVS by + /// looking up the AVS Directory contract. + function checkIfOperatorRegisteredToAVS( + address operator + ) public view returns (bool registered) { + return AVS_DIRECTORY.avsOperatorStatus(address(this), operator) + == IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED; + } + + /// @notice Deregister an EigenLayer layer operator from working in Bolt Protocol. + /// @dev This requires calling the EigenLayer AVS Directory contract to deregister the operator. + /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. + function deregisterOperatorFromAVS() public { + AVS_DIRECTORY.deregisterOperatorFromAVS(msg.sender); + } + + /// @notice emits an `AVSMetadataURIUpdated` event indicating the information has updated. + /// @param metadataURI The URI for metadata associated with an avs + function updateAVSMetadataURI( + string calldata metadataURI + ) public onlyOwner { + AVS_DIRECTORY.updateAVSMetadataURI(metadataURI); + } + + // ========= 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/contracts/BoltManager.sol b/bolt-contracts/src/contracts/BoltManager.sol index b3585dd56..a717ef889 100644 --- a/bolt-contracts/src/contracts/BoltManager.sol +++ b/bolt-contracts/src/contracts/BoltManager.sol @@ -18,17 +18,9 @@ import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; import {MapWithTimeData} from "../lib/MapWithTimeData.sol"; import {IBoltValidators} from "../interfaces/IBoltValidators.sol"; +import {IBoltMiddleware} from "../interfaces/IBoltMiddleware.sol"; import {IBoltManager} from "../interfaces/IBoltManager.sol"; -import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; -import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; -import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol"; -import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; -import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; -import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectoryStorage.sol"; -import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol"; -import {StrategyManagerStorage} from "@eigenlayer/src/contracts/core/StrategyManagerStorage.sol"; - contract BoltManager is IBoltManager, Ownable { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableMap for EnumerableMap.AddressToUintMap; @@ -41,116 +33,20 @@ contract BoltManager is IBoltManager, Ownable { /// BLS pubkey and are assigned a sequence number. IBoltValidators public validators; - /// @notice Set of Symbiotic operator addresses that have opted in to Bolt Protocol. - EnumerableMap.AddressToUintMap private symbioticOperators; - - /// @notice Set of Symbiotic protocol vaults that are used in Bolt Protocol. - EnumerableMap.AddressToUintMap private symbioticVaults; - - /// @notice Set of Symbiotic collateral addresses that are whitelisted. - EnumerableSet.AddressSet private whitelistedSymbioticCollaterals; - - /// @notice Set of EigenLayer operators addresses that have opted in to Bolt Protocol. - EnumerableMap.AddressToUintMap private eigenLayerOperators; - - /// @notice Set of EigenLayer protocol strategies that are used in Bolt Protocol. - EnumerableMap.AddressToUintMap private eigenLayerStrategies; - - /// @notice Set of EigenLayer collaterals addresses that are allowed. - EnumerableSet.AddressSet private whitelistedEigenLayerCollaterals; - - // ========= IMMUTABLES ========= - - /// @notice Address of the Bolt network in Symbiotic Protocol. - address public immutable BOLT_SYMBIOTIC_NETWORK; - - /// @notice Address of the Symbiotic Operator Registry contract. - address public immutable SYMBIOTIC_OPERATOR_REGISTRY; - - /// @notice Address of the Symbiotic Vault Registry contract. - address public immutable SYMBIOTIC_VAULT_REGISTRY; - - /// @notice Address of the Symbiotic Operator Network Opt-In contract. - address public immutable SYMBIOTIC_OPERATOR_NET_OPTIN; - - /// @notice Address of the EigenLayer AVS Directory contract. - AVSDirectoryStorage public immutable EIGENLAYER_AVS_DIRECTORY; - - /// @notice Address of the EigenLayer Delegation Manager contract. - DelegationManagerStorage public immutable EIGENLAYER_DELEGATION_MANAGER; - - /// @notice Address of the EigenLayer Strategy Manager contract. - StrategyManagerStorage public immutable EIGENLAYER_STRATEGY_MANAGER; - - /// @notice Start timestamp of the first epoch. - uint48 public immutable START_TIMESTAMP; - - // ========= CONSTANTS ========= - - /// @notice Slasher that can instantly slash operators without veto. - uint256 public constant INSTANT_SLASHER_TYPE = 0; - - /// @notice Slasher that can request a veto before actually slashing operators. - uint256 public constant VETO_SLASHER_TYPE = 1; - - /// @notice Duration of an epoch in seconds. - uint48 public constant EPOCH_DURATION = 1 days; - - /// @notice Duration of the slashing window in seconds. - uint48 public constant SLASHING_WINDOW = 7 days; + /// @notice Set of restaking protocols supported. Each address corresponds to the + /// associated Bolt Middleware contract. + EnumerableSet.AddressSet private restakingProtocols; // ========= CONSTRUCTOR ========= /// @notice Constructor for the BoltManager contract. /// @param _validators The address of the validators registry. - /// @param _symbioticNetwork The address of the Symbiotic network. - /// @param _symbioticOperatorRegistry The address of the Symbiotic operator registry. - /// @param _symbioticOperatorNetOptIn The address of the Symbiotic operator network opt-in contract. - /// @param _symbioticVaultRegistry The address of the Symbiotic vault registry. - constructor( - address _owner, - address _validators, - address _symbioticNetwork, - address _symbioticOperatorRegistry, - address _symbioticOperatorNetOptIn, - address _symbioticVaultRegistry, - address _eigenlayerAVSDirectory, - address _eigenlayerDelegationManager, - address _eigenlayerStrategyManager - ) Ownable(_owner) { + constructor(address _owner, address _validators) Ownable(_owner) { validators = IBoltValidators(_validators); - START_TIMESTAMP = Time.timestamp(); - - BOLT_SYMBIOTIC_NETWORK = _symbioticNetwork; - SYMBIOTIC_OPERATOR_REGISTRY = _symbioticOperatorRegistry; - SYMBIOTIC_OPERATOR_NET_OPTIN = _symbioticOperatorNetOptIn; - SYMBIOTIC_VAULT_REGISTRY = _symbioticVaultRegistry; - EIGENLAYER_AVS_DIRECTORY = AVSDirectoryStorage(_eigenlayerAVSDirectory); - EIGENLAYER_DELEGATION_MANAGER = DelegationManagerStorage(_eigenlayerDelegationManager); - EIGENLAYER_STRATEGY_MANAGER = StrategyManagerStorage(_eigenlayerStrategyManager); } // ========= VIEW FUNCTIONS ========= - /// @notice Get the start timestamp of an epoch. - function getEpochStartTs( - uint48 epoch - ) public view returns (uint48 timestamp) { - return START_TIMESTAMP + epoch * EPOCH_DURATION; - } - - /// @notice Get the epoch at a given timestamp. - function getEpochAtTs( - uint48 timestamp - ) public view returns (uint48 epoch) { - return (timestamp - START_TIMESTAMP) / EPOCH_DURATION; - } - - /// @notice Get 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. @@ -165,632 +61,26 @@ contract BoltManager is IBoltManager, Ownable { return validators.getValidatorByPubkeyHash(pubkeyHash).authorizedOperator == operator; } - /// @notice Get the list of collateral addresses that are whitelisted. - /// @return collaterals The list of collateral addresses that are whitelisted. - function getWhitelistedSymbioticCollaterals() public view returns (address[] memory collaterals) { - return whitelistedSymbioticCollaterals.values(); - } - - /// @notice Check if a collateral address is whitelisted. - /// @param collateral The collateral address to check the whitelist status for. - /// @return True if the collateral address is whitelisted, false otherwise. - function isSymbioticCollateralWhitelisted( - address collateral - ) public view returns (bool) { - return whitelistedSymbioticCollaterals.contains(collateral); - } - - /// @notice Get the list of EigenLayer strategies addresses that are allowed. - /// @return strategies The list of strategies addresses that are allowed. - function getWhitelistedEigenLayerCollaterals() public view returns (address[] memory strategies) { - return whitelistedEigenLayerCollaterals.values(); - } - - /// @notice Check if an EigenLayer strategy address is allowed. - /// @param strategy The strategy address to check if it is allowed. - /// @return true if the strategy address is allowed, false otherwise. - function isEigenLayerCollateralWhitelisted( - address strategy - ) public view returns (bool) { - return whitelistedEigenLayerCollaterals.contains(strategy); + /// @notice Returns the addresses of the middleware contracts of restaking protocols supported by Bolt. + function getSupportedRestakingProtocols() public view returns (address[] memory middlewares) { + return restakingProtocols.values(); } // ========= ADMIN FUNCTIONS ========= - /// @notice Add a collateral address to the whitelist. - /// @param collateral The collateral address to add to the whitelist. - function addWhitelistedSymbioticCollateral( - address collateral - ) public onlyOwner { - whitelistedSymbioticCollaterals.add(collateral); - } - - /// @notice Remove a collateral address from the whitelist. - /// @param collateral The collateral address to remove from the whitelist. - function removeWhitelistedSymbioticCollateral( - address collateral - ) public onlyOwner { - whitelistedSymbioticCollaterals.remove(collateral); - } - - /// @notice Add a collateral address to the whitelist. - /// @param collateral The collateral address to add to the whitelist. - function addWhitelistedEigenLayerCollateral( - address collateral + /// @notice Add a restaking protocol into Bolt + /// @param protocolMiddleware The address of the restaking protocol Bolt middleware + function addRestakingProtocol( + IBoltMiddleware protocolMiddleware ) public onlyOwner { - whitelistedEigenLayerCollaterals.add(collateral); + restakingProtocols.add(address(protocolMiddleware)); } - /// @notice Remove a collateral address from the whitelist. - /// @param collateral The collateral address to remove from the whitelist. - function removeWhitelistedEigenLayerCollateral( - address collateral + /// @notice Remove a restaking protocol from Bolt + /// @param protocolMiddleware The address of the restaking protocol Bolt middleware + function removeRestakingProtocol( + IBoltMiddleware protocolMiddleware ) public onlyOwner { - whitelistedEigenLayerCollaterals.remove(collateral); - } - - // ========= SYMBIOTIC MIDDLEWARE LOGIC ========= - - /// @notice Allow an operator to signal opt-in to Bolt Protocol. - /// @param operator The operator address to signal opt-in for. - function registerSymbioticOperator( - address operator - ) public { - if (symbioticOperators.contains(operator)) { - revert AlreadyRegistered(); - } - - if (!IRegistry(SYMBIOTIC_OPERATOR_REGISTRY).isEntity(operator)) { - revert NotOperator(); - } - - if (!IOptInService(SYMBIOTIC_OPERATOR_NET_OPTIN).isOptedIn(operator, BOLT_SYMBIOTIC_NETWORK)) { - revert OperatorNotOptedIn(); - } - - symbioticOperators.add(operator); - symbioticOperators.enable(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. - function pauseSymbioticOperator() public { - if (!symbioticOperators.contains(msg.sender)) { - revert NotRegistered(); - } - - symbioticOperators.disable(msg.sender); - } - - /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. - function unpauseSymbioticOperator() public { - if (!symbioticOperators.contains(msg.sender)) { - revert NotRegistered(); - } - - symbioticOperators.enable(msg.sender); - } - - /// @notice Allow a vault to signal opt-in to Bolt Protocol. - /// @param vault The vault address to signal opt-in for. - function registerSymbioticVault( - address vault - ) public { - if (symbioticVaults.contains(vault)) { - revert AlreadyRegistered(); - } - - if (!IRegistry(SYMBIOTIC_VAULT_REGISTRY).isEntity(vault)) { - revert NotVault(); - } - - if (!isSymbioticCollateralWhitelisted(IVault(vault).collateral())) { - revert CollateralNotWhitelisted(); - } - - // TODO: check slashing conditions and veto duration - - symbioticVaults.add(vault); - symbioticVaults.enable(vault); - } - - /// @notice Allow a vault to signal indefinite opt-out from Bolt Protocol. - function pauseSymbioticVault() public { - if (!symbioticVaults.contains(msg.sender)) { - revert NotRegistered(); - } - - symbioticVaults.disable(msg.sender); - } - - /// @notice Allow a disabled vault to signal opt-in to Bolt Protocol. - function unpauseSymbioticVault() public { - if (!symbioticVaults.contains(msg.sender)) { - revert NotRegistered(); - } - - symbioticVaults.enable(msg.sender); - } - - /// @notice Check if a vault is currently enabled to work in Bolt Protocol. - /// @param vault The vault address to check the enabled status for. - /// @return True if the vault is enabled, false otherwise. - function isSymbioticVaultEnabled( - address vault - ) public view returns (bool) { - (uint48 enabledTime, uint48 disabledTime) = symbioticVaults.getTimes(vault); - return enabledTime != 0 && disabledTime == 0; - } - - /// @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 isSymbioticOperatorEnabled( - address operator - ) public view returns (bool) { - (uint48 enabledTime, uint48 disabledTime) = symbioticOperators.getTimes(operator); - return enabledTime != 0 && disabledTime == 0; - } - - /// @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 getSymbioticProposersStatus( - bytes32[] memory pubkeyHashes - ) public view returns (ProposerStatus[] memory statuses) { - statuses = new ProposerStatus[](pubkeyHashes.length); - for (uint256 i = 0; i < pubkeyHashes.length; ++i) { - statuses[i] = getSymbioticProposerStatus(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 getSymbioticProposerStatus( - bytes32 pubkeyHash - ) public view returns (ProposerStatus memory status) { - if (pubkeyHash == bytes32(0)) { - revert InvalidQuery(); - } - - uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); - IBoltValidators.Validator memory validator = validators.getValidatorByPubkeyHash(pubkeyHash); - - address operator = validator.authorizedOperator; - - status.pubkeyHash = pubkeyHash; - status.active = validator.exists; - status.operator = operator; - - (uint48 enabledTime, uint48 disabledTime) = symbioticOperators.getTimes(operator); - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - return status; - } - - status.collaterals = new address[](symbioticVaults.length()); - status.amounts = new uint256[](symbioticVaults.length()); - - for (uint256 i = 0; i < symbioticVaults.length(); ++i) { - (address vault, uint48 enabledVaultTime, uint48 disabledVaultTime) = symbioticVaults.atWithTimes(i); - - address collateral = IVault(vault).collateral(); - status.collaterals[i] = collateral; - if (!_wasEnabledAt(enabledVaultTime, disabledVaultTime, epochStartTs)) { - continue; - } - - status.amounts[i] = getSymbioticOperatorStakeAt(operator, collateral, epochStartTs); - } - } - - /// @notice Get the stake of an operator in Symbiotic protocol at the current timestamp. - /// @param operator The operator address to check the stake for. - /// @param collateral The collateral address to check the stake for. - /// @return amount The stake of the operator at the current timestamp, in collateral token. - function getSymbioticOperatorStake(address operator, address collateral) public view returns (uint256 amount) { - uint48 timestamp = Time.timestamp(); - return getSymbioticOperatorStakeAt(operator, collateral, timestamp); - } - - /// @notice Get the stake of an operator in Symbiotic protocol at a given timestamp. - /// @param operator The operator address to check the stake for. - /// @param collateral The collateral address to check the stake for. - /// @param timestamp The timestamp to check the stake at. - /// @return amount The stake of the operator at the given timestamp, in collateral token. - function getSymbioticOperatorStakeAt( - address operator, - address collateral, - uint48 timestamp - ) public view returns (uint256 amount) { - if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) { - revert InvalidQuery(); - } - - uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); - - for (uint256 i = 0; i < symbioticVaults.length(); ++i) { - (address vault, uint48 enabledTime, uint48 disabledTime) = symbioticVaults.atWithTimes(i); - - if (collateral != IVault(vault).collateral()) { - continue; - } - - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - continue; - } - - // in order to have stake in a network, the operator needs to be opted in to that vault. - // this authorization is fully handled in the Vault, we just need to read the stake. - amount += IBaseDelegator(IVault(vault).delegator()).stakeAt( - // The stake for each subnetwork is stored in the vault's delegator contract. - // stakeAt returns the stake of "operator" at "timestamp" for "network" (or subnetwork) - // bytes(0) is for hints, which we don't currently use. - BOLT_SYMBIOTIC_NETWORK.subnetwork(0), - operator, - epochStartTs, - new bytes(0) - ); - } - - return amount; - } - - /// @notice Get the total stake of all Symbiotic operators at a given epoch for a collateral asset. - /// @param epoch The epoch to check the total stake for. - /// @param collateral The collateral address to check the total stake for. - /// @return totalStake The total stake of all operators at the given epoch, in collateral token. - function getSymbioticTotalStake(uint48 epoch, address collateral) public view returns (uint256 totalStake) { - uint48 epochStartTs = getEpochStartTs(epoch); - - // for epoch older than SLASHING_WINDOW total stake can be invalidated - if ( - epochStartTs < SLASHING_WINDOW || epochStartTs < Time.timestamp() - SLASHING_WINDOW - || epochStartTs > Time.timestamp() - ) { - revert InvalidQuery(); - } - - for (uint256 i; i < symbioticOperators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = symbioticOperators.atWithTimes(i); - - // just skip operator if it was added after the target epoch or paused - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - continue; - } - - totalStake += getSymbioticOperatorStakeAt(operator, collateral, epochStartTs); - } - } - - /// @notice Slash a given operator for a given amount of collateral. - /// @param timestamp The timestamp of the slash event. - /// @param operator The operator address to slash. - /// @param collateral The collateral address to slash. - /// @param amount The amount of collateral to slash. - function slash(uint48 timestamp, address operator, address collateral, uint256 amount) public onlyOwner { - // TODO: remove onlyOwner modifier and gate the slashing logic behind the BoltChallenger - // fault proof mechanism to allow for permissionless slashing. - - uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); - - for (uint256 i = 0; i < symbioticVaults.length(); ++i) { - (address vault, uint48 enabledTime, uint48 disabledTime) = symbioticVaults.atWithTimes(i); - - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - continue; - } - - if (collateral != IVault(vault).collateral()) { - continue; - } - - uint256 operatorStake = getSymbioticOperatorStakeAt(operator, collateral, epochStartTs); - - if (amount > operatorStake) { - revert SlashAmountTooHigh(); - } - - uint256 vaultStake = IBaseDelegator(IVault(vault).delegator()).stakeAt( - BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, epochStartTs, new bytes(0) - ); - - // Slash the vault pro-rata. - _slashSymbioticVault(epochStartTs, vault, operator, (amount * vaultStake) / operatorStake); - } - } - - // ========= EIGENLAYER MIDDLEWARE LOGIC ========= - - /// @notice Allow an operator to signal opt-in to Bolt Protocol. - /// @param operator The operator address to signal opt-in for. - function registerEigenLayerOperator( - address operator - ) public { - if (eigenLayerOperators.contains(operator)) { - revert AlreadyRegistered(); - } - - if (!EIGENLAYER_DELEGATION_MANAGER.isOperator(operator)) { - revert NotOperator(); - } - - if (!checkIfEigenLayerOperatorRegisteredToAVS(operator)) { - revert OperatorNotRegisteredToAVS(); - } - - eigenLayerOperators.add(operator); - eigenLayerOperators.enable(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. - function pauseEigenLayerOperator() public { - if (!eigenLayerOperators.contains(msg.sender)) { - revert NotRegistered(); - } - - eigenLayerOperators.disable(msg.sender); - } - - /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. - function unpauseEigenLayerOperator() public { - if (!eigenLayerOperators.contains(msg.sender)) { - revert NotRegistered(); - } - - eigenLayerOperators.enable(msg.sender); - } - - function registerEigenLayerStrategy( - address strategy - ) public { - if (eigenLayerStrategies.contains(strategy)) { - revert AlreadyRegistered(); - } - - if (!EIGENLAYER_STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy))) { - revert StrategyNotAllowed(); - } - - if (!isEigenLayerCollateralWhitelisted(address(IStrategy(strategy).underlyingToken()))) { - revert CollateralNotWhitelisted(); - } - - eigenLayerStrategies.add(strategy); - eigenLayerStrategies.enable(strategy); - } - - /// @notice Allow a strategy to signal indefinite opt-out from Bolt Protocol. - function pauseEigenLayerStrategy() public { - if (!eigenLayerStrategies.contains(msg.sender)) { - revert NotRegistered(); - } - - eigenLayerStrategies.disable(msg.sender); - } - - /// @notice Allow a disabled strategy to signal opt-in to Bolt Protocol. - function unpauseEigenLayerStrategy() public { - if (!eigenLayerStrategies.contains(msg.sender)) { - revert NotRegistered(); - } - - eigenLayerStrategies.enable(msg.sender); - } - - /// @notice Check if a strategy is currently enabled to work in Bolt Protocol. - /// @param strategy The strategy address to check the enabled status for. - /// @return True if the strategy is enabled, false otherwise. - function isEigenLayerStrategyEnabled( - address strategy - ) public view returns (bool) { - (uint48 enabledTime, uint48 disabledTime) = eigenLayerStrategies.getTimes(strategy); - return enabledTime != 0 && disabledTime == 0; - } - - /// @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 isEigenLayerOperatorEnabled( - address operator - ) public view returns (bool) { - (uint48 enabledTime, uint48 disabledTime) = eigenLayerOperators.getTimes(operator); - return enabledTime != 0 && disabledTime == 0; - } - - /// @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 getEigenLayerProposersStatus( - bytes32[] memory pubkeyHashes - ) public view returns (ProposerStatus[] memory statuses) { - statuses = new ProposerStatus[](pubkeyHashes.length); - for (uint256 i = 0; i < pubkeyHashes.length; ++i) { - statuses[i] = getEigenLayerProposerStatus(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 getEigenLayerProposerStatus( - bytes32 pubkeyHash - ) public view returns (ProposerStatus memory status) { - if (pubkeyHash == bytes32(0)) { - revert InvalidQuery(); - } - - uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); - IBoltValidators.Validator memory validator = validators.getValidatorByPubkeyHash(pubkeyHash); - - address operator = validator.authorizedOperator; - - status.pubkeyHash = pubkeyHash; - status.active = validator.exists; - status.operator = operator; - - (uint48 enabledTime, uint48 disabledTime) = eigenLayerOperators.getTimes(operator); - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - return status; - } - - status.collaterals = new address[](eigenLayerStrategies.length()); - status.amounts = new uint256[](eigenLayerStrategies.length()); - - for (uint256 i = 0; i < eigenLayerStrategies.length(); ++i) { - (address strategy, uint48 enabledVaultTime, uint48 disabledVaultTime) = eigenLayerStrategies.atWithTimes(i); - - address collateral = address(IStrategy(strategy).underlyingToken()); - status.collaterals[i] = collateral; - if (!_wasEnabledAt(enabledVaultTime, disabledVaultTime, epochStartTs)) { - continue; - } - - status.amounts[i] = getEigenLayerOperatorStake(operator, collateral); - } - } - - /// @notice Get the amount of tokens delegated to an operator across the allowed strategies. - // @param operator The operator address to get the stake for. - // @param strategies The list of strategies to get the stake for. - // @return tokenAmounts The amount of tokens delegated to the operator for each strategy. - function getEigenLayerOperatorStake(address operator, address collateral) public view returns (uint256 amount) { - uint48 timestamp = Time.timestamp(); - return getEigenLayerOperatorStakeAt(operator, collateral, timestamp); - } - - /// @notice Get the stake of an operator in EigenLayer protocol at a given timestamp. - /// @param operator The operator address to check the stake for. - /// @param collateral The collateral address to check the stake for. - /// @param timestamp The timestamp to check the stake at. - /// @return amount The stake of the operator at the given timestamp, in collateral token. - function getEigenLayerOperatorStakeAt( - address operator, - address collateral, - uint48 timestamp - ) public view returns (uint256 amount) { - if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) { - revert InvalidQuery(); - } - - uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); - - // NOTE: Can this be done more gas-efficiently? - IStrategy[] memory strategyMem = new IStrategy[](1); - - for (uint256 i = 0; i < eigenLayerStrategies.length(); i++) { - (address strategy, uint48 enabledTime, uint48 disabledTime) = eigenLayerStrategies.atWithTimes(i); - - if (collateral != address(IStrategy(strategy).underlyingToken())) { - continue; - } - - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - continue; - } - - strategyMem[0] = IStrategy(strategy); - // NOTE: order is preserved i.e., shares[i] corresponds to strategies[i] - uint256[] memory shares = EIGENLAYER_DELEGATION_MANAGER.getOperatorShares(operator, strategyMem); - amount += IStrategy(strategy).sharesToUnderlyingView(shares[0]); - } - - return amount; - } - - /// @notice Get the total stake of all EigenLayer operators at a given epoch for a collateral asset. - /// @param epoch The epoch to check the total stake for. - /// @param collateral The collateral address to check the total stake for. - /// @return totalStake The total stake of all operators at the given epoch, in collateral token. - function getEigenLayerTotalStake(uint48 epoch, address collateral) public view returns (uint256 totalStake) { - uint48 epochStartTs = getEpochStartTs(epoch); - - // for epoch older than SLASHING_WINDOW total stake can be invalidated - // NOTE: not available in EigenLayer yet since slashing is not live - // if ( - // epochStartTs < SLASHING_WINDOW || - // epochStartTs < Time.timestamp() - SLASHING_WINDOW || - // epochStartTs > Time.timestamp() - // ) { - // revert InvalidQuery(); - // } - - for (uint256 i; i < eigenLayerOperators.length(); ++i) { - (address operator, uint48 enabledTime, uint48 disabledTime) = eigenLayerOperators.atWithTimes(i); - - // just skip operator if it was added after the target epoch or paused - if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { - continue; - } - - totalStake += getEigenLayerOperatorStake(operator, collateral); - } - } - - // ========= EIGENLAYER AVS FUNCTIONS ========= - - /// @notice Register an EigenLayer layer operator to work in Bolt Protocol. - /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator. - /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator - function registerEigenLayerOperatorToAVS( - address operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature - ) public { - EIGENLAYER_AVS_DIRECTORY.registerOperatorToAVS(operator, operatorSignature); - } - - function checkIfEigenLayerOperatorRegisteredToAVS( - address operator - ) public view returns (bool registered) { - return EIGENLAYER_AVS_DIRECTORY.avsOperatorStatus(address(this), operator) - == IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED; - } - - /// @notice Deregister an EigenLayer layer operator from working in Bolt Protocol. - /// @dev This requires calling the EigenLayer AVS Directory contract to deregister the operator. - /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. - function deregisterEigenLayerOperatorFromAVS() public { - EIGENLAYER_AVS_DIRECTORY.deregisterOperatorFromAVS(msg.sender); - } - - /// @notice emits an `AVSMetadataURIUpdated` event indicating the information has updated. - /// @param metadataURI The URI for metadata associated with an avs - function updateEigenLayerAVSMetadataURI( - string calldata metadataURI - ) public onlyOwner { - EIGENLAYER_AVS_DIRECTORY.updateAVSMetadataURI(metadataURI); - } - - // ========= 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); - } - - /// @notice Slash an operator for a given amount of collateral. - /// @param timestamp The timestamp of the slash event. - /// @param operator The operator address to slash. - /// @param amount The amount of collateral to slash. - function _slashSymbioticVault(uint48 timestamp, address vault, address operator, uint256 amount) private { - address slasher = IVault(vault).slasher(); - uint256 slasherType = IEntity(slasher).TYPE(); - - if (slasherType == INSTANT_SLASHER_TYPE) { - ISlasher(slasher).slash(BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0)); - } else if (slasherType == VETO_SLASHER_TYPE) { - IVetoSlasher(slasher).requestSlash( - BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0) - ); - } else { - revert UnknownSlasherType(); - } + restakingProtocols.remove(address(protocolMiddleware)); } } diff --git a/bolt-contracts/src/contracts/BoltSymbioticMiddleware.sol b/bolt-contracts/src/contracts/BoltSymbioticMiddleware.sol new file mode 100644 index 000000000..bc3d33622 --- /dev/null +++ b/bolt-contracts/src/contracts/BoltSymbioticMiddleware.sol @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol"; +import {Time} from "lib/openzeppelin-contracts/contracts/utils/types/Time.sol"; +import {EnumerableMap} from "lib/openzeppelin-contracts/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; + +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol"; +import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; +import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; +import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; +import {IEntity} from "@symbiotic/interfaces/common/IEntity.sol"; + +import {MapWithTimeData} from "../lib/MapWithTimeData.sol"; +import {IBoltValidators} from "../interfaces/IBoltValidators.sol"; +import {IBoltMiddleware} from "../interfaces/IBoltMiddleware.sol"; +import {IBoltManager} from "../interfaces/IBoltManager.sol"; + +contract BoltSymbioticMiddleware is IBoltMiddleware, Ownable { + using EnumerableSet for EnumerableSet.AddressSet; + using EnumerableMap for EnumerableMap.AddressToUintMap; + using MapWithTimeData for EnumerableMap.AddressToUintMap; + using Subnetwork for address; + + // ========= STORAGE ========= + + /// @notice Validators registry, where validators are registered via their + /// BLS pubkey and are assigned a sequence number. + IBoltManager public boltManager; + + /// @notice Set of Symbiotic operator addresses that have opted in to Bolt Protocol. + EnumerableMap.AddressToUintMap private operators; + + /// @notice Set of Symbiotic protocol vaults that are used in Bolt Protocol. + EnumerableMap.AddressToUintMap private vaults; + + /// @notice Set of Symbiotic collateral addresses that are whitelisted. + EnumerableSet.AddressSet private whitelistedCollaterals; + + // ========= IMMUTABLES ========= + + /// @notice Address of the Bolt network in Symbiotic Protocol. + address public immutable BOLT_SYMBIOTIC_NETWORK; + + /// @notice Address of the Symbiotic Operator Registry contract. + address public immutable OPERATOR_REGISTRY; + + /// @notice Address of the Symbiotic Vault Registry contract. + address public immutable VAULT_REGISTRY; + + /// @notice Address of the Symbiotic Operator Network Opt-In contract. + address public immutable OPERATOR_NET_OPTIN; + + /// @notice Start timestamp of the first epoch. + uint48 public immutable START_TIMESTAMP; + + // ========= CONSTANTS ========= + + /// @notice Slasher that can instantly slash operators without veto. + uint256 public constant INSTANT_SLASHER_TYPE = 0; + + /// @notice Slasher that can request a veto before actually slashing operators. + uint256 public constant VETO_SLASHER_TYPE = 1; + + /// @notice Duration of an epoch in seconds. + uint48 public constant EPOCH_DURATION = 1 days; + + /// @notice Duration of the slashing window in seconds. + uint48 public constant SLASHING_WINDOW = 7 days; + + bytes32 public constant NAME_HASH = keccak256("SYMBIOTIC"); + + // ========= ERRORS ========= + + error NotVault(); + error SlashAmountTooHigh(); + error UnknownSlasherType(); + + // ========= CONSTRUCTOR ========= + + /// @notice Constructor for the BoltSymbioticMiddleware contract. + /// @param _boltManager The address of the Bolt Manager contract. + /// @param _symbioticNetwork The address of the Symbiotic network. + /// @param _symbioticOperatorRegistry The address of the Symbiotic operator registry. + /// @param _symbioticOperatorNetOptIn The address of the Symbiotic operator network opt-in contract. + /// @param _symbioticVaultRegistry The address of the Symbiotic vault registry. + constructor( + address _owner, + address _boltManager, + address _symbioticNetwork, + address _symbioticOperatorRegistry, + address _symbioticOperatorNetOptIn, + address _symbioticVaultRegistry + ) Ownable(_owner) { + boltManager = IBoltManager(_boltManager); + START_TIMESTAMP = Time.timestamp(); + + BOLT_SYMBIOTIC_NETWORK = _symbioticNetwork; + OPERATOR_REGISTRY = _symbioticOperatorRegistry; + OPERATOR_NET_OPTIN = _symbioticOperatorNetOptIn; + VAULT_REGISTRY = _symbioticVaultRegistry; + } + + // ========= VIEW FUNCTIONS ========= + + /// @notice Get the start timestamp of an epoch. + function getEpochStartTs( + uint48 epoch + ) public view returns (uint48 timestamp) { + return START_TIMESTAMP + epoch * EPOCH_DURATION; + } + + /// @notice Get the epoch at a given timestamp. + function getEpochAtTs( + uint48 timestamp + ) public view returns (uint48 epoch) { + return (timestamp - START_TIMESTAMP) / EPOCH_DURATION; + } + + /// @notice Get 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, bytes32 pubkeyHash) public view returns (bool) { + if (operator == address(0) || pubkeyHash == bytes32(0)) { + revert InvalidQuery(); + } + + return boltManager.validators().getValidatorByPubkeyHash(pubkeyHash).authorizedOperator == operator; + } + + /// @notice Get the list of collateral addresses that are whitelisted. + /// @return collaterals The list of collateral addresses that are whitelisted. + function getWhitelistedCollaterals() public view returns (address[] memory collaterals) { + return whitelistedCollaterals.values(); + } + + /// @notice Check if a collateral address is whitelisted. + /// @param collateral The collateral address to check the whitelist status for. + /// @return True if the collateral address is whitelisted, false otherwise. + function isCollateralWhitelisted( + address collateral + ) public view returns (bool) { + return whitelistedCollaterals.contains(collateral); + } + + // ========= ADMIN FUNCTIONS ========= + + /// @notice Add a collateral address to the whitelist. + /// @param collateral The collateral address to add to the whitelist. + function addWhitelistedCollateral( + address collateral + ) public onlyOwner { + whitelistedCollaterals.add(collateral); + } + + /// @notice Remove a collateral address from the whitelist. + /// @param collateral The collateral address to remove from the whitelist. + function removeWhitelistedCollateral( + address collateral + ) public onlyOwner { + whitelistedCollaterals.remove(collateral); + } + + // ========= SYMBIOTIC MIDDLEWARE LOGIC ========= + + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// @param operator The operator address to signal opt-in for. + function registerOperator( + address operator + ) public { + if (operators.contains(operator)) { + revert AlreadyRegistered(); + } + + if (!IRegistry(OPERATOR_REGISTRY).isEntity(operator)) { + revert NotOperator(); + } + + if (!IOptInService(OPERATOR_NET_OPTIN).isOptedIn(operator, BOLT_SYMBIOTIC_NETWORK)) { + revert OperatorNotOptedIn(); + } + + operators.add(operator); + operators.enable(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. + function pauseOperator() public { + if (!operators.contains(msg.sender)) { + revert NotRegistered(); + } + + operators.disable(msg.sender); + } + + /// @notice Allow a disabled operator to signal opt-in to Bolt Protocol. + function unpauseOperator() public { + if (!operators.contains(msg.sender)) { + revert NotRegistered(); + } + + operators.enable(msg.sender); + } + + /// @notice Allow a vault to signal opt-in to Bolt Protocol. + /// @param vault The vault address to signal opt-in for. + function registerVault( + address vault + ) public { + if (vaults.contains(vault)) { + revert AlreadyRegistered(); + } + + if (!IRegistry(VAULT_REGISTRY).isEntity(vault)) { + revert NotVault(); + } + + if (!isCollateralWhitelisted(IVault(vault).collateral())) { + revert CollateralNotWhitelisted(); + } + + // TODO: check slashing conditions and veto duration + + vaults.add(vault); + vaults.enable(vault); + } + + /// @notice Allow a vault to signal indefinite opt-out from Bolt Protocol. + function pauseVault() public { + if (!vaults.contains(msg.sender)) { + revert NotRegistered(); + } + + vaults.disable(msg.sender); + } + + /// @notice Allow a disabled vault to signal opt-in to Bolt Protocol. + function unpauseVault() public { + if (!vaults.contains(msg.sender)) { + revert NotRegistered(); + } + + vaults.enable(msg.sender); + } + + /// @notice Check if a vault is currently enabled to work in Bolt Protocol. + /// @param vault The vault address to check the enabled status for. + /// @return True if the vault is enabled, false otherwise. + function isVaultEnabled( + address vault + ) public view returns (bool) { + (uint48 enabledTime, uint48 disabledTime) = vaults.getTimes(vault); + return enabledTime != 0 && disabledTime == 0; + } + + /// @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) { + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + return enabledTime != 0 && disabledTime == 0; + } + + /// @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 getProposersStatus( + bytes32[] memory pubkeyHashes + ) public view returns (IBoltValidators.ProposerStatus[] memory statuses) { + statuses = new IBoltValidators.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( + bytes32 pubkeyHash + ) public view returns (IBoltValidators.ProposerStatus memory status) { + if (pubkeyHash == bytes32(0)) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(Time.timestamp())); + IBoltValidators.Validator memory validator = boltManager.validators().getValidatorByPubkeyHash(pubkeyHash); + address operator = validator.authorizedOperator; + + status.pubkeyHash = pubkeyHash; + status.active = validator.exists; + status.operator = operator; + + (uint48 enabledTime, uint48 disabledTime) = operators.getTimes(operator); + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + return status; + } + + status.collaterals = new address[](vaults.length()); + status.amounts = new uint256[](vaults.length()); + + for (uint256 i = 0; i < vaults.length(); ++i) { + (address vault, uint48 enabledVaultTime, uint48 disabledVaultTime) = vaults.atWithTimes(i); + + address collateral = IVault(vault).collateral(); + status.collaterals[i] = collateral; + if (!_wasEnabledAt(enabledVaultTime, disabledVaultTime, epochStartTs)) { + continue; + } + + status.amounts[i] = getOperatorStakeAt(operator, collateral, epochStartTs); + } + } + + /// @notice Get the stake of an operator in Symbiotic protocol at the current timestamp. + /// @param operator The operator address to check the stake for. + /// @param collateral The collateral address to check the stake for. + /// @return amount The stake of the operator at the current timestamp, in collateral token. + function getOperatorStake(address operator, address collateral) public view returns (uint256 amount) { + uint48 timestamp = Time.timestamp(); + return getOperatorStakeAt(operator, collateral, timestamp); + } + + /// @notice Get the stake of an operator in Symbiotic protocol at a given timestamp. + /// @param operator The operator address to check the stake for. + /// @param collateral The collateral address to check the stake for. + /// @param timestamp The timestamp to check the stake at. + /// @return amount The stake of the operator at the given timestamp, in collateral token. + function getOperatorStakeAt( + address operator, + address collateral, + uint48 timestamp + ) public view returns (uint256 amount) { + if (timestamp > Time.timestamp() || timestamp < START_TIMESTAMP) { + revert InvalidQuery(); + } + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); + + for (uint256 i = 0; i < vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); + + if (collateral != IVault(vault).collateral()) { + continue; + } + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + // in order to have stake in a network, the operator needs to be opted in to that vault. + // this authorization is fully handled in the Vault, we just need to read the stake. + amount += IBaseDelegator(IVault(vault).delegator()).stakeAt( + // The stake for each subnetwork is stored in the vault's delegator contract. + // stakeAt returns the stake of "operator" at "timestamp" for "network" (or subnetwork) + // bytes(0) is for hints, which we don't currently use. + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), + operator, + epochStartTs, + new bytes(0) + ); + } + + return amount; + } + + /// @notice Get the total stake of all Symbiotic operators at a given epoch for a collateral asset. + /// @param epoch The epoch to check the total stake for. + /// @param collateral The collateral address to check the total stake for. + /// @return totalStake The total stake of all operators at the given epoch, in collateral token. + function getTotalStake(uint48 epoch, address collateral) public view returns (uint256 totalStake) { + uint48 epochStartTs = getEpochStartTs(epoch); + + // for epoch older than SLASHING_WINDOW total stake can be invalidated + if ( + epochStartTs < SLASHING_WINDOW || epochStartTs < Time.timestamp() - SLASHING_WINDOW + || epochStartTs > Time.timestamp() + ) { + revert InvalidQuery(); + } + + for (uint256 i; i < operators.length(); ++i) { + (address operator, uint48 enabledTime, uint48 disabledTime) = operators.atWithTimes(i); + + // just skip operator if it was added after the target epoch or paused + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + totalStake += getOperatorStakeAt(operator, collateral, epochStartTs); + } + } + + /// @notice Slash a given operator for a given amount of collateral. + /// @param timestamp The timestamp of the slash event. + /// @param operator The operator address to slash. + /// @param collateral The collateral address to slash. + /// @param amount The amount of collateral to slash. + function slash(uint48 timestamp, address operator, address collateral, uint256 amount) public onlyOwner { + // TODO: remove onlyOwner modifier and gate the slashing logic behind the BoltChallenger + // fault proof mechanism to allow for permissionless slashing. + + uint48 epochStartTs = getEpochStartTs(getEpochAtTs(timestamp)); + + for (uint256 i = 0; i < vaults.length(); ++i) { + (address vault, uint48 enabledTime, uint48 disabledTime) = vaults.atWithTimes(i); + + if (!_wasEnabledAt(enabledTime, disabledTime, epochStartTs)) { + continue; + } + + if (collateral != IVault(vault).collateral()) { + continue; + } + + uint256 operatorStake = getOperatorStakeAt(operator, collateral, epochStartTs); + + if (amount > operatorStake) { + revert SlashAmountTooHigh(); + } + + uint256 vaultStake = IBaseDelegator(IVault(vault).delegator()).stakeAt( + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, epochStartTs, new bytes(0) + ); + + // Slash the vault pro-rata. + _slashVault(epochStartTs, vault, operator, (amount * vaultStake) / operatorStake); + } + } + + // ========= 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); + } + + /// @notice Slash an operator for a given amount of collateral. + /// @param timestamp The timestamp of the slash event. + /// @param operator The operator address to slash. + /// @param amount The amount of collateral to slash. + function _slashVault(uint48 timestamp, address vault, address operator, uint256 amount) private { + address slasher = IVault(vault).slasher(); + uint256 slasherType = IEntity(slasher).TYPE(); + + if (slasherType == INSTANT_SLASHER_TYPE) { + ISlasher(slasher).slash(BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0)); + } else if (slasherType == VETO_SLASHER_TYPE) { + IVetoSlasher(slasher).requestSlash( + BOLT_SYMBIOTIC_NETWORK.subnetwork(0), operator, amount, timestamp, new bytes(0) + ); + } else { + revert UnknownSlasherType(); + } + } +} diff --git a/bolt-contracts/src/interfaces/IBoltManager.sol b/bolt-contracts/src/interfaces/IBoltManager.sol index 8afaa8bfb..b290ce07f 100644 --- a/bolt-contracts/src/interfaces/IBoltManager.sol +++ b/bolt-contracts/src/interfaces/IBoltManager.sol @@ -1,151 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {BLS12381} from "../lib/bls/BLS12381.sol"; import {IBoltValidators} from "./IBoltValidators.sol"; interface IBoltManager { - struct ProposerStatus { - bytes32 pubkeyHash; - bool active; - address operator; - address[] collaterals; - uint256[] amounts; - } - error InvalidQuery(); - error AlreadyRegistered(); - error NotRegistered(); - error OperatorNotOptedIn(); - error NotOperator(); - error NotVault(); - error CollateralNotWhitelisted(); - error UnknownSlasherType(); - error SlashAmountTooHigh(); - error StrategyNotAllowed(); - error OperatorNotRegisteredToAVS(); - - function getEpochStartTs( - uint48 epoch - ) external view returns (uint48); - - function getEpochAtTs( - uint48 timestamp - ) external view returns (uint48); - - function getCurrentEpoch() external view returns (uint48); - - function addWhitelistedSymbioticCollateral( - address collateral - ) external; - - function addWhitelistedEigenLayerCollateral( - address collateral - ) external; - - function removeWhitelistedSymbioticCollateral( - address collateral - ) external; - - function removeWhitelistedEigenLayerCollateral( - address collateral - ) external; - - function getWhitelistedSymbioticCollaterals() external view returns (address[] memory); - - function getWhitelistedEigenLayerCollaterals() external view returns (address[] memory); - - function isSymbioticCollateralWhitelisted( - address collateral - ) external view returns (bool); - - function isEigenLayerCollateralWhitelisted( - address collateral - ) external view returns (bool); - - function registerSymbioticOperator( - address operator - ) external; - - function registerEigenLayerOperator( - address operator - ) external; - - function pauseSymbioticOperator() external; - - function pauseEigenLayerOperator() external; - - function unpauseSymbioticOperator() external; - - function unpauseEigenLayerOperator() external; - function registerSymbioticVault( - address vault - ) external; - - function registerEigenLayerStrategy( - address vault - ) external; - - function pauseSymbioticVault() external; - - function pauseEigenLayerStrategy() external; - - function unpauseSymbioticVault() external; - - function unpauseEigenLayerStrategy() external; - - function isSymbioticVaultEnabled( - address vault - ) external view returns (bool); - - function isEigenLayerStrategyEnabled( - address strategy - ) external view returns (bool); - - function isSymbioticOperatorEnabled( - address operator - ) external view returns (bool); - - function isEigenLayerOperatorEnabled( - address operator - ) external view returns (bool); - - function getSymbioticProposersStatus( - bytes32[] memory pubkeyHashes - ) external view returns (ProposerStatus[] memory); - - function getEigenLayerProposersStatus( - bytes32[] memory pubkeyHashes - ) external view returns (ProposerStatus[] memory); - - function getSymbioticProposerStatus( - bytes32 pubkeyHash - ) external view returns (ProposerStatus memory); - - function getEigenLayerProposerStatus( - bytes32 pubkeyHash - ) external view returns (ProposerStatus memory); + function validators() external view returns (IBoltValidators); function isOperatorAuthorizedForValidator(address operator, bytes32 pubkeyHash) external view returns (bool); - function getSymbioticOperatorStake(address operator, address collateral) external view returns (uint256); - - function getEigenLayerOperatorStake(address operator, address collateral) external view returns (uint256); - - function getSymbioticOperatorStakeAt( - address operator, - address collateral, - uint48 timestamp - ) external view returns (uint256); - - function getEigenLayerOperatorStakeAt( - address operator, - address collateral, - uint48 timestamp - ) external view returns (uint256); - - function getSymbioticTotalStake(uint48 epoch, address collateral) external view returns (uint256); - - function getEigenLayerTotalStake(uint48 epoch, address collateral) external view returns (uint256); + function getSupportedRestakingProtocols() external view returns (address[] memory middlewares); } diff --git a/bolt-contracts/src/interfaces/IBoltMiddleware.sol b/bolt-contracts/src/interfaces/IBoltMiddleware.sol new file mode 100644 index 000000000..421d47fd0 --- /dev/null +++ b/bolt-contracts/src/interfaces/IBoltMiddleware.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BLS12381} from "../lib/bls/BLS12381.sol"; +import {IBoltValidators} from "./IBoltValidators.sol"; + +interface IBoltMiddleware { + error InvalidQuery(); + error AlreadyRegistered(); + error NotRegistered(); + error OperatorNotOptedIn(); + error NotOperator(); + error CollateralNotWhitelisted(); + error NotAllowed(); + + function NAME_HASH() external view returns (bytes32); + + function getEpochStartTs( + uint48 epoch + ) external view returns (uint48); + + function getEpochAtTs( + uint48 timestamp + ) external view returns (uint48); + + function getCurrentEpoch() external view returns (uint48); + + function addWhitelistedCollateral( + address collateral + ) external; + + function removeWhitelistedCollateral( + address collateral + ) external; + + function getWhitelistedCollaterals() external view returns (address[] memory); + + function isCollateralWhitelisted( + address collateral + ) external view returns (bool); + + function registerOperator( + address operator + ) external; + + function pauseOperator() external; + + function unpauseOperator() external; + + function isOperatorEnabled( + address operator + ) external view returns (bool); + + function getProposersStatus( + bytes32[] memory pubkeyHashes + ) external view returns (IBoltValidators.ProposerStatus[] memory); + + function getProposerStatus( + bytes32 pubkeyHash + ) external view returns (IBoltValidators.ProposerStatus memory); + + function isOperatorAuthorizedForValidator(address operator, bytes32 pubkeyHash) external view returns (bool); + + function getOperatorStake(address operator, address collateral) external view returns (uint256); + + function getOperatorStakeAt( + address operator, + address collateral, + uint48 timestamp + ) external view returns (uint256); + + function getTotalStake(uint48 epoch, address collateral) external view returns (uint256); +} diff --git a/bolt-contracts/src/interfaces/IBoltValidators.sol b/bolt-contracts/src/interfaces/IBoltValidators.sol index 8c40c35a4..0765e7474 100644 --- a/bolt-contracts/src/interfaces/IBoltValidators.sol +++ b/bolt-contracts/src/interfaces/IBoltValidators.sol @@ -20,6 +20,14 @@ interface IBoltValidators { bool exists; } + struct ProposerStatus { + bytes32 pubkeyHash; + bool active; + address operator; + address[] collaterals; + uint256[] amounts; + } + error InvalidBLSSignature(); error InvalidAuthorizedCollateralProvider(); error InvalidAuthorizedOperator(); diff --git a/bolt-contracts/test/BoltManager.EigenLayer.t.sol b/bolt-contracts/test/BoltManager.EigenLayer.t.sol index da0613b44..1b06f127f 100644 --- a/bolt-contracts/test/BoltManager.EigenLayer.t.sol +++ b/bolt-contracts/test/BoltManager.EigenLayer.t.sol @@ -5,8 +5,9 @@ import {Test, console} from "forge-std/Test.sol"; import {BoltValidators} from "../src/contracts/BoltValidators.sol"; import {BoltManager} from "../src/contracts/BoltManager.sol"; +import {BoltEigenLayerMiddleware} from "../src/contracts/BoltEigenLayerMiddleware.sol"; import {IBoltValidators} from "../src/interfaces/IBoltValidators.sol"; -import {IBoltManager} from "../src/interfaces/IBoltManager.sol"; +import {IBoltMiddleware} from "../src/interfaces/IBoltMiddleware.sol"; import {AVSDirectoryStorage} from "@eigenlayer/src/contracts/core/AVSDirectoryStorage.sol"; import {DelegationManagerStorage} from "@eigenlayer/src/contracts/core/DelegationManagerStorage.sol"; @@ -25,6 +26,7 @@ contract BoltManagerEigenLayerTest is Test { BoltValidators public validators; BoltManager public manager; + BoltEigenLayerMiddleware public middleware; EigenLayerDeployer public eigenLayerDeployer; address staker = makeAddr("staker"); @@ -46,13 +48,10 @@ contract BoltManagerEigenLayerTest is Test { // Deploy Bolt contracts validators = new BoltValidators(admin); - manager = new BoltManager( - admin, - address(validators), - address(0), - address(0), - address(0), - address(0), + manager = new BoltManager(admin, address(validators)); + middleware = new BoltEigenLayerMiddleware( + address(admin), + address(manager), address(eigenLayerDeployer.avsDirectory()), address(eigenLayerDeployer.delegationManager()), address(eigenLayerDeployer.strategyManager()) @@ -62,10 +61,10 @@ contract BoltManagerEigenLayerTest is Test { function _adminRoutine() internal { // PART 0: Admin setup -- Collateral whitelist vm.startPrank(admin); - manager.addWhitelistedEigenLayerCollateral(address(eigenLayerDeployer.weth())); + middleware.addWhitelistedCollateral(address(eigenLayerDeployer.weth())); vm.stopPrank(); - assertEq(manager.getWhitelistedEigenLayerCollaterals().length, 1); - assertEq(manager.getWhitelistedEigenLayerCollaterals()[0], address(eigenLayerDeployer.weth())); + assertEq(middleware.getWhitelistedCollaterals().length, 1); + assertEq(middleware.getWhitelistedCollaterals()[0], address(eigenLayerDeployer.weth())); } function _eigenLayerOptInRoutine() internal { @@ -113,7 +112,7 @@ contract BoltManagerEigenLayerTest is Test { // Note that msg.sender which is the ServiceManager contract is used to identify the AVS itself vm.prank(admin); - manager.updateEigenLayerAVSMetadataURI("https://boltprotocol.xyz"); + middleware.updateAVSMetadataURI("https://boltprotocol.xyz"); // 5. As a operator, I can now opt-in into an AVS by interacting with the ServiceManager. // Two steps happen: @@ -130,7 +129,7 @@ contract BoltManagerEigenLayerTest is Test { bytes32 operatorRegistrationDigestHash = eigenLayerDeployer.avsDirectory() .calculateOperatorAVSRegistrationDigestHash({ operator: operator, - avs: address(manager), + avs: address(middleware), salt: bytes32(0), expiry: UINT256_MAX }); @@ -140,10 +139,10 @@ contract BoltManagerEigenLayerTest is Test { ISignatureUtils.SignatureWithSaltAndExpiry(operatorRawSignature, bytes32(0), UINT256_MAX); vm.expectEmit(true, true, true, true); emit IAVSDirectory.OperatorAVSRegistrationStatusUpdated( - operator, address(manager), IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED + operator, address(middleware), IAVSDirectory.OperatorAVSRegistrationStatus.REGISTERED ); - manager.registerEigenLayerOperatorToAVS(operator, operatorSignature); - assertEq(manager.checkIfEigenLayerOperatorRegisteredToAVS(operator), true); + middleware.registerOperatorToAVS(operator, operatorSignature); + assertEq(middleware.checkIfOperatorRegisteredToAVS(operator), true); // PART 2: Validator and proposer opt into BOLT manager // @@ -160,25 +159,25 @@ contract BoltManagerEigenLayerTest is Test { // 2. --- Operator and strategy registration into BoltManager (middleware) --- - manager.registerEigenLayerOperator(operator); - assertEq(manager.isEigenLayerOperatorEnabled(operator), true); + middleware.registerOperator(operator); + assertEq(middleware.isOperatorEnabled(operator), true); - manager.registerEigenLayerStrategy(address(eigenLayerDeployer.wethStrat())); - assertEq(manager.isEigenLayerStrategyEnabled(address(eigenLayerDeployer.wethStrat())), true); + middleware.registerStrategy(address(eigenLayerDeployer.wethStrat())); + assertEq(middleware.isStrategyEnabled(address(eigenLayerDeployer.wethStrat())), true); } function test_deregisterEigenLayerOperatorFromAVS() public { _eigenLayerOptInRoutine(); vm.prank(operator); - manager.deregisterEigenLayerOperatorFromAVS(); - assertEq(manager.checkIfEigenLayerOperatorRegisteredToAVS(operator), false); + middleware.deregisterOperatorFromAVS(); + assertEq(middleware.checkIfOperatorRegisteredToAVS(operator), false); } function test_getEigenLayerOperatorStake() public { _eigenLayerOptInRoutine(); - uint256 amount = manager.getEigenLayerOperatorStake(operator, address(eigenLayerDeployer.weth())); - uint256 totalStake = manager.getEigenLayerTotalStake(2, address(eigenLayerDeployer.weth())); + uint256 amount = middleware.getOperatorStake(operator, address(eigenLayerDeployer.weth())); + uint256 totalStake = middleware.getTotalStake(2, address(eigenLayerDeployer.weth())); assertEq(amount, 1 ether); assertEq(totalStake, 1 ether); } @@ -188,7 +187,7 @@ contract BoltManagerEigenLayerTest is Test { bytes32 pubkeyHash = _pubkeyHash(validatorPubkey); - BoltManager.ProposerStatus memory status = manager.getEigenLayerProposerStatus(pubkeyHash); + IBoltValidators.ProposerStatus memory status = middleware.getProposerStatus(pubkeyHash); assertEq(status.pubkeyHash, pubkeyHash); assertEq(status.operator, operator); assertEq(status.active, true); @@ -213,7 +212,7 @@ contract BoltManagerEigenLayerTest is Test { validators.registerValidatorUnsafe(pubkey, staker, operator); } - BoltManager.ProposerStatus[] memory statuses = manager.getEigenLayerProposersStatus(pubkeyHashes); + IBoltValidators.ProposerStatus[] memory statuses = middleware.getProposersStatus(pubkeyHashes); assertEq(statuses.length, 10); } @@ -223,12 +222,12 @@ contract BoltManagerEigenLayerTest is Test { bytes32 pubkeyHash = bytes32(uint256(1)); vm.expectRevert(IBoltValidators.ValidatorDoesNotExist.selector); - manager.getEigenLayerProposerStatus(pubkeyHash); + middleware.getProposerStatus(pubkeyHash); } function testGetWhitelistedCollaterals() public { _adminRoutine(); - address[] memory collaterals = manager.getWhitelistedEigenLayerCollaterals(); + address[] memory collaterals = middleware.getWhitelistedCollaterals(); assertEq(collaterals.length, 1); assertEq(collaterals[0], address(eigenLayerDeployer.weth())); } @@ -236,13 +235,13 @@ contract BoltManagerEigenLayerTest is Test { function testNonWhitelistedCollateral() public { _adminRoutine(); vm.startPrank(admin); - manager.removeWhitelistedEigenLayerCollateral(address(eigenLayerDeployer.weth())); + middleware.removeWhitelistedCollateral(address(eigenLayerDeployer.weth())); vm.stopPrank(); address strat = address(eigenLayerDeployer.wethStrat()); vm.startPrank(admin); - vm.expectRevert(IBoltManager.CollateralNotWhitelisted.selector); - manager.registerEigenLayerStrategy(strat); + vm.expectRevert(IBoltMiddleware.CollateralNotWhitelisted.selector); + middleware.registerStrategy(strat); vm.stopPrank(); } diff --git a/bolt-contracts/test/BoltManager.Symbiotic.t.sol b/bolt-contracts/test/BoltManager.Symbiotic.t.sol index 8674ecff2..6306bf911 100644 --- a/bolt-contracts/test/BoltManager.Symbiotic.t.sol +++ b/bolt-contracts/test/BoltManager.Symbiotic.t.sol @@ -20,10 +20,12 @@ import {IMigratablesFactory} from "@symbiotic/interfaces/common/IMigratablesFact import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; import {SimpleCollateral} from "@symbiotic/../test/mocks/SimpleCollateral.sol"; -import {IBoltManager} from "../src/interfaces/IBoltManager.sol"; import {IBoltValidators} from "../src/interfaces/IBoltValidators.sol"; +import {IBoltMiddleware} from "../src/interfaces/IBoltMiddleware.sol"; + import {BoltValidators} from "../src/contracts/BoltValidators.sol"; import {BoltManager} from "../src/contracts/BoltManager.sol"; +import {BoltSymbioticMiddleware} from "../src/contracts/BoltSymbioticMiddleware.sol"; import {BLS12381} from "../src/lib/bls/BLS12381.sol"; import {SymbioticSetupFixture} from "./fixtures/SymbioticSetup.f.sol"; @@ -37,6 +39,7 @@ contract BoltManagerTest is Test { BoltValidators public validators; BoltManager public manager; + BoltSymbioticMiddleware public middleware; IVaultFactory public vaultFactory; IDelegatorFactory public delegatorFactory; @@ -146,21 +149,20 @@ contract BoltManagerTest is Test { // --- Deploy Bolt contracts --- validators = new BoltValidators(admin); - manager = new BoltManager( + manager = new BoltManager(admin, address(validators)); + + middleware = new BoltSymbioticMiddleware( admin, - address(validators), + address(manager), networkAdmin, address(operatorRegistry), address(operatorNetworkOptInService), - address(vaultFactory), - address(0), - address(0), - address(0) + address(vaultFactory) ); - // --- Whitelist collateral in BoltManager --- + // --- Whitelist collateral in BoltSymbioticMiddleware --- vm.prank(admin); - manager.addWhitelistedSymbioticCollateral(address(collateral)); + middleware.addWhitelistedCollateral(address(collateral)); } /// @notice Internal helper to register Symbiotic contracts and opt-in operators and vaults. @@ -172,7 +174,7 @@ contract BoltManagerTest is Test { networkRegistry.registerNetwork(); vm.prank(networkAdmin); - networkMiddlewareService.setMiddleware(address(manager)); + networkMiddlewareService.setMiddleware(address(middleware)); // --- Register Validator in BoltValidators --- @@ -201,11 +203,11 @@ contract BoltManagerTest is Test { // --- Register Vault and Operator in BoltManager (middleware) --- - manager.registerSymbioticVault(address(vault)); - assertEq(manager.isSymbioticVaultEnabled(address(vault)), true); + middleware.registerVault(address(vault)); + assertEq(middleware.isVaultEnabled(address(vault)), true); - manager.registerSymbioticOperator(operator); - assertEq(manager.isSymbioticOperatorEnabled(operator), true); + middleware.registerOperator(operator); + assertEq(middleware.isOperatorEnabled(operator), true); // --- Set the stake limit for the Vault --- @@ -249,10 +251,10 @@ contract BoltManagerTest is Test { // initial state uint256 shares = networkRestakeDelegator.totalOperatorNetworkShares(subnetwork); uint256 stakeFromDelegator = networkRestakeDelegator.stake(subnetwork, operator); - uint256 stakeFromManager = manager.getSymbioticOperatorStake(operator, address(collateral)); + uint256 stakeFromMiddleware = middleware.getOperatorStake(operator, address(collateral)); assertEq(shares, 0); - assertEq(stakeFromManager, stakeFromDelegator); - assertEq(stakeFromManager, 0); + assertEq(stakeFromMiddleware, stakeFromDelegator); + assertEq(stakeFromMiddleware, 0); vm.warp(block.timestamp + EPOCH_DURATION + 1); assertEq(vault.currentEpoch(), 1); @@ -279,14 +281,14 @@ contract BoltManagerTest is Test { assertEq(vault.currentEpoch(), 2); // it takes 2 epochs to activate the stake - assertEq(manager.getSymbioticTotalStake(0, address(collateral)), 0); - assertEq(manager.getSymbioticTotalStake(1, address(collateral)), 0); - assertEq(manager.getSymbioticTotalStake(2, address(collateral)), 1 ether); + assertEq(middleware.getTotalStake(0, address(collateral)), 0); + assertEq(middleware.getTotalStake(1, address(collateral)), 0); + assertEq(middleware.getTotalStake(2, address(collateral)), 1 ether); stakeFromDelegator = networkRestakeDelegator.stake(subnetwork, operator); - stakeFromManager = manager.getSymbioticOperatorStake(operator, address(collateral)); - assertEq(stakeFromDelegator, stakeFromManager); - assertEq(stakeFromManager, 1 ether); + stakeFromMiddleware = middleware.getOperatorStake(operator, address(collateral)); + assertEq(stakeFromDelegator, stakeFromMiddleware); + assertEq(stakeFromMiddleware, 1 ether); } function testGetProposerStatus() public { @@ -305,7 +307,7 @@ contract BoltManagerTest is Test { vm.warp(block.timestamp + EPOCH_DURATION * 2 + 1); assertEq(vault.currentEpoch(), 2); - BoltManager.ProposerStatus memory status = manager.getSymbioticProposerStatus(pubkeyHash); + IBoltValidators.ProposerStatus memory status = middleware.getProposerStatus(pubkeyHash); assertEq(status.pubkeyHash, pubkeyHash); assertEq(status.operator, operator); assertEq(status.active, true); @@ -333,7 +335,7 @@ contract BoltManagerTest is Test { vm.warp(block.timestamp + EPOCH_DURATION * 2 + 1); assertEq(vault.currentEpoch(), 2); - BoltManager.ProposerStatus[] memory statuses = manager.getSymbioticProposersStatus(pubkeyHashes); + IBoltValidators.ProposerStatus[] memory statuses = middleware.getProposersStatus(pubkeyHashes); assertEq(statuses.length, 10); } @@ -343,21 +345,21 @@ contract BoltManagerTest is Test { bytes32 pubkeyHash = bytes32(uint256(1)); vm.expectRevert(IBoltValidators.ValidatorDoesNotExist.selector); - manager.getSymbioticProposerStatus(pubkeyHash); + middleware.getProposerStatus(pubkeyHash); } function testGetWhitelistedCollaterals() public view { - address[] memory collaterals = manager.getWhitelistedSymbioticCollaterals(); + address[] memory collaterals = middleware.getWhitelistedCollaterals(); assertEq(collaterals.length, 1); assertEq(collaterals[0], address(collateral)); } function testNonWhitelistedCollateral() public { vm.prank(admin); - manager.removeWhitelistedSymbioticCollateral(address(collateral)); + middleware.removeWhitelistedCollateral(address(collateral)); vm.prank(vaultAdmin); - vm.expectRevert(IBoltManager.CollateralNotWhitelisted.selector); - manager.registerSymbioticVault(address(vault)); + vm.expectRevert(IBoltMiddleware.CollateralNotWhitelisted.selector); + middleware.registerVault(address(vault)); } }