diff --git a/smart-contracts/src/holesky/contracts/BoltEigenLayerMiddlewareV1.sol b/smart-contracts/src/holesky/contracts/BoltEigenLayerMiddlewareV1.sol index ed14ae1..e33757f 100644 --- a/smart-contracts/src/holesky/contracts/BoltEigenLayerMiddlewareV1.sol +++ b/smart-contracts/src/holesky/contracts/BoltEigenLayerMiddlewareV1.sol @@ -10,8 +10,10 @@ import {PauseableEnumerableSet} from "@symbiotic/middleware-sdk/libraries/Pausea import { IAllocationManager, IAllocationManagerTypes } from "@eigenlayer/src/contracts/interfaces/IAllocationManager.sol"; +import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol"; import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; +import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; import {IAVSRegistrar} from "@eigenlayer/src/contracts/interfaces/IAVSRegistrar.sol"; import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; @@ -32,6 +34,9 @@ contract BoltEigenLayerMiddlewareV1 is { using PauseableEnumerableSet for PauseableEnumerableSet.AddressSet; + /// @notice Address of the EigenLayer AVS Directory contract. + IAVSDirectory public AVS_DIRECTORY; + /// @notice Address of the EigenLayer Allocation Manager contract. IAllocationManager public ALLOCATION_MANAGER; @@ -58,7 +63,7 @@ contract BoltEigenLayerMiddlewareV1 is * * Total storage slots: 50 */ - uint256[44] private __gap; + uint256[42] private __gap; // ========= Events ========= // @@ -74,16 +79,35 @@ contract BoltEigenLayerMiddlewareV1 is /// @notice Emitted when a strategy is removed from the whitelist event StrategyRemovedFromWhitelist(address strategy); + // ========= Errors ========= // + + /// @notice Error thrown when an invalid strategy address is provided + error InvalidStrategyAddress(); + + /// @notice Error thrown when a strategy is already whitelisted + error StrategyAlreadyWhitelisted(); + + /// @notice Error thrown when a strategy is not whitelisted + error StrategyNotWhitelisted(); + + /// @notice Error thrown when an unallowed caller calls a function that requires an operator + error NotOperator(); + + /// @notice Error thrown when an unallowed caller calls a function that requires the AllocationManager + error NotAllocationManager(); + // ========= Initializer & Proxy functionality ========= // /// @notice Initialize the contract /// @param owner The address of the owner + /// @param _avsDirectory The address of the EigenLayer AVS Directory contract /// @param _eigenlayerAllocationManager The address of the EigenLayer Allocation Manager contract /// @param _eigenlayerDelegationManager The address of the EigenLayer Delegation Manager contract /// @param _eigenlayerStrategyManager The address of the EigenLayer Strategy Manager contract /// @param _operatorsRegistry The address of the Operators Registry contract function initialize( address owner, + IAVSDirectory _avsDirectory, IAllocationManager _eigenlayerAllocationManager, IDelegationManager _eigenlayerDelegationManager, IStrategyManager _eigenlayerStrategyManager, @@ -91,6 +115,7 @@ contract BoltEigenLayerMiddlewareV1 is ) public initializer { __Ownable_init(owner); + AVS_DIRECTORY = _avsDirectory; ALLOCATION_MANAGER = _eigenlayerAllocationManager; DELEGATION_MANAGER = _eigenlayerDelegationManager; STRATEGY_MANAGER = _eigenlayerStrategyManager; @@ -104,6 +129,14 @@ contract BoltEigenLayerMiddlewareV1 is address newImplementation ) internal override onlyOwner {} + // ========= modifiers ========= // + + /// @notice Modifier to check if the caller is the AllocationManager + modifier onlyAllocationManager() { + require(msg.sender == address(ALLOCATION_MANAGER), NotAllocationManager()); + _; + } + // ========= AVS functions ========= // /// @notice Get the collaterals and amounts staked by an operator across the whitelisted strategies @@ -116,6 +149,8 @@ contract BoltEigenLayerMiddlewareV1 is // Use the beginning of the current epoch to check which strategies were enabled at that time. // Only the strategies enabled at the beginning of the epoch are considered for the operator's collateral. uint48 timestamp = OPERATORS_REGISTRY.getCurrentEpochStartTimestamp(); + + // Only take strategies that are active at IStrategy[] memory activeStrategies = _getActiveStrategiesAt(timestamp); address[] memory collateralTokens = new address[](activeStrategies.length); @@ -138,9 +173,33 @@ contract BoltEigenLayerMiddlewareV1 is /// @param collateral The collateral token address /// @return The amount staked by the operator for the collateral function getOperatorStake(address operator, address collateral) public view returns (uint256) { - // TODO: impl + // Use the beginning of the current epoch to check which strategies were enabled at that time. + // Only the strategies enabled at the beginning of the epoch are considered for the operator's stake. + uint48 timestamp = OPERATORS_REGISTRY.getCurrentEpochStartTimestamp(); + + uint256 activeStake = 0; + for (uint256 i = 0; i < whitelistedStrategies.length(); i++) { + (address strategy, uint48 enabledAt, uint48 disabledAt) = whitelistedStrategies.at(i); - return 0; + if (!_wasEnabledAt(enabledAt, disabledAt, timestamp)) { + continue; + } + + if (address(IStrategy(strategy).underlyingToken()) != collateral) { + continue; + } + + // get the shares of the operator for the strategy + // NOTE: the EL slashing-magnitudes branch removed the operatorShares(operator, strategy) function: + // https://github.com/Layr-Labs/eigenlayer-contracts/blob/dev/src/contracts/interfaces/IDelegationManager.sol#L352-L359 + // so we need to use getOperatorShares(operator, [strategy]) instead. + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = IStrategy(strategy); + uint256[] memory shares = DELEGATION_MANAGER.getOperatorShares(operator, strategies); + activeStake += IStrategy(strategy).sharesToUnderlyingView(shares[0]); + } + + return activeStake; } /// @notice Get the list of whitelisted strategies for this AVS and whether they are enabled @@ -162,25 +221,61 @@ contract BoltEigenLayerMiddlewareV1 is return (strategies, enabled); } + /// @notice Get the active strategies for this AVS + /// @return The active strategies + function getActiveStrategies() public view returns (IStrategy[] memory) { + // Use the beginning of the current epoch to check which strategies were enabled at that time. + uint48 timestamp = OPERATORS_REGISTRY.getCurrentEpochStartTimestamp(); + return _getActiveStrategiesAt(timestamp); + } + + // ========= [pre-slashing] AVS Registration functions ========= // + + /// @notice Register an operator through the AVS Directory + /// @param rpcEndpoint The RPC URL of the operator + /// @param extraData Arbitrary data the operator can provide as part of registration + /// @param operatorSignature The signature of the operator + /// @dev This function is used before the ELIP-002 (slashing) EigenLayer upgrade to register operators. + /// @dev Operators must use this function to register before the upgrade. After the upgrade, this will be removed. + function registerThroughAVSDirectory( + string memory rpcEndpoint, + string memory extraData, + ISignatureUtils.SignatureWithSaltAndExpiry calldata operatorSignature + ) public { + address operator = msg.sender; + + require(DELEGATION_MANAGER.isOperator(operator), NotOperator()); + + AVS_DIRECTORY.registerOperatorToAVS(operator, operatorSignature); + OPERATORS_REGISTRY.registerOperator(operator, rpcEndpoint, extraData); + } + // ========= AVS Registrar functions ========= // /// @notice Allows the AllocationManager to hook into the middleware to validate operator registration /// @param operator The address of the operator /// @param operatorSetIds The operator set IDs the operator is registering for - /// @param data Arbitrary data the operator can provide as part of registration - function registerOperator(address operator, uint32[] calldata operatorSetIds, bytes calldata data) external { + /// @param data Arbitrary ABI-encoded data the operator must provide as part of registration + function registerOperator( + address operator, + uint32[] calldata operatorSetIds, + bytes calldata data + ) external onlyAllocationManager { // NOTE: this function is called by AllocationManager.registerForOperatorSets(), // called by operators when registering to this AVS. If this call reverts, // the registration will be unsuccessful. + // Split the data into rpcEndpoint and extraData from ABI encoding + (string memory rpcEndpoint, string memory extraData) = abi.decode(data, (string, string)); + // We forward the call to the OperatorsRegistry to register the operator in its storage. - OPERATORS_REGISTRY.registerOperator(operator, string(data), ""); + OPERATORS_REGISTRY.registerOperator(operator, rpcEndpoint, extraData); } /// @notice Allows the AllocationManager to hook into the middleware to validate operator deregistration /// @param operator The address of the operator /// @param operatorSetIds The operator set IDs the operator is deregistering from - function deregisterOperator(address operator, uint32[] calldata operatorSetIds) external { + function deregisterOperator(address operator, uint32[] calldata operatorSetIds) external onlyAllocationManager { // NOTE: this function is called by AllocationManager.deregisterFromOperatorSets, // called by operators when deregistering from this AVS. // Failure does nothing here: if this call reverts the deregistration will still go through. @@ -198,9 +293,9 @@ contract BoltEigenLayerMiddlewareV1 is function addStrategyToWhitelist( address strategy ) public onlyOwner { - require(strategy != address(0), "Invalid strategy address"); - require(!whitelistedStrategies.contains(strategy), "Strategy already whitelisted"); - require(STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy)), "Strategy not allowed"); + require(strategy != address(0), InvalidStrategyAddress()); + require(!whitelistedStrategies.contains(strategy), StrategyAlreadyWhitelisted()); + require(STRATEGY_MANAGER.strategyIsWhitelistedForDeposit(IStrategy(strategy)), StrategyNotWhitelisted()); whitelistedStrategies.register(Time.timestamp(), strategy); emit StrategyAddedToWhitelist(strategy); @@ -211,7 +306,7 @@ contract BoltEigenLayerMiddlewareV1 is function pauseStrategy( address strategy ) public onlyOwner { - require(whitelistedStrategies.contains(strategy), "Strategy not whitelisted"); + require(whitelistedStrategies.contains(strategy), StrategyNotWhitelisted()); whitelistedStrategies.pause(Time.timestamp(), strategy); emit StrategyPaused(strategy); @@ -222,7 +317,7 @@ contract BoltEigenLayerMiddlewareV1 is function unpauseStrategy( address strategy ) public onlyOwner { - require(whitelistedStrategies.contains(strategy), "Strategy not whitelisted"); + require(whitelistedStrategies.contains(strategy), StrategyNotWhitelisted()); whitelistedStrategies.unpause(Time.timestamp(), OPERATORS_REGISTRY.EPOCH_DURATION(), strategy); emit StrategyUnpaused(strategy); @@ -234,7 +329,7 @@ contract BoltEigenLayerMiddlewareV1 is function removeStrategyFromWhitelist( address strategy ) public onlyOwner { - require(whitelistedStrategies.contains(strategy), "Strategy not whitelisted"); + require(whitelistedStrategies.contains(strategy), StrategyNotWhitelisted()); whitelistedStrategies.unregister(Time.timestamp(), OPERATORS_REGISTRY.EPOCH_DURATION(), strategy); emit StrategyRemovedFromWhitelist(strategy); @@ -269,11 +364,16 @@ contract BoltEigenLayerMiddlewareV1 is } /// @notice Update the metadata URI for this AVS + /// @param contractName The name of the contract to update the metadata URI for /// @param metadataURI The new metadata URI - function updateAVSMetadataURI( - string calldata metadataURI - ) public onlyOwner { - ALLOCATION_MANAGER.updateAVSMetadataURI(address(this), metadataURI); + function updateAVSMetadataURI(string calldata contractName, string calldata metadataURI) public onlyOwner { + bytes32 contractNameHash = keccak256(abi.encodePacked(contractName)); + + if (contractNameHash == keccak256("ALLOCATION_MANAGER")) { + ALLOCATION_MANAGER.updateAVSMetadataURI(address(this), metadataURI); + } else if (contractNameHash == keccak256("AVS_DIRECTORY")) { + AVS_DIRECTORY.updateAVSMetadataURI(metadataURI); + } } // ========== Internal helpers ========== // @@ -285,7 +385,7 @@ contract BoltEigenLayerMiddlewareV1 is IStrategy[] calldata strategies ) internal view { for (uint256 i = 0; i < strategies.length; i++) { - require(whitelistedStrategies.contains(address(strategies[i])), "Strategy not whitelisted"); + require(whitelistedStrategies.contains(address(strategies[i])), StrategyNotWhitelisted()); } } @@ -296,12 +396,12 @@ contract BoltEigenLayerMiddlewareV1 is uint48 timestamp ) internal view returns (IStrategy[] memory) { uint256 activeCount = 0; - IStrategy[] memory activeStrategies = new IStrategy[](whitelistedStrategies.length()); + address[] memory activeStrategies = new address[](whitelistedStrategies.length()); for (uint256 i = 0; i < whitelistedStrategies.length(); i++) { (address strategy, uint48 enabledAt, uint48 disabledAt) = whitelistedStrategies.at(i); if (_wasEnabledAt(enabledAt, disabledAt, timestamp)) { - activeStrategies[activeCount] = IStrategy(strategy); + activeStrategies[activeCount] = strategy; activeCount++; } } @@ -309,8 +409,9 @@ contract BoltEigenLayerMiddlewareV1 is // Resize the array to the actual number of active strategies IStrategy[] memory result = new IStrategy[](activeCount); for (uint256 i = 0; i < activeCount; i++) { - result[i] = activeStrategies[i]; + result[i] = IStrategy(activeStrategies[i]); } + return result; } /// @notice Check if a map entry was active at a given timestamp. diff --git a/smart-contracts/test/holesky/BoltEigenLayerMiddlewareV1.t.sol b/smart-contracts/test/holesky/BoltEigenLayerMiddlewareV1.t.sol index cc57b86..24b8b2e 100644 --- a/smart-contracts/test/holesky/BoltEigenLayerMiddlewareV1.t.sol +++ b/smart-contracts/test/holesky/BoltEigenLayerMiddlewareV1.t.sol @@ -11,12 +11,13 @@ import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegati import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; import {IStrategy} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; +import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; import {OperatorSet} from "@eigenlayer/src/contracts/libraries/OperatorSetLib.sol"; import {OperatorsRegistryV1} from "../../src/holesky/contracts/OperatorsRegistryV1.sol"; import {IOperatorsRegistryV1} from "../../src/holesky/interfaces/IOperatorsRegistryV1.sol"; import {BoltEigenLayerMiddlewareV1} from "../../src/holesky/contracts/BoltEigenLayerMiddlewareV1.sol"; -import {IWETH} from "./util/IWETH.sol"; +import {IWETH} from "../util/IWETH.sol"; contract BoltEigenLayerMiddlewareV1Test is Test { OperatorsRegistryV1 registry; @@ -29,6 +30,7 @@ contract BoltEigenLayerMiddlewareV1Test is Test { IAllocationManager holeskyAllocationManager = IAllocationManager(0x78469728304326CBc65f8f95FA756B0B73164462); IDelegationManager holeskyDelegationManager = IDelegationManager(0xA44151489861Fe9e3055d95adC98FbD462B948e7); IStrategyManager holeskyStrategyManager = IStrategyManager(0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6); + IAVSDirectory holeskyAVSDirectory = IAVSDirectory(0x055733000064333CaDDbC92763c58BF0192fFeBf); IERC20 holeskyStEth = IERC20(0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034); IStrategy holeskyStEthStrategy = IStrategy(0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3); @@ -56,13 +58,27 @@ contract BoltEigenLayerMiddlewareV1Test is Test { // --- Deploy the EL middleware --- middleware = new BoltEigenLayerMiddlewareV1(); middleware.initialize( - admin, holeskyAllocationManager, holeskyDelegationManager, holeskyStrategyManager, registry + admin, + holeskyAVSDirectory, + holeskyAllocationManager, + holeskyDelegationManager, + holeskyStrategyManager, + registry ); // 1. Whitelist the strategies in the middleware middleware.addStrategyToWhitelist(address(holeskyStEthStrategy)); middleware.addStrategyToWhitelist(address(holeskyWethStrategy)); + // check that the strategies are whitelisted + (address[] memory whitelistedStrategies, bool[] memory statuses) = middleware.getWhitelistedStrategies(); + assertEq(whitelistedStrategies.length, 2); + assertEq(statuses.length, 2); + assertEq(whitelistedStrategies[0], address(holeskyStEthStrategy)); + assertEq(whitelistedStrategies[1], address(holeskyWethStrategy)); + assertEq(statuses[0], true); + assertEq(statuses[1], true); + IStrategy[] memory strategies = new IStrategy[](2); strategies[0] = holeskyStEthStrategy; strategies[1] = holeskyWethStrategy; @@ -77,28 +93,28 @@ contract BoltEigenLayerMiddlewareV1Test is Test { registry.updateRestakingMiddleware("EIGENLAYER", middleware); // 4. Update the metadata URI - middleware.updateAVSMetadataURI("https://grugbrain.dev"); + middleware.updateAVSMetadataURI("ALLOCATION_MANAGER", "https://grugbrain.dev"); vm.stopPrank(); } /// Helper function to register an operator - function _registerOperator(address signer, string memory rpcEndpoint) public { + function _registerOperator(address signer, string memory rpcEndpoint, string memory extraData) public { // 1. Register the new EL operator in DelegationManager uint32 allocationDelay = 1; address delegationApprover = address(0x0); // this is optional, skip it string memory uri = "some-meetadata.uri.com"; - vm.prank(operator); + vm.prank(signer); holeskyDelegationManager.registerAsOperator(delegationApprover, allocationDelay, uri); // 2. Register the operator in the bolt AVS - vm.prank(operator); + vm.prank(signer); holeskyAllocationManager.registerForOperatorSets( - operator, + signer, IAllocationManagerTypes.RegisterParams({ avs: address(middleware), operatorSetIds: new uint32[](0), // specify the newly created operator set - data: bytes(rpcEndpoint) + data: abi.encode(rpcEndpoint, extraData) }) ); @@ -108,7 +124,7 @@ contract BoltEigenLayerMiddlewareV1Test is Test { } function testRegister() public { - _registerOperator(operator, "http://stopjava.com"); + _registerOperator(operator, "http://stopjava.com", "operator1"); } function testRegisterAndDepositCollateral() public { @@ -127,7 +143,7 @@ contract BoltEigenLayerMiddlewareV1Test is Test { assertEq(holeskyWethStrategy.sharesToUnderlyingView(shares), 100 ether); // 2. Register the operator in both EL and the bolt AVS - _registerOperator(operator, "http://stopjava.com"); + _registerOperator(operator, "http://stopjava.com", "operator1"); // 3. Delegate funds from the staker to the operator vm.prank(staker); @@ -152,6 +168,10 @@ contract BoltEigenLayerMiddlewareV1Test is Test { vm.prank(operator); holeskyAllocationManager.modifyAllocations(operator, allocs); + // make sure the strategies are active in the middleware + IStrategy[] memory activeStrats = middleware.getActiveStrategies(); + assertEq(activeStrats.length, 2); + // 5. try to read the operator's collateral directly on the avs (address[] memory collaterals, uint256[] memory amounts) = middleware.getOperatorCollaterals(operator); assertEq(collaterals.length, 2); diff --git a/smart-contracts/test/holesky/util/IWETH.sol b/smart-contracts/test/util/IWETH.sol similarity index 100% rename from smart-contracts/test/holesky/util/IWETH.sol rename to smart-contracts/test/util/IWETH.sol