diff --git a/src/contracts/interfaces/IKeyRegistrar.sol b/src/contracts/interfaces/IKeyRegistrar.sol index c174909978..e20de10a0c 100644 --- a/src/contracts/interfaces/IKeyRegistrar.sol +++ b/src/contracts/interfaces/IKeyRegistrar.sol @@ -16,6 +16,7 @@ interface IKeyRegistrarErrors { error ConfigurationAlreadySet(); error OperatorSetNotConfigured(); error KeyNotFound(OperatorSet operatorSet, address operator); + error OperatorStillSlashable(OperatorSet operatorSet, address operator); } interface IKeyRegistrarTypes { @@ -69,8 +70,9 @@ interface IKeyRegistrar is IKeyRegistrarErrors, IKeyRegistrarEvents, ISemVerMixi * @notice Deregisters a cryptographic key for an operator with a specific operator set * @param operator Address of the operator to deregister key for * @param operatorSet The operator set to deregister the key from - * @dev Can be called by avs directly or by addresses they've authorized via PermissionController + * @dev Can be called by the operator directly or by addresses they've authorized via PermissionController * @dev Reverts if key was not registered + * @dev Reverts if operator is still slashable for the operator set (prevents key rotation while slashable) * @dev Keys remain in global key registry to prevent reuse */ function deregisterKey(address operator, OperatorSet memory operatorSet) external; diff --git a/src/contracts/permissions/KeyRegistrar.sol b/src/contracts/permissions/KeyRegistrar.sol index fb601c07f2..7b9a21c384 100644 --- a/src/contracts/permissions/KeyRegistrar.sol +++ b/src/contracts/permissions/KeyRegistrar.sol @@ -9,6 +9,7 @@ import "../mixins/PermissionControllerMixin.sol"; import "../mixins/SignatureUtilsMixin.sol"; import "../interfaces/IPermissionController.sol"; import "../interfaces/IAllocationManager.sol"; +import "../interfaces/IKeyRegistrar.sol"; import "../libraries/OperatorSetLib.sol"; import "./KeyRegistrarStorage.sol"; @@ -88,8 +89,11 @@ contract KeyRegistrar is KeyRegistrarStorage, PermissionControllerMixin, Signatu } /// @inheritdoc IKeyRegistrar - function deregisterKey(address operator, OperatorSet memory operatorSet) external { - require(address(allocationManager.getAVSRegistrar(operatorSet.avs)) == msg.sender, InvalidPermissions()); + function deregisterKey(address operator, OperatorSet memory operatorSet) external checkCanCall(operator) { + // Operators can only deregister if they are not slashable for this operator set + require( + !allocationManager.isOperatorSlashable(operator, operatorSet), OperatorStillSlashable(operatorSet, operator) + ); CurveType curveType = operatorSetCurveTypes[operatorSet.key()]; require(curveType != CurveType.NONE, OperatorSetNotConfigured()); diff --git a/src/test/mocks/AllocationManagerMock.sol b/src/test/mocks/AllocationManagerMock.sol index 89eb76195b..5425bb4ed0 100644 --- a/src/test/mocks/AllocationManagerMock.sol +++ b/src/test/mocks/AllocationManagerMock.sol @@ -26,6 +26,7 @@ contract AllocationManagerMock is Test { mapping(bytes32 operatorSetKey => IStrategy[] strategies) internal _strategies; mapping(bytes32 operatorSetKey => mapping(address operator => mapping(IStrategy strategy => uint minimumSlashableStake))) internal _minimumSlashableStake; + mapping(bytes32 operatorSetKey => mapping(address operator => bool)) internal _isOperatorSlashable; function getSlashCount(OperatorSet memory operatorSet) external view returns (uint) { return _getSlashCount[operatorSet.key()]; @@ -149,4 +150,12 @@ contract AllocationManagerMock is Test { return minimumSlashableStake; } + + function isOperatorSlashable(address operator, OperatorSet memory operatorSet) external view returns (bool) { + return _isOperatorSlashable[operatorSet.key()][operator]; + } + + function setIsOperatorSlashable(address operator, OperatorSet memory operatorSet, bool isSlashable) external { + _isOperatorSlashable[operatorSet.key()][operator] = isSlashable; + } } diff --git a/src/test/unit/KeyRegistrarUnit.t.sol b/src/test/unit/KeyRegistrarUnit.t.sol index f362a9a17b..98b4a4b78c 100644 --- a/src/test/unit/KeyRegistrarUnit.t.sol +++ b/src/test/unit/KeyRegistrarUnit.t.sol @@ -438,7 +438,7 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { function testDeregisterKey_RevertOperatorSetNotConfigured() public { OperatorSet memory operatorSet = _createOperatorSet(avs1, DEFAULT_OPERATOR_SET_ID); - vm.prank(avs1); + vm.prank(operator1); vm.expectRevert(IKeyRegistrarErrors.OperatorSetNotConfigured.selector); keyRegistrar.deregisterKey(operator1, operatorSet); } @@ -540,7 +540,10 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { bytes32 keyHash = keccak256(ecdsaKey1); assertTrue(keyRegistrar.isKeyGloballyRegistered(keyHash)); - vm.prank(avs1); + // Set operator as not slashable + allocationManagerMock.setIsOperatorSlashable(operator1, operatorSet1, false); + + vm.prank(operator1); keyRegistrar.deregisterKey(operator1, operatorSet1); // Key should still be globally registered after deregistration @@ -571,7 +574,10 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { bytes32 keyHash = BN254.hashG1Point(bn254G1Key1); assertTrue(keyRegistrar.isKeyGloballyRegistered(keyHash)); - vm.prank(avs1); + // Set operator as not slashable + allocationManagerMock.setIsOperatorSlashable(operator1, operatorSet1, false); + + vm.prank(operator1); keyRegistrar.deregisterKey(operator1, operatorSet1); // Key should still be globally registered after deregistration @@ -787,7 +793,10 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { vm.prank(operator1); keyRegistrar.registerKey(operator1, operatorSet, ecdsaKey1, signature); - vm.prank(avs1); + // Set operator as not slashable + allocationManagerMock.setIsOperatorSlashable(operator1, operatorSet, false); + + vm.prank(operator1); vm.expectEmit(true, true, true, true); emit KeyDeregistered(operatorSet, operator1, IKeyRegistrarTypes.CurveType.ECDSA); keyRegistrar.deregisterKey(operator1, operatorSet); @@ -806,7 +815,10 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { vm.prank(operator1); keyRegistrar.registerKey(operator1, operatorSet, bn254Key1, signature); - vm.prank(avs1); + // Set operator as not slashable + allocationManagerMock.setIsOperatorSlashable(operator1, operatorSet, false); + + vm.prank(operator1); vm.expectEmit(true, true, true, true); emit KeyDeregistered(operatorSet, operator1, IKeyRegistrarTypes.CurveType.BN254); keyRegistrar.deregisterKey(operator1, operatorSet); @@ -820,7 +832,7 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { vm.prank(avs1); keyRegistrar.configureOperatorSet(operatorSet, IKeyRegistrarTypes.CurveType.ECDSA); - vm.prank(avs1); + vm.prank(operator1); vm.expectRevert(abi.encodeWithSelector(IKeyRegistrarErrors.KeyNotFound.selector, operatorSet, operator1)); keyRegistrar.deregisterKey(operator1, operatorSet); } @@ -828,8 +840,53 @@ contract KeyRegistrarUnitTests is EigenLayerUnitTestSetup { function testDeregisterKey_RevertUnauthorized() public { OperatorSet memory operatorSet = _createOperatorSet(avs1, DEFAULT_OPERATOR_SET_ID); - vm.prank(operator1); + vm.prank(operator2); // operator2 is not authorized to call on behalf of operator1 vm.expectRevert(PermissionControllerMixin.InvalidPermissions.selector); keyRegistrar.deregisterKey(operator1, operatorSet); } + + function testDeregisterKey_OperatorNotSlashable() public { + OperatorSet memory operatorSet = _createOperatorSet(avs1, DEFAULT_OPERATOR_SET_ID); + + vm.prank(avs1); + keyRegistrar.configureOperatorSet(operatorSet, IKeyRegistrarTypes.CurveType.ECDSA); + + bytes memory signature = _generateECDSASignature(operator1, operatorSet, ecdsaAddress1, ecdsaPrivKey1); + + vm.prank(operator1); + keyRegistrar.registerKey(operator1, operatorSet, ecdsaKey1, signature); + + // Set operator as not slashable + allocationManagerMock.setIsOperatorSlashable(operator1, operatorSet, false); + + // Operator should be able to deregister their key when not slashable + vm.prank(operator1); + vm.expectEmit(true, true, true, true); + emit KeyDeregistered(operatorSet, operator1, IKeyRegistrarTypes.CurveType.ECDSA); + keyRegistrar.deregisterKey(operator1, operatorSet); + + assertFalse(keyRegistrar.isRegistered(operatorSet, operator1)); + } + + function testDeregisterKey_RevertOperatorStillSlashable() public { + OperatorSet memory operatorSet = _createOperatorSet(avs1, DEFAULT_OPERATOR_SET_ID); + + vm.prank(avs1); + keyRegistrar.configureOperatorSet(operatorSet, IKeyRegistrarTypes.CurveType.ECDSA); + + bytes memory signature = _generateECDSASignature(operator1, operatorSet, ecdsaAddress1, ecdsaPrivKey1); + + vm.prank(operator1); + keyRegistrar.registerKey(operator1, operatorSet, ecdsaKey1, signature); + + // Set operator as slashable + allocationManagerMock.setIsOperatorSlashable(operator1, operatorSet, true); + + // Operator should not be able to deregister their key when still slashable + vm.prank(operator1); + vm.expectRevert(abi.encodeWithSelector(IKeyRegistrarErrors.OperatorStillSlashable.selector, operatorSet, operator1)); + keyRegistrar.deregisterKey(operator1, operatorSet); + + assertTrue(keyRegistrar.isRegistered(operatorSet, operator1)); + } }