Skip to content

Feat/operator sets #579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5dbd94a
feat: operator set scaffold
ypatil12 Jun 3, 2024
51ff168
fix: impl/storage compile errors; pending updating of tests
ypatil12 Jun 3, 2024
d477943
chore: `forge fmt`
0xClandestine Jun 4, 2024
bcf7d21
fix: `OperatorSet` struct misuse
0xClandestine Jun 4, 2024
3ff5cb6
fix: comment
ypatil12 Jun 5, 2024
b31d4ed
Merge branch 'feat/operator-sets' of https://github.com/Layr-Labs/eig…
0xClandestine Jun 5, 2024
73cf5e2
chore: verbose use of `OperatorSet`
0xClandestine Jun 5, 2024
5d6a28c
test: `registerOperatorToOperatorSet`
0xClandestine Jun 5, 2024
e94c74b
feat: `registerOperatorToOperatorSets`
0xClandestine Jun 6, 2024
331afaa
chore: `forge fmt`
0xClandestine Jun 6, 2024
b4724c0
feat: interface changes
0xClandestine Jun 6, 2024
17e9f78
fix: operator set digest
0xClandestine Jun 6, 2024
6bcc71c
fix: `OPERATOR_SET_REGISTRATION_TYPEHASH`
0xClandestine Jun 6, 2024
08aa8fc
chore: `forge fmt`
0xClandestine Jun 6, 2024
0b07f47
test: wrong avs using signature
0xClandestine Jun 6, 2024
97a6dba
fix: optimize for SSTOREs
0xClandestine Jun 6, 2024
e9359ad
test: `deregisterOperatorFromOperatorSets`
0xClandestine Jun 6, 2024
b92b285
chore: rename `operatorSetStrategies`
0xClandestine Jun 6, 2024
18c7133
test: `addStrategiesToOperatorSet`
0xClandestine Jun 6, 2024
b869392
test: `removeStrategiesFromOperatorSet`
0xClandestine Jun 6, 2024
c4437c6
test: more coverage
0xClandestine Jun 7, 2024
5d09658
chore: improve natspec
0xClandestine Jun 7, 2024
648cc94
WIP: simp mode
0xClandestine Jun 17, 2024
d8c046f
WIP: simp mode
0xClandestine Jun 17, 2024
d12ea4c
WIP: simp mode
0xClandestine Jun 17, 2024
c926f2f
test: simp mode
0xClandestine Jun 18, 2024
92479e2
test: simp mode
0xClandestine Jun 18, 2024
27ed1a1
test: simp mode
0xClandestine Jun 18, 2024
3b0450e
refactor: simp mode storage
0xClandestine Jun 19, 2024
5f90d78
Revert "refactor: simp mode storage"
0xClandestine Jun 19, 2024
f4c3ae5
Reapply "refactor: simp mode storage"
0xClandestine Jun 19, 2024
c190c1b
feat: simp mode
0xClandestine Jun 20, 2024
3b984ab
fix(optimize): salt cancellation
0xClandestine Jun 20, 2024
b944193
test: improvements
0xClandestine Jun 20, 2024
98d0c4f
test: improvements
0xClandestine Jun 20, 2024
d83a870
fix: move `isOperatorSetAVS` update out of loop
0xClandestine Jun 24, 2024
1fa4a42
fix: standby update typehash
0xClandestine Jun 24, 2024
1e9609f
test: cleanup
0xClandestine Jun 25, 2024
1328e23
fix: move mutation out of loop
0xClandestine Jun 25, 2024
0553d18
nit: cleanup
0xClandestine Jun 25, 2024
22ceb9c
fix: remove unused events
ypatil12 Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion script/deploy/devnet/M2_Deploy_From_Scratch.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ contract Deployer_M2 is Script, Test {
// Second, deploy the *implementation* contracts, using the *proxy contracts* as inputs
delegationImplementation = new DelegationManager(strategyManager, slasher, eigenPodManager);
strategyManagerImplementation = new StrategyManager(delegation, eigenPodManager, slasher);
avsDirectoryImplementation = new AVSDirectory(delegation);
avsDirectoryImplementation = new AVSDirectory(delegation, strategyManager);
slasherImplementation = new Slasher(strategyManager, delegation);
eigenPodManagerImplementation = new EigenPodManager(
ethPOSDeposit,
Expand Down
2 changes: 1 addition & 1 deletion script/deploy/holesky/M2_Deploy_From_Scratch.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ contract M2_Deploy_Holesky_From_Scratch is ExistingDeploymentParser {
);

eigenPodBeacon = new UpgradeableBeacon(address(eigenPodImplementation));
avsDirectoryImplementation = new AVSDirectory(delegationManager);
avsDirectoryImplementation = new AVSDirectory(delegationManager, strategyManager);
delegationManagerImplementation = new DelegationManager(strategyManager, slasher, eigenPodManager);
strategyManagerImplementation = new StrategyManager(delegationManager, eigenPodManager, slasher);
slasherImplementation = new Slasher(strategyManager, delegationManager);
Expand Down
2 changes: 1 addition & 1 deletion script/deploy/mainnet/M2_Mainnet_Upgrade.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ contract M2_Mainnet_Upgrade is ExistingDeploymentParser {
*/
function _deployImplementationContracts() internal {
// 1. Deploy New TUPS
avsDirectoryImplementation = new AVSDirectory(delegationManager);
avsDirectoryImplementation = new AVSDirectory(delegationManager, strategyManager);
avsDirectory = AVSDirectory(
address(
new TransparentUpgradeableProxy(
Expand Down
220 changes: 199 additions & 21 deletions src/contracts/core/AVSDirectory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,23 @@ contract AVSDirectory is
// @dev Chain ID at the time of contract deployment
uint256 internal immutable ORIGINAL_CHAIN_ID;

/*******************************************************************************
INITIALIZING FUNCTIONS
*******************************************************************************/
/// @notice Canonical, virtual beacon chain ETH strategy
IStrategy public constant beaconChainETHStrategy = IStrategy(0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0);

/**
* @dev Initializes the immutable addresses of the strategy mananger, delegationManager, slasher,
*
* INITIALIZING FUNCTIONS
*
*/

/**
* @dev Initializes the immutable addresses of the strategy mananger, delegationManager, slasher,
* and eigenpodManager contracts
*/
constructor(IDelegationManager _delegation) AVSDirectoryStorage(_delegation) {
constructor(
IDelegationManager _delegation,
IStrategyManager _strategyManager
) AVSDirectoryStorage(_delegation, _strategyManager) {
_disableInitializers();
ORIGINAL_CHAIN_ID = block.chainid;
}
Expand All @@ -48,10 +56,114 @@ contract AVSDirectory is
_transferOwnership(initialOwner);
}

/*******************************************************************************
EXTERNAL FUNCTIONS
*******************************************************************************/
/**
*
* EXTERNAL FUNCTIONS
*
*/

/**
* @notice Called by AVSs to add an operator to an operator set
*
* @param operator the address of the operator to be added to the operator set
* @param operatorSetID the ID of the operator set
* @param operatorSignature the signature of the operator on their intent to register
*
* @dev msg.sender is used as the AVS
* @dev operator must not have a deregistration from the operator set
* @dev if this is the first operator set in the AVS that the operator is
* registering for, a OperatorAVSRegistrationStatusUpdated event is emitted with
* a REGISTERED status
* =
*/
function registerOperatorToOperatorSet(
address operator,
uint32 operatorSetID,
ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature
) external onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS) {
require(
operatorSignature.expiry >= block.timestamp,
"AVSDirectory.registerOperatorToOperatorSet: operator signature expired"
);
require(
operatorSetRegistrations[msg.sender][operator][operatorSetID] != true,
"AVSDirectory.registerOperatorToOperatorSet: operator already registered to operator set"
);
require(
!operatorSaltIsSpent[operator][operatorSignature.salt],
"AVSDirectory.registerOperatorToAVS: salt already spent"
);
require(
delegation.isOperator(operator),
"AVSDirectory.registerOperatorToAVS: operator not registered to EigenLayer yet"
);

// Calculate the digest hash
bytes32 operatorSetRegistrationDigestHash = calculateOperatorAVSRegistrationDigestHash({
operator: operator,
avs: msg.sender,
salt: operatorSignature.salt,
expiry: operatorSignature.expiry
});

// Check that the signature is valid
EIP1271SignatureUtils.checkSignature_EIP1271(
operator, operatorSetRegistrationDigestHash, operatorSignature.signature
);

// Set as Operator Set AVS, preventing any further legacy registrations
if (!isOperatorSetAVS[msg.sender]) {
isOperatorSetAVS[msg.sender] = true;
}

// Update operator set registration
operatorSetRegistrations[msg.sender][operator][operatorSetID] = true;
operatorAVSOperatorSetCount[msg.sender][operator] += 1;

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

// Set the operator as registered if not already
if (avsOperatorStatus[msg.sender][operator] != OperatorAVSRegistrationStatus.REGISTERED) {
avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.REGISTERED;
emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.REGISTERED);
}

emit OperatorAddedToOperatorSet(operator, OperatorSet(msg.sender, operatorSetID));
}

/**
* @notice Called by AVSs or operators to remove an operator to from operator set
*
* @param operator the address of the operator to be removed from the
* operator set
* @param operatorSetID the ID of the operator set
*
* @dev msg.sender is used as the AVS
* @dev operator must be registered for msg.sender AVS and the given
* operator set
*/
function deregisterOperatorFromOperatorSet(
address operator,
uint32 operatorSetID
) external onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS) {
require(
operatorSetRegistrations[msg.sender][operator][operatorSetID] == true,
"AVSDirectory.deregisterOperatorFromOperatorSet: operator not registered for operator set"
);

// Update operator set registration
operatorSetRegistrations[msg.sender][operator][operatorSetID] = false;
operatorAVSOperatorSetCount[msg.sender][operator] -= 1;

emit OperatorRemovedFromOperatorSet(operator, OperatorSet(msg.sender, operatorSetID));

// Set the operator as deregistered if no longer registered for any operator sets
if (operatorAVSOperatorSetCount[msg.sender][operator] == 0) {
avsOperatorStatus[msg.sender][operator] = OperatorAVSRegistrationStatus.UNREGISTERED;
emit OperatorAVSRegistrationStatusUpdated(operator, msg.sender, OperatorAVSRegistrationStatus.UNREGISTERED);
}
}

/**
* @notice Called by the AVS's service manager contract to register an operator with the avs.
Expand All @@ -62,7 +174,6 @@ contract AVSDirectory is
address operator,
ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature
) external onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS) {

require(
operatorSignature.expiry >= block.timestamp,
"AVSDirectory.registerOperatorToAVS: operator signature expired"
Expand All @@ -77,7 +188,12 @@ contract AVSDirectory is
);
require(
delegation.isOperator(operator),
"AVSDirectory.registerOperatorToAVS: operator not registered to EigenLayer yet");
"AVSDirectory.registerOperatorToAVS: operator not registered to EigenLayer yet"
);
require(
!isOperatorSetAVS[msg.sender],
"AVSDirectory.registerOperatorToAVS: operator set AVS cannot register operators with legacy method"
);

// Calculate the digest hash
bytes32 operatorRegistrationDigestHash = calculateOperatorAVSRegistrationDigestHash({
Expand All @@ -89,9 +205,7 @@ contract AVSDirectory is

// Check that the signature is valid
EIP1271SignatureUtils.checkSignature_EIP1271(
operator,
operatorRegistrationDigestHash,
operatorSignature.signature
operator, operatorRegistrationDigestHash, operatorSignature.signature
);

// Set the operator as registered
Expand All @@ -107,7 +221,10 @@ contract AVSDirectory is
* @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 onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS) {
function deregisterOperatorFromAVS(address operator)
external
onlyWhenNotPaused(PAUSED_OPERATOR_REGISTER_DEREGISTER_TO_AVS)
{
require(
avsOperatorStatus[msg.sender][operator] == OperatorAVSRegistrationStatus.REGISTERED,
"AVSDirectory.deregisterOperatorFromAVS: operator not registered"
Expand All @@ -127,6 +244,47 @@ contract AVSDirectory is
emit AVSMetadataURIUpdated(msg.sender, metadataURI);
}

/**
* @notice Adds strategies to an operator set
* @param operatorSetID The ID of the operator set
* @param strategies The strategies to add to the operator set
* TODO: align with team on if we want to keep the two require statements
*/
function addStrategiesToOperatorSet(uint32 operatorSetID, IStrategy[] calldata strategies) external {
uint256 strategiesToAdd = strategies.length;
for (uint256 i = 0; i < strategiesToAdd; i++) {
// Require that the strategy is valid
require(
strategyManager.strategyIsWhitelistedForDeposit(strategies[i])
|| strategies[i] == beaconChainETHStrategy,
"AVSDirectory.addStrategiesToOperatorSet: invalid strategy considered"
);
require(
!operatorSetStrategies[msg.sender][operatorSetID][strategies[i]],
"AVSDirectory.addStrategiesToOperatorSet: strategy already added to operator set"
);
operatorSetStrategies[msg.sender][operatorSetID][strategies[i]] = true;
emit OperatorSetStrategyAdded(OperatorSet(msg.sender, operatorSetID), strategies[i]);
}
}

/**
* @notice Removes strategies from an operator set
* @param operatorSetID The ID of the operator set
* @param strategies The strategies to remove from the operator set
*/
function removeStrategiesFromOperatorSet(uint32 operatorSetID, IStrategy[] calldata strategies) external {
uint256 strategiesToRemove = strategies.length;
for (uint256 i = 0; i < strategiesToRemove; i++) {
require(
operatorSetStrategies[msg.sender][operatorSetID][strategies[i]],
"AVSDirectory.removeStrategiesFromOperatorSet: strategy not a member of operator set"
);
operatorSetStrategies[msg.sender][operatorSetID][strategies[i]] = false;
emit OperatorSetStrategyRemoved(OperatorSet(msg.sender, operatorSetID), strategies[i]);
}
}

/**
* @notice Called by an operator to cancel a salt that has been used to register with an AVS.
* @param salt A unique and single use value associated with the approver signature.
Expand All @@ -136,9 +294,11 @@ contract AVSDirectory is
operatorSaltIsSpent[msg.sender][salt] = true;
}

/*******************************************************************************
VIEW FUNCTIONS
*******************************************************************************/
/**
*
* VIEW FUNCTIONS
*
*/

/**
* @notice Calculates the digest hash to be signed by an operator to register with an AVS
Expand All @@ -152,15 +312,33 @@ contract AVSDirectory is
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;
}

/**
* @notice Calculates the digest hash to be signed by an operator to register with an operator set
* @param operator The operator set that the operator is registering to
* @param operatorSet A struct containing info about a given operator set.
* @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 calculateOperatorSetRegistrationDigestHash(
address operator,
OperatorSet memory operatorSet,
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)
abi.encode(OPERATOR_SET_REGISTRATION_TYPEHASH, operator, operatorSet.avs, operatorSet.id, salt, expiry)
);
// calculate the digest hash
bytes32 digestHash = keccak256(
abi.encodePacked("\x19\x01", domainSeparator(), structHash)
);
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator(), structHash));
return digestHash;
}

Expand Down
31 changes: 25 additions & 6 deletions src/contracts/core/AVSDirectoryStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ pragma solidity ^0.8.12;
import "../interfaces/IAVSDirectory.sol";
import "../interfaces/IStrategyManager.sol";
import "../interfaces/IDelegationManager.sol";
import "../interfaces/ISlasher.sol";
import "../interfaces/IEigenPodManager.sol";

abstract contract AVSDirectoryStorage is IAVSDirectory {
/// @notice The EIP-712 typehash for the contract's domain
Expand All @@ -16,31 +14,52 @@ abstract contract AVSDirectoryStorage is IAVSDirectory {
bytes32 public constant OPERATOR_AVS_REGISTRATION_TYPEHASH =
keccak256("OperatorAVSRegistration(address operator,address avs,bytes32 salt,uint256 expiry)");

/// @notice The EIP-712 typehash for the `OperatorSetRegistration` struct used by the contract
bytes32 public constant OPERATOR_SET_REGISTRATION_TYPEHASH = keccak256(
"OperatorSetRegistration(address operator,address avs,uint32 operatorSetID,bytes32 salt,uint256 expiry)"
);

/// @notice The DelegationManager contract for EigenLayer
IDelegationManager public immutable delegation;

/// @notice The StrategyManager contract for EigenLayer
IStrategyManager public immutable strategyManager;

/**
* @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.
* Use the getter function `domainSeparator` to get the current domain separator for this contract.
*/
bytes32 internal _DOMAIN_SEPARATOR;

/// @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.
/// @dev Salt is used in the `registerOperatorToAVS` and `registerOperatorToOperatorSet` function.
mapping(address => mapping(bytes32 => bool)) public operatorSaltIsSpent;

constructor(IDelegationManager _delegation) {
/// @notice Mapping: AVS => whether or not the AVS uses operator set
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: standardize lower or uppercase avs in comments

mapping(address => bool) public isOperatorSetAVS;

/// @notice Mapping: AVS => operatorSetID => strategy => whether or not the strategy is in the operator set
mapping(address => mapping(uint32 => mapping(IStrategy => bool))) public operatorSetStrategies;

/// @notice Mapping: avs => operator => operatorSetID => whether the operator is registered for the operator set
mapping(address => mapping(address => mapping(uint32 => bool))) public operatorSetRegistrations;

/// @notice Mapping: avs => operator => number of operator sets the operator is registered for the AVS
mapping(address => mapping(address => uint256)) public operatorAVSOperatorSetCount;

constructor(IDelegationManager _delegation, IStrategyManager _strategyManager) {
delegation = _delegation;
strategyManager = _strategyManager;
}

/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[47] private __gap;
uint256[43] private __gap;
}
Loading