Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/contracts/interfaces/IKeyRegistrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface IKeyRegistrarErrors {
error ConfigurationAlreadySet();
error OperatorSetNotConfigured();
error KeyNotFound(OperatorSet operatorSet, address operator);
error OperatorStillSlashable(OperatorSet operatorSet, address operator);
}

interface IKeyRegistrarTypes {
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 8 additions & 1 deletion src/contracts/permissions/KeyRegistrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -89,7 +90,13 @@ 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());
// Only the operator or authorized addresses can deregister keys
require(_checkCanCall(operator), InvalidPermissions());

// 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());
Expand Down
9 changes: 9 additions & 0 deletions src/test/mocks/AllocationManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
Expand Down Expand Up @@ -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;
}
}
71 changes: 64 additions & 7 deletions src/test/unit/KeyRegistrarUnit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -820,16 +832,61 @@ 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);
}

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));
}
}