Skip to content
Merged
60 changes: 57 additions & 3 deletions src/RegistryCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
pragma solidity ^0.8.27;

import {IPauserRegistry} from "eigenlayer-contracts/src/contracts/interfaces/IPauserRegistry.sol";
import {IAllocationManager} from
"eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {
IAllocationManager,
OperatorSet
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {IBLSApkRegistry, IBLSApkRegistryTypes} from "./interfaces/IBLSApkRegistry.sol";
import {IStakeRegistry} from "./interfaces/IStakeRegistry.sol";
import {IIndexRegistry} from "./interfaces/IIndexRegistry.sol";
Expand Down Expand Up @@ -132,7 +134,11 @@ contract RegistryCoordinator is RegistryCoordinatorStorage {
OnlyM2QuorumsAllowed()
);

_deregisterOperator({operator: msg.sender, quorumNumbers: quorumNumbers});
_deregisterOperator({
operator: msg.sender,
quorumNumbers: quorumNumbers,
shouldForceDeregister: false
});
}

/// @inheritdoc IRegistryCoordinator
Expand Down Expand Up @@ -197,6 +203,54 @@ 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.
*/
function _updateStakesAndDeregisterLoiterers(
address[] memory operators,
bytes32[] memory operatorIds,
uint8 quorumNumber
) internal virtual override {
bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = bytes1(quorumNumber);
bool[] memory doesNotMeetStakeThreshold =
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: accountIdentifier, 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({
operator: operators[i],
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: registeredInCore
});
}
}
}

/// @notice Return bitmap representing all quorums(Legacy M2 and OperatorSet) quorums
function _getTotalQuorumBitmap() internal view returns (uint256) {
// This creates a number where all bits up to quorumCount are set to 1
Expand Down
112 changes: 90 additions & 22 deletions src/SlashingRegistryCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,11 @@ contract SlashingRegistryCoordinator is
uint32[] memory operatorSetIds
) external override onlyAllocationManager onlyWhenNotPaused(PAUSED_DEREGISTER_OPERATOR) {
bytes memory quorumNumbers = _getQuorumNumbers(operatorSetIds);
_deregisterOperator(operator, quorumNumbers);
_deregisterOperator({
operator: operator,
quorumNumbers: quorumNumbers,
shouldForceDeregister: false
});
}

/// @inheritdoc ISlashingRegistryCoordinator
Expand All @@ -243,16 +247,9 @@ contract SlashingRegistryCoordinator is
bytes memory quorumNumbers = currentBitmap.bitmapToBytesArray();
for (uint256 j = 0; j < quorumNumbers.length; j++) {
// update the operator's stake for each quorum
uint8 quorumNumber = uint8(quorumNumbers[j]);
bool[] memory shouldBeDeregistered = stakeRegistry.updateOperatorsStake(
singleOperator, singleOperatorId, quorumNumber
_updateStakesAndDeregisterLoiterers(
singleOperator, singleOperatorId, uint8(quorumNumbers[j])
);

if (shouldBeDeregistered[0]) {
bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = quorumNumbers[j];
_deregisterOperator(operators[i], singleQuorumNumber);
}
}
}
}
Expand Down Expand Up @@ -303,13 +300,7 @@ contract SlashingRegistryCoordinator is
prevOperatorAddress = operator;
}

bool[] memory shouldBeDeregistered =
stakeRegistry.updateOperatorsStake(currQuorumOperators, operatorIds, quorumNumber);
for (uint256 j = 0; j < currQuorumOperators.length; ++j) {
if (shouldBeDeregistered[j]) {
_deregisterOperator(currQuorumOperators[j], quorumNumbers[i:i + 1]);
}
}
_updateStakesAndDeregisterLoiterers(currQuorumOperators, operatorIds, quorumNumber);

// Update timestamp that all operators in quorum have been updated all at once
quorumUpdateBlockNumber[quorumNumber] = block.number;
Expand Down Expand Up @@ -344,7 +335,11 @@ contract SlashingRegistryCoordinator is
operatorInfo.status == OperatorStatus.REGISTERED && !quorumsToRemove.isEmpty()
&& quorumsToRemove.isSubsetOf(currentBitmap)
) {
_deregisterOperator({operator: operator, quorumNumbers: quorumNumbers});
_deregisterOperator({
operator: operator,
quorumNumbers: quorumNumbers,
shouldForceDeregister: true
});
}
}

Expand Down Expand Up @@ -522,7 +517,11 @@ contract SlashingRegistryCoordinator is

bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = quorumNumbers[i];
_deregisterOperator(operatorKickParams[i].operator, singleQuorumNumber);
_deregisterOperator({
operator: operatorKickParams[i].operator,
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: true
});
}
}
}
Expand All @@ -531,8 +530,16 @@ contract SlashingRegistryCoordinator is
* @dev Deregister the operator from one or more quorums
* This method updates the operator's quorum bitmap and status, then deregisters
* the operator with the BLSApkRegistry, IndexRegistry, and StakeRegistry
* @param operator the operator to deregister
* @param quorumNumbers the quorum numbers to deregister from
* @param shouldForceDeregister whether the operator needs to be deregistered from the OperatorSets of
* the core EigenLayer contract AllocationManager
*/
function _deregisterOperator(address operator, bytes memory quorumNumbers) internal virtual {
function _deregisterOperator(
address operator,
bytes memory quorumNumbers,
bool shouldForceDeregister
) internal virtual {
// Fetch the operator's info and ensure they are registered
OperatorInfo storage operatorInfo = _operatorInfo[operator];
bytes32 operatorId = operatorInfo.operatorId;
Expand Down Expand Up @@ -571,8 +578,9 @@ contract SlashingRegistryCoordinator is
stakeRegistry.deregisterOperator(operatorId, quorumNumbers);
indexRegistry.deregisterOperator(operatorId, quorumNumbers);

// If the caller is not the allocationManager, then this is a force deregistration not consented by the operator
if (msg.sender != address(allocationManager)) {
// If the operator is not deregistered from the EigenLayer core protocol, then we need to force deregister them
// from their respective OperatorSets in the AllocationManager
if (shouldForceDeregister) {
_forceDeregisterOperator(operator, quorumNumbers);
}

Expand All @@ -583,13 +591,35 @@ contract SlashingRegistryCoordinator is
/**
* @notice Helper function to handle operator set deregistration for OperatorSets quorums. This is used
* when an operator is force-deregistered from a set of quorums.
* Due to deregistration being possible in the AllocationManager but not in the AVS as a result of the
* try/catch in `AllocationManager.deregisterFromOperatorSets`, we need to first check that the operator
* is not already deregistered from the OperatorSet in the AllocationManager.
* @param operator The operator to deregister
* @param quorumNumbers The quorum numbers the operator is force-deregistered from
*/
function _forceDeregisterOperator(
address operator,
bytes memory quorumNumbers
) internal virtual {
uint32[] memory operatorSetIds = new uint32[](quorumNumbers.length);
uint256 numDeregister = 0;
for (uint256 i = 0; i < quorumNumbers.length; ++i) {
uint32 operatorSetId = uint32(uint8(quorumNumbers[i]));
if (
allocationManager.isMemberOfOperatorSet(
operator, OperatorSet({avs: accountIdentifier, id: operatorSetId})
)
) {
operatorSetIds[numDeregister] = operatorSetId;
numDeregister++;
}
}

// resize operatorSetIds array length to numDeregister
assembly {
mstore(operatorSetIds, numDeregister)
}

allocationManager.deregisterFromOperatorSets(
IAllocationManagerTypes.DeregisterParams({
operator: operator,
Expand All @@ -599,6 +629,44 @@ 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.
*/
function _updateStakesAndDeregisterLoiterers(
address[] memory operators,
bytes32[] memory operatorIds,
uint8 quorumNumber
) internal virtual {
bytes memory singleQuorumNumber = new bytes(1);
singleQuorumNumber[0] = bytes1(quorumNumber);
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: accountIdentifier, 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({
operator: operators[j],
quorumNumbers: singleQuorumNumber,
shouldForceDeregister: registeredInCore
});
}
}
}

/**
* @notice Checks if the caller is the ejector
* @dev Reverts if the caller is not the ejector
Expand Down
6 changes: 0 additions & 6 deletions src/interfaces/IInstantSlasher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import {ISlasher} from "./ISlasher.sol";
/// @notice A slashing contract that immediately executes slashing requests without any delay or veto period
/// @dev Extends base interfaces to provide access controlled slashing functionality
interface IInstantSlasher is ISlasher {
/// @notice Initializes the contract with a slasher address
/// @param _slasher Address authorized to create and fulfill slashing requests
function initialize(
address _slasher
) external;

/// @notice Immediately executes a slashing request
/// @param _slashingParams Parameters defining the slashing request including operator and amount
/// @dev Can only be called by the authorized slasher
Expand Down
3 changes: 3 additions & 0 deletions src/interfaces/ISlasher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ interface ISlasherEvents is ISlasherTypes {
interface ISlasher is ISlasherErrors, ISlasherEvents {
/// @notice Returns the address authorized to create and fulfill slashing requests
function slasher() external view returns (address);

/// @notice Returns the next slashing request ID
function nextRequestId() external view returns (uint256);
}
10 changes: 2 additions & 8 deletions src/interfaces/IVetoableSlasher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface IVetoableSlasherTypes {
/// @notice Structure containing details about a vetoable slashing request
struct VetoableSlashingRequest {
IAllocationManager.SlashingParams params;
uint256 requestTimestamp;
uint256 requestBlock;
SlashingStatus status;
}
}
Expand Down Expand Up @@ -58,17 +58,11 @@ interface IVetoableSlasher is
IVetoableSlasherEvents
{
/// @notice Duration of the veto period during which the veto committee can cancel slashing requests
/// @dev Set to 3 days (259,200 seconds)
function VETO_PERIOD() external view returns (uint256);
function vetoWindowBlocks() external view returns (uint32);

/// @notice Address of the committee that has veto power over slashing requests
function vetoCommittee() external view returns (address);

/// @notice Initializes the contract with a veto committee and slasher address
/// @param _vetoCommittee Address of the committee that can veto slashing requests
/// @param _slasher Address authorized to create and fulfill slashing requests
function initialize(address _vetoCommittee, address _slasher) external;

/// @notice Queues a new slashing request
/// @param params Parameters defining the slashing request including operator and amount
/// @dev Can only be called by the authorized slasher
Expand Down
9 changes: 1 addition & 8 deletions src/slashers/InstantSlasher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ contract InstantSlasher is IInstantSlasher, SlasherBase {
IAllocationManager _allocationManager,
ISlashingRegistryCoordinator _slashingRegistryCoordinator,
address _slasher
) SlasherBase(_allocationManager, _slashingRegistryCoordinator) {}

/// @inheritdoc IInstantSlasher
function initialize(
address _slasher
) external override initializer {
__SlasherBase_init(_slasher);
}
) SlasherBase(_allocationManager, _slashingRegistryCoordinator, _slasher) {}

/// @inheritdoc IInstantSlasher
function fulfillSlashingRequest(
Expand Down
Loading
Loading