Skip to content

Commit

Permalink
feat: support avs<>operator mapping with new APIs and events
Browse files Browse the repository at this point in the history
  • Loading branch information
bowenli86 committed Dec 6, 2023
1 parent 25aa120 commit 3868f04
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 1 deletion.
95 changes: 95 additions & 0 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,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(
avsOperatorStatus[msg.sender][operator] != OperatorAVSRegistrationStatus.REGISTERED,
"DelegationManager.registerOperatorToAVS: operator already registered"
);
require(
!operatorSaltIsSpent[operator][operatorSignature.salt],
"DelegationManager.registerOperatorToAVS: salt already spent"
);

// 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
);

// Set the operator as registered
avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.REGISTERED;

// Mark the salt as spent
operatorSaltIsSpent[operator][operatorSignature.salt] = true;

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
*******************************************************************************/
Expand Down Expand Up @@ -905,6 +976,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.
*/
Expand Down
11 changes: 11 additions & 0 deletions src/contracts/core/DelegationManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -93,6 +97,13 @@ abstract contract DelegationManagerStorage is IDelegationManager {
/// See conversation here: https://github.com/Layr-Labs/eigenlayer-contracts/pull/365/files#r1417525270
address private __deprecated_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;
Expand Down
58 changes: 58 additions & 0 deletions src/contracts/interfaces/IDelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,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);

Expand All @@ -118,6 +124,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);

Expand Down Expand Up @@ -320,6 +329,52 @@ 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);

/**
* @notice returns the address of the operator that `staker` is delegated to.
* @notice Mapping: staker => operator whom the staker is currently delegated to.
Expand Down Expand Up @@ -435,6 +490,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.
*
Expand Down
9 changes: 9 additions & 0 deletions src/test/events/IDelegationManagerEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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);

Expand Down
16 changes: 15 additions & 1 deletion src/test/mocks/DelegationManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,20 +106,34 @@ 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) {}

function STAKER_DELEGATION_TYPEHASH() external view returns (bytes32) {}

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) {}
Expand Down
95 changes: 95 additions & 0 deletions src/test/unit/DelegationUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,28 @@ contract DelegationManagerUnitTests is EigenLayerUnitTestSetup, IDelegationManag
return stakerSignatureAndExpiry;
}

/**
* @notice internal function for calculating a signature from the operator corresponding to `_operatorPrivateKey`, delegating them to
* the `operator`, and expiring at `expiry`.
*/
function _getOperatorSignature(
uint256 _operatorPrivateKey,
address operator,
address avs,
bytes32 salt,
uint256 expiry
) internal view returns (ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature) {
operatorSignature.expiry = expiry;
operatorSignature.salt = salt;
{
bytes32 digestHash = delegationManager.calculateOperatorAVSRegistrationDigestHash(operator, avs, salt, expiry);
(uint8 v, bytes32 r, bytes32 s) = cheats.sign(_operatorPrivateKey, digestHash);
operatorSignature.signature = abi.encodePacked(r, s, v);
}
return operatorSignature;
}


// @notice Assumes operator does not have a delegation approver & staker != approver
function _delegateToOperatorWhoAcceptsAllStakers(address staker, address operator) internal {
ISignatureUtils.SignatureWithExpiry memory approverSignatureAndExpiry;
Expand Down Expand Up @@ -569,7 +591,9 @@ contract DelegationManagerUnitTests_RegisterModifyOperator is DelegationManagerU
emit OperatorMetadataURIUpdated(defaultOperator, metadataURI);
delegationManager.updateOperatorMetadataURI(metadataURI);
}
}

contract DelegationManagerUnitTests_operatorAVSRegisterationStatus is DelegationManagerUnitTests {
// @notice Tests that an avs who calls `updateAVSMetadataURI` will correctly see an `AVSMetadataURIUpdated` event emitted with their input
function testFuzz_UpdateAVSMetadataURI(string memory metadataURI) public {
// call `updateAVSMetadataURI` and check for event
Expand All @@ -578,6 +602,77 @@ contract DelegationManagerUnitTests_RegisterModifyOperator is DelegationManagerU
emit AVSMetadataURIUpdated(defaultAVS, metadataURI);
delegationManager.updateAVSMetadataURI(metadataURI);
}

// @notice Verifies an operator registers successfull to avs and see an `OperatorAVSRegistrationStatusUpdated` event emitted
function testFuzz_registerOperatorToAVS(bytes32 salt) public {
cheats.prank(defaultAVS);
cheats.expectEmit(true, true, true, true, address(delegationManager));

address operator = cheats.addr(delegationSignerPrivateKey);
emit OperatorAVSRegistrationStatusUpdated(operator, defaultAVS, OperatorAVSRegistrationStatus.REGISTERED);

uint256 expiry = type(uint256).max;
ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature = _getOperatorSignature(
delegationSignerPrivateKey,
operator,
defaultAVS,
salt,
expiry
);

delegationManager.registerOperatorToAVS(operator, operatorSignature);
cheats.stopPrank();
}

// @notice Verifies an operator registers fails when the signature is not from the operator
function testFuzz_revert_whenSignatureAddressIsNotOperator(bytes32 salt) public {
cheats.prank(defaultAVS);
address operator = cheats.addr(delegationSignerPrivateKey);

uint256 expiry = type(uint256).max;
ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature = _getOperatorSignature(
delegationSignerPrivateKey,
operator,
defaultAVS,
salt,
expiry
);

cheats.prank(operator);
cheats.expectRevert("EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer");
delegationManager.registerOperatorToAVS(operator, operatorSignature);
cheats.stopPrank();
}

// @notice Verifies an operator registers fails when the signature expiry already expires
function testFuzz_revert_whenExpiryHasExpired(bytes32 salt, uint256 expiry, ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature) public {
cheats.prank(defaultAVS);
address operator = cheats.addr(delegationSignerPrivateKey);
cheats.assume(operatorSignature.expiry < block.timestamp);

cheats.expectRevert("DelegationManager.registerOperatorToAVS: operator signature expired");
delegationManager.registerOperatorToAVS(operator, operatorSignature);
}

// @notice Verifies an operator registers fails when it's already registered to the avs
function testFuzz_revert_whenOperatorAlreadyRegisteredToAVS(bytes32 salt) public {
cheats.prank(defaultAVS);
address operator = cheats.addr(delegationSignerPrivateKey);
uint256 expiry = type(uint256).max;
ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature = _getOperatorSignature(
delegationSignerPrivateKey,
operator,
defaultAVS,
salt,
expiry
);

delegationManager.registerOperatorToAVS(operator, operatorSignature);

cheats.expectRevert("DelegationManager.registerOperatorToAVS: operator already registered");
delegationManager.registerOperatorToAVS(operator, operatorSignature);
cheats.stopPrank();
}
}

contract DelegationManagerUnitTests_delegateTo is DelegationManagerUnitTests {
Expand Down

0 comments on commit 3868f04

Please sign in to comment.