Skip to content

Commit

Permalink
fix: handle eject operators correctly for post try-catch (#402)
Browse files Browse the repository at this point in the history
Changes:

- ejectOperator in SlashingRegistryCoordinator now only uses
_forceDeregisterOperator, allowing AllocationManager to handle callbacks
for deregistration

- RegistryCoordinator now handles M2 and non-M2 quorums separately:

- M2 quorums use _deregisterOperator

- Non-M2 quorums use _forceDeregisterOperator

- Added bitmap intersection support to filter quorum types properly

This change improves the ejection flow by ensuring proper deregistration
handling through the AllocationManager while maintaining special
handling for M2 quorums on ejection
  • Loading branch information
stevennevins authored Feb 18, 2025
1 parent 2e423f0 commit 1c0ccfb
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 74 deletions.
2 changes: 0 additions & 2 deletions src/EjectionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
IStakeRegistry
} from "./EjectionManagerStorage.sol";

// TODO: double check order of inheritance since we separated storage from logic...

/**
* @title Used for automated ejection of operators from the SlashingRegistryCoordinator under a ratelimit
* @author Layr Labs, Inc.
Expand Down
149 changes: 120 additions & 29 deletions src/RegistryCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,120 @@ contract RegistryCoordinator is RegistryCoordinatorStorage {
emit M2QuorumRegistrationDisabled();
}

/// @inheritdoc ISlashingRegistryCoordinator
function ejectOperator(
address operator,
bytes memory quorumNumbers
)
public
virtual
override(ISlashingRegistryCoordinator, SlashingRegistryCoordinator)
onlyEjector
{
_kickOperators({operator: operator, quorumNumbers: quorumNumbers, isEjection: true});
}

/**
*
* INTERNAL FUNCTIONS
*
*/

/**
* @notice Internal function to handle operator registration with churn
* @param operator The operator to register
* @param operatorId The operator's ID
* @param quorumNumbers The quorum numbers to register for
* @param socket The operator's socket
* @param operatorKickParams The parameters needed to kick operators from quorums that have reached their caps
* @param churnApproverSignature The churnApprover's signature approving the registration
*/
function _registerOperatorWithChurn(
address operator,
bytes32 operatorId,
bytes memory quorumNumbers,
string memory socket,
OperatorKickParam[] memory operatorKickParams,
SignatureWithSaltAndExpiry memory churnApproverSignature
) internal virtual override {
// verify churnApprover's signature
_verifyChurnApproverSignature(
operator, operatorId, operatorKickParams, churnApproverSignature
);

// quorum bitmap and registration status
RegisterResults memory results = _registerOperator({
operator: operator,
operatorId: operatorId,
quorumNumbers: quorumNumbers,
socket: socket,
checkMaxOperatorCount: false
});

// Check that each quorum's operator count is below the configured maximum. If the max
// is exceeded, use `operatorKickParams` to deregister an existing operator to make space
for (uint256 i = 0; i < quorumNumbers.length; i++) {
OperatorSetParam memory operatorSetParams = _quorumParams[uint8(quorumNumbers[i])];

/**
* If the new operator count for any quorum exceeds the maximum, validate
* that churn can be performed, then deregister the specified operator
*/
if (results.numOperatorsPerQuorum[i] > operatorSetParams.maxOperatorCount) {
_validateChurn({
quorumNumber: uint8(quorumNumbers[i]),
totalQuorumStake: results.totalStakes[i],
newOperator: operator,
newOperatorStake: results.operatorStakes[i],
kickParams: operatorKickParams[i],
setParams: operatorSetParams
});

bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = quorumNumbers[i];
_kickOperators({
operator: operatorKickParams[i].operator,
quorumNumbers: singleQuorumNumber,
isEjection: false
});
}
}
}

/// @dev override the _kickOperators function to handle M2 quorum ejection
function _kickOperators(
address operator,
bytes memory quorumNumbers,
bool isEjection
) internal virtual override {
if (isEjection) {
lastEjectionTimestamp[operator] = block.timestamp;
}

OperatorInfo storage operatorInfo = _operatorInfo[operator];
uint192 quorumsToRemove =
uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount));
if (operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty()) {
// For each quorum number, check if it's an M2 quorum
for (uint256 i = 0; i < quorumNumbers.length; i++) {
bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = quorumNumbers[i];

if (_isM2Quorum(uint8(quorumNumbers[i]))) {
// For M2 quorums, use _deregisterOperator
_deregisterOperator({
operator: operator,
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: true
});
} else {
// For non-M2 quorums, use _forceDeregisterOperator
_forceDeregisterOperator(operator, singleQuorumNumber);
}
}
}
}

/// @dev override the _forceDeregisterOperator function to handle M2 quorum deregistration
function _forceDeregisterOperator(
address operator,
Expand Down Expand Up @@ -204,14 +312,16 @@ contract RegistryCoordinator is RegistryCoordinatorStorage {
}

/**
* @dev Helper function to update operator stakes and deregister loiterers
* Loiterers are AVS registered operators who have force deregistered from the OperatorSet/quorum
* in the core EigenLayer contract AllocationManager but not deregistered from the OperatorSet/quorum
* in this contract. Potentially due to out of gas errors in the deregistration callback. This function
* will handle that edge case by deregistering the operator from the AVS if they are no longer registered
* in the AllocationManager.
* @dev Helper function to update operator stakes and deregister operators with insufficient stake
* This function handles two cases:
* 1. Operators who no longer meet the minimum stake requirement for a quorum
* 2. Operators who have been force-deregistered from the AllocationManager but not from this contract
* (e.g. due to out of gas errors in the deregistration callback)
* @param operators The list of operators to check and update
* @param operatorIds The corresponding operator IDs
* @param quorumNumber The quorum number to check stakes for
*/
function _updateStakesAndDeregisterLoiterers(
function _updateOperatorsStakes(
address[] memory operators,
bytes32[] memory operatorIds,
uint8 quorumNumber
Expand All @@ -222,30 +332,11 @@ contract RegistryCoordinator is RegistryCoordinatorStorage {
stakeRegistry.updateOperatorsStake(operators, operatorIds, quorumNumber);

for (uint256 i = 0; i < operators.length; ++i) {
bool isM2Quorum = _isM2Quorum(quorumNumber);
bool registeredInCore;
// If its an operatorSet quorum, its possible for registeredInCore to be true/false
// so check for operatorSet inclusion in the AllocationManager
if (!isM2Quorum) {
registeredInCore = allocationManager.isMemberOfOperatorSet(
operators[i], OperatorSet({avs: avs, id: uint32(quorumNumber)})
);
}

// Determine if the operator should be deregistered
// If the operator does not have the minimum stake, they need to be force deregistered.
// Additionally, it is possible for an operator to have deregistered from an OperatorSet
// in the core EigenLayer contract AllocationManager but not have the deregistration
// callback succeed here in `deregisterOperator` due to out of gas errors. If that is the case,
// we need to deregister the operator from the OperatorSet in this contract
bool shouldDeregister =
doesNotMeetStakeThreshold[i] || (!registeredInCore && !isM2Quorum);

if (shouldDeregister) {
_deregisterOperator({
if (doesNotMeetStakeThreshold[i]) {
_kickOperators({
operator: operators[i],
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: registeredInCore
isEjection: false
});
}
}
Expand Down
91 changes: 48 additions & 43 deletions src/SlashingRegistryCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,7 @@ contract SlashingRegistryCoordinator is
bytes memory quorumNumbers = currentBitmap.bitmapToBytesArray();
for (uint256 j = 0; j < quorumNumbers.length; j++) {
// update the operator's stake for each quorum
_updateStakesAndDeregisterLoiterers(
singleOperator, singleOperatorId, uint8(quorumNumbers[j])
);
_updateOperatorsStakes(singleOperator, singleOperatorId, uint8(quorumNumbers[j]));
}
}
}
Expand Down Expand Up @@ -302,7 +300,7 @@ contract SlashingRegistryCoordinator is
prevOperatorAddress = operator;
}

_updateStakesAndDeregisterLoiterers(currQuorumOperators, operatorIds, quorumNumber);
_updateOperatorsStakes(currQuorumOperators, operatorIds, quorumNumber);

// Update timestamp that all operators in quorum have been updated all at once
quorumUpdateBlockNumber[quorumNumber] = block.number;
Expand All @@ -325,24 +323,11 @@ contract SlashingRegistryCoordinator is
*/

/// @inheritdoc ISlashingRegistryCoordinator
function ejectOperator(address operator, bytes memory quorumNumbers) external onlyEjector {
lastEjectionTimestamp[operator] = block.timestamp;

OperatorInfo storage operatorInfo = _operatorInfo[operator];
bytes32 operatorId = operatorInfo.operatorId;
uint192 quorumsToRemove =
uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount));
uint192 currentBitmap = _currentOperatorBitmap(operatorId);
if (
operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty()
&& quorumsToRemove.isSubsetOf(currentBitmap)
) {
_deregisterOperator({
operator: operator,
quorumNumbers: quorumNumbers,
shouldForceDeregister: true
});
}
function ejectOperator(
address operator,
bytes memory quorumNumbers
) public virtual onlyEjector {
_kickOperators({operator: operator, quorumNumbers: quorumNumbers, isEjection: true});
}

/**
Expand Down Expand Up @@ -398,6 +383,33 @@ contract SlashingRegistryCoordinator is
*
*/

/**
* @notice Internal function to handle operator ejection logic
* @param operator The operator to eject
* @param quorumNumbers The quorum numbers to eject the operator from
*/
function _kickOperators(
address operator,
bytes memory quorumNumbers,
bool isEjection
) internal virtual {
if (isEjection) {
lastEjectionTimestamp[operator] = block.timestamp;
}

OperatorInfo storage operatorInfo = _operatorInfo[operator];
bytes32 operatorId = operatorInfo.operatorId;
uint192 quorumsToRemove =
uint192(BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers, quorumCount));
uint192 currentBitmap = _currentOperatorBitmap(operatorId);
if (
operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty()
&& quorumsToRemove.isSubsetOf(currentBitmap)
) {
_forceDeregisterOperator(operator, quorumNumbers);
}
}

/**
* @notice Register the operator for one or more quorums. This method updates the
* operator's quorum bitmap, socket, and status, then registers them with each registry.
Expand Down Expand Up @@ -519,10 +531,10 @@ contract SlashingRegistryCoordinator is

bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = quorumNumbers[i];
_deregisterOperator({
_kickOperators({
operator: operatorKickParams[i].operator,
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: true
isEjection: false
});
}
}
Expand Down Expand Up @@ -632,14 +644,16 @@ contract SlashingRegistryCoordinator is
}

/**
* @dev Helper function to update operator stakes and deregister loiterers
* Loiterers are AVS registered operators who have force deregistered from the OperatorSet/quorum
* in the core EigenLayer contract AllocationManager but not deregistered from the OperatorSet/quorum
* in this contract. Potentially due to out of gas errors in the deregistration callback. This function
* will handle that edge case by deregistering the operator from the AVS if they are no longer registered
* in the AllocationManager.
* @dev Helper function to update operator stakes and deregister operators with insufficient stake
* This function handles two cases:
* 1. Operators who no longer meet the minimum stake requirement for a quorum
* 2. Operators who have been force-deregistered from the AllocationManager but not from this contract
* (e.g. due to out of gas errors in the deregistration callback)
* @param operators The list of operators to check and update
* @param operatorIds The corresponding operator IDs
* @param quorumNumber The quorum number to check stakes for
*/
function _updateStakesAndDeregisterLoiterers(
function _updateOperatorsStakes(
address[] memory operators,
bytes32[] memory operatorIds,
uint8 quorumNumber
Expand All @@ -649,21 +663,12 @@ contract SlashingRegistryCoordinator is
bool[] memory doesNotMeetStakeThreshold =
stakeRegistry.updateOperatorsStake(operators, operatorIds, quorumNumber);
for (uint256 j = 0; j < operators.length; ++j) {
// whether the operator is registered in the core EigenLayer contract AllocationManager
bool registeredInCore = allocationManager.isMemberOfOperatorSet(
operators[j], OperatorSet({avs: avs, id: uint32(quorumNumber)})
);

// If the operator does not have the minimum stake, they need to be force deregistered.
// Additionally, it is possible for an operator to have deregistered from an OperatorSet
// in the core EigenLayer contract AllocationManager but not have the deregistration
// callback succeed here in `deregisterOperator` due to out of gas errors. If that is the case,
// we need to deregister the operator from the OperatorSet in this contract
if (doesNotMeetStakeThreshold[j] || !registeredInCore) {
_deregisterOperator({
if (doesNotMeetStakeThreshold[j]) {
_kickOperators({
operator: operators[j],
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: registeredInCore
isEjection: false
});
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/libraries/BitmapUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,12 @@ library BitmapUtils {
function minus(uint256 a, uint256 b) internal pure returns (uint256) {
return a & ~b;
}

/**
* @notice Returns a new bitmap that contains only bits set in both `a` and `b`
* @dev Result is the intersection of `a` and `b`
*/
function and(uint256 a, uint256 b) internal pure returns (uint256) {
return a & b;
}
}

0 comments on commit 1c0ccfb

Please sign in to comment.