diff --git a/src/contracts/core/DelegationManager.sol b/src/contracts/core/DelegationManager.sol index 3fc1befc95..4ed142b6be 100644 --- a/src/contracts/core/DelegationManager.sol +++ b/src/contracts/core/DelegationManager.sol @@ -450,6 +450,77 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg } } + /** + * @notice Called by an avs to register an operator with the avs. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature) external { + + require( + operatorSignature.expiry >= block.timestamp, + "DelegationManager.registerOperatorToAVS: operator signature expired" + ); + require( + !operatorSaltIsSpent[operator][operatorSignature.salt], + "DelegationManager.registerOperatorToAVS: salt already spent" + ); + require( + avsOperatorStatus[msg.sender][operator] == OperatorAVSRegistrationStatus.REGISTERED, + "DelegationManager.registerOperatorWithAVS: operator already registered" + ); + + // Calculate the digest hash + bytes32 operatorRegistrationDigestHash = calculateOperatorAVSRegistrationDigestHash({ + operator: operator, + avs: msg.sender, + salt: operatorSignature.salt, + expiry: operatorSignature.expiry + }); + + // Check that the signature is valid + EIP1271SignatureUtils.checkSignature_EIP1271( + operator, + operatorRegistrationDigestHash, + operatorSignature.signature + ); + + // Mark the salt as spent + operatorSaltIsSpent[operator][operatorSignature.salt] = true; + + // Set the operator as registered + avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.REGISTERED; + + emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.REGISTERED); + } + + /** + * @notice Called by an avs to deregister an operator with the avs. + * @param operator The address of the operator to deregister. + */ + function deregisterOperatorFromAVS(address operator) external { + require( + avsOperatorStatus[msg.sender][operator] == OperatorAVSRegistrationStatus.REGISTERED, + "DelegationManager.deregisterOperatorFromAVS: operator not registered" + ); + + // Set the operator as deregistered + avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.UNREGISTERED; + + emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.UNREGISTERED); + } + + /** + * @notice Returns whether or not an operator is registered to an avs. + * @param operator The address of the operator. + * @param avs The address of the avs. + */ + function isRegisteredToAVS(address operator, address avs) external view returns (bool) { + return avsOperatorStatus[avs][operator] == OperatorAVSRegistrationStatus.REGISTERED; + } + /******************************************************************************* INTERNAL FUNCTIONS *******************************************************************************/ @@ -947,6 +1018,30 @@ contract DelegationManager is Initializable, OwnableUpgradeable, Pausable, Deleg return approverDigestHash; } + /** + * @notice Calculates the digest hash to be signed by an operator to register with an AVS + * @param operator The account registering as an operator + * @param avs The AVS the operator is registering to + * @param salt A unique and single use value associated with the approver signature. + * @param expiry Time after which the approver's signature becomes invalid + */ + function calculateOperatorAVSRegistrationDigestHash( + address operator, + address avs, + bytes32 salt, + uint256 expiry + ) public view returns (bytes32) { + // calculate the struct hash + bytes32 structHash = keccak256( + abi.encode(OPERATOR_AVS_REGISTRATION_TYPEHASH, operator, avs, salt, expiry) + ); + // calculate the digest hash + bytes32 digestHash = keccak256( + abi.encodePacked("\x19\x01", domainSeparator(), structHash) + ); + return digestHash; + } + /** * @dev Recalculates the domain separator when the chainid changes due to a fork. */ diff --git a/src/contracts/core/DelegationManagerStorage.sol b/src/contracts/core/DelegationManagerStorage.sol index 789db18f0b..8beb2319e7 100644 --- a/src/contracts/core/DelegationManagerStorage.sol +++ b/src/contracts/core/DelegationManagerStorage.sol @@ -25,6 +25,10 @@ abstract contract DelegationManagerStorage is IDelegationManager { bytes32 public constant DELEGATION_APPROVAL_TYPEHASH = keccak256("DelegationApproval(address staker,address operator,bytes32 salt,uint256 expiry)"); + /// @notice The EIP-712 typehash for the `Registration` struct used by the contract + bytes32 public constant OPERATOR_AVS_REGISTRATION_TYPEHASH = + keccak256("OperatorAVSRegistration(address operator,address avs,uint256 expiry)"); + /** * @notice Original EIP-712 Domain separator for this contract. * @dev The domain separator may change in the event of a fork that modifies the ChainID. @@ -92,6 +96,13 @@ abstract contract DelegationManagerStorage is IDelegationManager { /// @notice the address of the StakeRegistry contract to call for stake updates when operator shares are changed IStakeRegistryStub public stakeRegistry; + /// @notice Mapping: AVS => operator => enum of operator status to the AVS + mapping(address => mapping(address => OperatorAVSRegistrationStatus)) public avsOperatorStatus; + + /// @notice Mapping: operator => 32-byte salt => whether or not the salt has already been used by the operator. + /// @dev Salt is used in the `registerOperatorToAVS` function. + mapping(address => mapping(bytes32 => bool)) public operatorSaltIsSpent; + constructor(IStrategyManager _strategyManager, ISlasher _slasher, IEigenPodManager _eigenPodManager) { strategyManager = _strategyManager; eigenPodManager = _eigenPodManager; diff --git a/src/contracts/interfaces/IDelegationManager.sol b/src/contracts/interfaces/IDelegationManager.sol index f28b86fe8e..c3ac47baaa 100644 --- a/src/contracts/interfaces/IDelegationManager.sol +++ b/src/contracts/interfaces/IDelegationManager.sol @@ -104,6 +104,12 @@ interface IDelegationManager is ISignatureUtils { address withdrawer; } + /// @notice Enum representing the status of an operator's registration with an AVS + enum OperatorAVSRegistrationStatus { + UNREGISTERED, // Operator not registered to AVS + REGISTERED // Operator registered to AVS + } + // @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails. event OperatorRegistered(address indexed operator, OperatorDetails operatorDetails); @@ -122,6 +128,9 @@ interface IDelegationManager is ISignatureUtils { */ event AVSMetadataURIUpdated(address indexed avs, string metadataURI); + /// @notice Emitted when an operator's registration status for an AVS is updated + event OperatorAVSRegistrationStatusUpdated(address indexed operator, address indexed avs, OperatorAVSRegistrationStatus status); + /// @notice Emitted whenever an operator's shares are increased for a given strategy. Note that shares is the delta in the operator's shares. event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); @@ -324,6 +333,49 @@ interface IDelegationManager is ISignatureUtils { uint256 shares ) external; + /** + * @notice Called by an avs to register an operator with the avs. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external; + + /** + * @notice Called by an avs to deregister an operator with the avs. + * @param operator The address of the operator to deregister. + */ + function deregisterOperatorFromAVS(address operator) external; + + /** + * @notice Returns whether or not an operator is registered to an avs. + * @param operator The address of the operator. + * @param avs The address of the avs. + */ + function isRegisteredToAVS(address operator, address avs) external view returns (bool); + + /** + * @notice Returns whether or not the salt has already been used by the operator. + * @dev Salts is used in the `registerOperatorToAVS` function. + */ + function operatorSaltIsSpent(address avs, bytes32 salt) external view returns (bool); + + /** + * @notice Calculates the digest hash to be signed by an operator to register with an AVS + * @param operator The account registering as an operator + * @param avs The AVS the operator is registering to + * @param salt A unique and single use value associated with the approver signature. + * @param expiry Time after which the approver's signature becomes invalid + */ + function calculateOperatorAVSRegistrationDigestHash( + address operator, + address avs, + bytes32 salt, + uint256 expiry + ) external view returns (bytes32); + /// @notice the address of the StakeRegistry contract to call for stake updates when operator shares are changed function stakeRegistry() external view returns (IStakeRegistryStub); @@ -442,6 +494,9 @@ interface IDelegationManager is ISignatureUtils { /// @notice The EIP-712 typehash for the DelegationApproval struct used by the contract function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32); + /// @notice The EIP-712 typehash for the Registration struct used by the contract + function OPERATOR_AVS_REGISTRATION_TYPEHASH() external view returns (bytes32); + /** * @notice Getter function for the current EIP-712 domain separator for this contract. * diff --git a/src/test/events/IDelegationManagerEvents.sol b/src/test/events/IDelegationManagerEvents.sol index e82d9c458a..7667823393 100644 --- a/src/test/events/IDelegationManagerEvents.sol +++ b/src/test/events/IDelegationManagerEvents.sol @@ -26,6 +26,15 @@ interface IDelegationManagerEvents { */ event AVSMetadataURIUpdated(address indexed avs, string metadataURI); + /// @notice Enum representing the status of an operator's registration with an AVS + enum OperatorAVSRegistrationStatus { + UNREGISTERED, // Operator not registered to AVS + REGISTERED // Operator registered to AVS + } + + /// @notice Emitted when an operator's registration status for an AVS is updated + event OperatorAVSRegistrationStatusUpdated(address indexed operator, address indexed avs, OperatorAVSRegistrationStatus status); + /// @notice Emitted whenever an operator's shares are increased for a given strategy event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); diff --git a/src/test/mocks/DelegationManagerMock.sol b/src/test/mocks/DelegationManagerMock.sol index de9749993c..c287733c16 100644 --- a/src/test/mocks/DelegationManagerMock.sol +++ b/src/test/mocks/DelegationManagerMock.sol @@ -109,7 +109,11 @@ contract DelegationManagerMock is IDelegationManager, Test { function calculateStakerDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) external pure returns (bytes32 stakerDigestHash) {} - function calculateApproverDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) external pure returns (bytes32 approverDigestHash) {} + function calculateApproverDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/) + external pure returns (bytes32 approverDigestHash) {} + + function calculateOperatorAVSRegistrationDigestHash(address /*operator*/, address /*avs*/, bytes32 /*salt*/, uint256 /*expiry*/) + external pure returns (bytes32 digestHash) {} function DOMAIN_TYPEHASH() external view returns (bytes32) {} @@ -117,12 +121,22 @@ contract DelegationManagerMock is IDelegationManager, Test { function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32) {} + function OPERATOR_AVS_REGISTRATION_TYPEHASH() external view returns (bytes32) {} + function domainSeparator() external view returns (bytes32) {} function cumulativeWithdrawalsQueued(address staker) external view returns (uint256) {} function calculateWithdrawalRoot(Withdrawal memory withdrawal) external pure returns (bytes32) {} + function registerOperatorToAVS(address operator, ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature) external {} + + function deregisterOperatorFromAVS(address operator) external {} + + function isRegisteredToAVS(address operator, address avs) external view returns (bool) {} + + function operatorSaltIsSpent(address avs, bytes32 salt) external view returns (bool) {} + function queueWithdrawals( QueuedWithdrawalParams[] calldata queuedWithdrawalParams ) external returns (bytes32[] memory) {}