Skip to content

Commit 3419d2a

Browse files
authored
fix: enforce quorum registration on churn (#467)
NOTE: #464 was merged to wrong target **Motivation:** It is possible to break the `maxOperatorCount` invariant by doing the following: Let's assume there are two quorums, 1 and 2, with a `maxOperatorCount` of 2. 1. Alice & Bob register for quorum 1 2. Bob registers for quorum 2 3. Bob deregisters from quorum 1, Charlie enters 4. Quorum 1 Members: Alice/Charlie. Quorum 2 members: Bob 5. Eve creates a churn registration that exits Bob. Quorum 1 has 3 members. This works just fine since we allow a churn to occur if the `operatorToKick` is registered to the AVS (not the quorum): https://github.com/Layr-Labs/eigenlayer-middleware/blob/f5adbcac55d9336cd646ce71bc467aa7e20f1a12/src/SlashingRegistryCoordinator.sol#L405-L408 Although this assumes that the `churnApprover` is buggy, we should still be enforcing that you are churning a user if they are registered for the quorum. **Modifications:** Require that the `operatorToKick` is registered for the quorum. **Result:** Stricter churn guarantees.
1 parent 107541d commit 3419d2a

File tree

3 files changed

+41
-10
lines changed

3 files changed

+41
-10
lines changed

src/SlashingRegistryCoordinator.sol

+8
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ contract SlashingRegistryCoordinator is
677677
/**
678678
* @notice Validates that an incoming operator is eligible to replace an existing
679679
* operator based on the stake of both
680+
* @dev In order to be churned out, the existing operator must be registered for the quorum
680681
* @dev In order to churn, the incoming operator needs to have more stake than the
681682
* existing operator by a proportion given by `kickBIPsOfOperatorStake`
682683
* @dev In order to be churned out, the existing operator needs to have a proportion
@@ -705,6 +706,13 @@ contract SlashingRegistryCoordinator is
705706
require(newOperator != operatorToKick, CannotChurnSelf());
706707
require(kickParams.quorumNumber == quorumNumber, QuorumOperatorCountMismatch());
707708

709+
uint192 quorumBitmap;
710+
quorumBitmap = uint192(BitmapUtils.setBit(quorumBitmap, quorumNumber));
711+
require(
712+
quorumBitmap.isSubsetOf(_currentOperatorBitmap(idToKick)),
713+
OperatorNotRegisteredForQuorum()
714+
);
715+
708716
// Get the target operator's stake and check that it is below the kick thresholds
709717
uint96 operatorToKickStake = stakeRegistry.getCurrentStake(idToKick, quorumNumber);
710718
require(

src/interfaces/ISlashingRegistryCoordinator.sol

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ interface ISlashingRegistryCoordinatorErrors {
5959
error LookAheadPeriodTooLong();
6060
/// @notice Thrown when the number of operators in a quorum would exceed the maximum allowed.
6161
error MaxOperatorCountReached();
62+
/// @notice Thrown when the operator is not registered for the quorum.
63+
error OperatorNotRegisteredForQuorum();
6264
}
6365

6466
interface ISlashingRegistryCoordinatorTypes {

test/unit/SlashingRegistryCoordinatorUnit.t.sol

+31-10
Original file line numberDiff line numberDiff line change
@@ -1937,25 +1937,48 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is
19371937
);
19381938
}
19391939

1940+
/// @dev Asserts that an operator cannot be churned out if it is not registered for the quorum
19401941
function test_registerOperatorWithChurn_revert_notRegisteredForQuorum() public {
19411942
_setOperatorWeight(testOperator.key.addr, registeringStake);
1943+
_setOperatorWeight(operatorToKick.key.addr, operatorToKickStake);
1944+
1945+
// Create a new quorum
1946+
IStakeRegistryTypes.StrategyParams[] memory strategyParams =
1947+
new IStakeRegistryTypes.StrategyParams[](1);
1948+
strategyParams[0] =
1949+
IStakeRegistryTypes.StrategyParams({strategy: mockStrategy, multiplier: 1 ether});
1950+
1951+
operatorSetParams = ISlashingRegistryCoordinatorTypes.OperatorSetParam({
1952+
maxOperatorCount: 1,
1953+
kickBIPsOfOperatorStake: 5000,
1954+
kickBIPsOfTotalStake: 5000
1955+
});
1956+
1957+
vm.startPrank(proxyAdminOwner);
1958+
slashingRegistryCoordinator.createTotalDelegatedStakeQuorum(
1959+
operatorSetParams,
1960+
1 ether, // minimum stake
1961+
strategyParams
1962+
);
1963+
vm.stopPrank();
19421964

1943-
Operator memory unregisteredOperator = operatorsByID[operatorIds.at(5)];
1944-
_setOperatorWeight(unregisteredOperator.key.addr, operatorToKickStake);
1965+
// Register extra operator in the new quorum
1966+
registerOperatorInSlashingRegistryCoordinator(extraOperator1, "socket:8545", uint32(2));
19451967

1968+
// Setup churn data
19461969
ISlashingRegistryCoordinatorTypes.OperatorKickParam[] memory operatorKickParams =
19471970
new ISlashingRegistryCoordinatorTypes.OperatorKickParam[](quorumNumbers.length);
19481971
operatorKickParams[0] = ISlashingRegistryCoordinatorTypes.OperatorKickParam({
1949-
operator: unregisteredOperator.key.addr,
1950-
quorumNumber: uint8(quorumNumbers[0])
1972+
operator: operatorToKick.key.addr,
1973+
quorumNumber: uint8(2) // 3rd quorum
19511974
});
19521975

19531976
ISignatureUtilsMixinTypes.SignatureWithSaltAndExpiry memory churnApproverSignature =
19541977
_signChurnApproval(
19551978
testOperator.key.addr,
19561979
testOperatorId,
19571980
operatorKickParams,
1958-
bytes32(uint256(3)), // Different salt from previous tests
1981+
bytes32(uint256(4)),
19591982
defaultExpiry
19601983
);
19611984

@@ -1965,7 +1988,7 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is
19651988
IAllocationManagerTypes.RegisterParams memory registerParams = IAllocationManagerTypes
19661989
.RegisterParams({
19671990
avs: address(serviceManager),
1968-
operatorSetIds: new uint32[](quorumNumbers.length),
1991+
operatorSetIds: new uint32[](1),
19691992
data: abi.encode(
19701993
ISlashingRegistryCoordinatorTypes.RegistrationType.CHURN,
19711994
"socket:8545",
@@ -1975,12 +1998,10 @@ contract SlashingRegistryCoordinator_RegisterWithChurn is
19751998
)
19761999
});
19772000

1978-
for (uint256 i = 0; i < quorumNumbers.length; i++) {
1979-
registerParams.operatorSetIds[i] = uint8(quorumNumbers[i]);
1980-
}
2001+
registerParams.operatorSetIds[0] = uint32(2);
19812002

19822003
vm.prank(testOperator.key.addr);
1983-
vm.expectRevert(abi.encodeWithSignature("OperatorNotRegistered()"));
2004+
vm.expectRevert(abi.encodeWithSignature("OperatorNotRegisteredForQuorum()"));
19842005
IAllocationManager(coreDeployment.allocationManager).registerForOperatorSets(
19852006
testOperator.key.addr, registerParams
19862007
);

0 commit comments

Comments
 (0)