diff --git a/src/SlashingRegistryCoordinator.sol b/src/SlashingRegistryCoordinator.sol index 0eae566d..6915f63c 100644 --- a/src/SlashingRegistryCoordinator.sol +++ b/src/SlashingRegistryCoordinator.sol @@ -677,6 +677,7 @@ contract SlashingRegistryCoordinator is /** * @notice Validates that an incoming operator is eligible to replace an existing * operator based on the stake of both + * @dev In order to be churned out, the existing operator must be registered for the quorum * @dev In order to churn, the incoming operator needs to have more stake than the * existing operator by a proportion given by `kickBIPsOfOperatorStake` * @dev In order to be churned out, the existing operator needs to have a proportion @@ -705,6 +706,13 @@ contract SlashingRegistryCoordinator is require(newOperator != operatorToKick, CannotChurnSelf()); require(kickParams.quorumNumber == quorumNumber, QuorumOperatorCountMismatch()); + uint192 quorumBitmap; + quorumBitmap = uint192(BitmapUtils.setBit(quorumBitmap, quorumNumber)); + require( + quorumBitmap.isSubsetOf(_currentOperatorBitmap(idToKick)), + OperatorNotRegisteredForQuorum() + ); + // Get the target operator's stake and check that it is below the kick thresholds uint96 operatorToKickStake = stakeRegistry.getCurrentStake(idToKick, quorumNumber); require( diff --git a/src/interfaces/ISlashingRegistryCoordinator.sol b/src/interfaces/ISlashingRegistryCoordinator.sol index 1259dea4..6ffd1de9 100644 --- a/src/interfaces/ISlashingRegistryCoordinator.sol +++ b/src/interfaces/ISlashingRegistryCoordinator.sol @@ -59,6 +59,8 @@ interface ISlashingRegistryCoordinatorErrors { error LookAheadPeriodTooLong(); /// @notice Thrown when the number of operators in a quorum would exceed the maximum allowed. error MaxOperatorCountReached(); + /// @notice Thrown when the operator is not registered for the quorum. + error OperatorNotRegisteredForQuorum(); } interface ISlashingRegistryCoordinatorTypes { diff --git a/test/unit/SlashingRegistryCoordinatorUnit.t.sol b/test/unit/SlashingRegistryCoordinatorUnit.t.sol index a2cf0c4e..ce9e642b 100644 --- a/test/unit/SlashingRegistryCoordinatorUnit.t.sol +++ b/test/unit/SlashingRegistryCoordinatorUnit.t.sol @@ -1937,17 +1937,40 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is ); } + /// @dev Asserts that an operator cannot be churned out if it is not registered for the quorum function test_registerOperatorWithChurn_revert_notRegisteredForQuorum() public { _setOperatorWeight(testOperator.key.addr, registeringStake); + _setOperatorWeight(operatorToKick.key.addr, operatorToKickStake); + + // Create a new quorum + IStakeRegistryTypes.StrategyParams[] memory strategyParams = + new IStakeRegistryTypes.StrategyParams[](1); + strategyParams[0] = + IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether}); + + operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({ + maxOperatorCount: 1, + kickBIPsOfOperatorStake: 5000, + kickBIPsOfTotalStake: 5000 + }); + + vm.startPrank(proxyAdminOwner); + slashingRegistryCoordinator.createTotalDelegatedStakeQuorum( + operatorSetParams, + 1 ether, // minimum stake + strategyParams + ); + vm.stopPrank(); - Operator memory unregisteredOperator = operatorsByID[operatorIds.at(5)]; - _setOperatorWeight(unregisteredOperator.key.addr, operatorToKickStake); + // Register extra operator in the new quorum + registerOperatorInSlashingRegistryCoordinator(extraOperator1, "socket:8545", uint32(2)); + // Setup churn data ISlashingRegistryCoordinatorTypes.OperatorKickParam[] memory operatorKickParams = new ISlashingRegistryCoordinatorTypes.OperatorKickParam[](quorumNumbers.length); operatorKickParams[0] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({ - operator: unregisteredOperator.key.addr, - quorumNumber: uint8(quorumNumbers[0]) + operator: operatorToKick.key.addr, + quorumNumber: uint8(2) // 3rd quorum }); ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory churnApproverSignature = @@ -1955,7 +1978,7 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is testOperator.key.addr, testOperatorId, operatorKickParams, - bytes32(uint256(3)), // Different salt from previous tests + bytes32(uint256(4)), defaultExpiry ); @@ -1965,7 +1988,7 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes .RegisterParams({ avs: address(serviceManager), - operatorSetIds: new uint32[](quorumNumbers.length), + operatorSetIds: new uint32[](1), data: abi.encode( ISlashingRegistryCoordinatorTypes.RegistrationType.CHURN, "socket:8545", @@ -1975,12 +1998,10 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is ) }); - for (uint256 i = 0; i < quorumNumbers.length; i++) { - registerParams.operatorSetIds[i] = uint8(quorumNumbers[i]); - } + registerParams.operatorSetIds[0] = uint32(2); vm.prank(testOperator.key.addr); - vm.expectRevert(abi.encodeWithSignature("OperatorNotRegistered()")); + vm.expectRevert(abi.encodeWithSignature("OperatorNotRegisteredForQuorum()")); IAllocationManager(coreDeployment.allocationManager).registerForOperatorSets( testOperator.key.addr, registerParams );