From afbcba869f6270f0b3ce56aa5f543c64231a0add Mon Sep 17 00:00:00 2001 From: steven <12021290+stevennevins@users.noreply.github.com> Date: Thu, 23 May 2024 11:34:41 -0400 Subject: [PATCH] feat: ecdsa key rotation (#252) * feat: add operator key rotation * test: update existing tests to account for signing key * fix: frontrunning with different signing key * test: verify the checkpoint logic for the signing keys * feat: improve event for signing key update * chore: clean up test function names and order functions * fix: storage layout gap * feat: prevent signing at current block * test: add two more test cases for RBN * fix: typo in function signature * chore: remove unnecessary test contract * fix: typo for invalid quorum * fix: invalidQuorum -> validQuorum for NotOwner test --- .../IECDSAStakeRegistryEventsAndErrors.sol | 27 +- src/unaudited/ECDSAStakeRegistry.sol | 150 ++++- src/unaudited/ECDSAStakeRegistryStorage.sol | 13 +- .../ECDSAStakeRegistryPermissioned.sol | 24 +- .../ECDSAStakeRegistryEqualWeightUnit.t.sol | 74 ++- .../ECDSAStakeRegistryPermissionedUnit.t.sol | 64 +- test/unit/ECDSAStakeRegistryUnit.t.sol | 577 ++++++++++++++---- 7 files changed, 748 insertions(+), 181 deletions(-) diff --git a/src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol b/src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol index 48072f54..445db814 100644 --- a/src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol +++ b/src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol @@ -36,13 +36,20 @@ interface ECDSAStakeRegistryEventsAndErrors { /// @notice Emitted when the weight required to be an operator changes /// @param oldMinimumWeight The previous weight /// @param newMinimumWeight The updated weight - event UpdateMinimumWeight(uint256 oldMinimumWeight, uint256 newMinimumWeight); + event UpdateMinimumWeight( + uint256 oldMinimumWeight, + uint256 newMinimumWeight + ); /// @notice Emitted when the system updates an operator's weight /// @param _operator The address of the operator updated /// @param oldWeight The operator's weight before the update /// @param newWeight The operator's weight after the update - event OperatorWeightUpdated(address indexed _operator, uint256 oldWeight, uint256 newWeight); + event OperatorWeightUpdated( + address indexed _operator, + uint256 oldWeight, + uint256 newWeight + ); /// @notice Emitted when the system updates the total weight /// @param oldTotalWeight The total weight before the update @@ -52,6 +59,17 @@ interface ECDSAStakeRegistryEventsAndErrors { /// @notice Emits when setting a new threshold weight. event ThresholdWeightUpdated(uint256 _thresholdWeight); + /// @notice Emitted when an operator's signing key is updated + /// @param operator The address of the operator whose signing key was updated + /// @param updateBlock The block number at which the signing key was updated + /// @param newSigningKey The operator's signing key after the update + /// @param oldSigningKey The operator's signing key before the update + event SigningKeyUpdate( + address indexed operator, + uint256 indexed updateBlock, + address indexed newSigningKey, + address oldSigningKey + ); /// @notice Indicates when the lengths of the signers array and signatures array do not match. error LengthMismatch(); @@ -64,9 +82,12 @@ interface ECDSAStakeRegistryEventsAndErrors { /// @notice Thrown when the threshold update is greater than BPS error InvalidThreshold(); - /// @notice Thrown when missing operators in an update + /// @notice Thrown when missing operators in an update error MustUpdateAllOperators(); + /// @notice Reference blocks must be for blocks that have already been confirmed + error InvalidReferenceBlock(); + /// @notice Indicates operator weights were out of sync and the signed weight exceed the total error InvalidSignedWeight(); diff --git a/src/unaudited/ECDSAStakeRegistry.sol b/src/unaudited/ECDSAStakeRegistry.sol index 5a168cb3..ab4bdbeb 100644 --- a/src/unaudited/ECDSAStakeRegistry.sol +++ b/src/unaudited/ECDSAStakeRegistry.sol @@ -43,13 +43,14 @@ contract ECDSAStakeRegistry is __ECDSAStakeRegistry_init(_serviceManager, _thresholdWeight, _quorum); } - /// @notice Registers a new operator using a provided signature + /// @notice Registers a new operator using a provided signature and signing key /// @param _operatorSignature Contains the operator's signature, salt, and expiry + /// @param _signingKey The signing key to add to the operator's history function registerOperatorWithSignature( - address _operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature, + address _signingKey ) external { - _registerOperatorWithSig(_operator, _operatorSignature); + _registerOperatorWithSig(msg.sender, _operatorSignature, _signingKey); } /// @notice Deregisters an existing operator @@ -57,6 +58,18 @@ contract ECDSAStakeRegistry is _deregisterOperator(msg.sender); } + /** + * @notice Updates the signing key for an operator + * @dev Only callable by the operator themselves + * @param _newSigningKey The new signing key to set for the operator + */ + function updateOperatorSigningKey(address _newSigningKey) external { + if (!_operatorRegistered[msg.sender]) { + revert OperatorNotRegistered(); + } + _updateOperatorSigningKey(msg.sender, _newSigningKey); + } + /** * @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints, * @dev Queries stakes from the Eigenlayer core DelegationManager contract @@ -106,18 +119,18 @@ contract ECDSAStakeRegistry is /// @notice Verifies if the provided signature data is valid for the given data hash. /// @param _dataHash The hash of the data that was signed. - /// @param _signatureData Encoded signature data consisting of an array of signers, an array of signatures, and a reference block number. + /// @param _signatureData Encoded signature data consisting of an array of operators, an array of signatures, and a reference block number. /// @return The function selector that indicates the signature is valid according to ERC1271 standard. function isValidSignature( bytes32 _dataHash, bytes memory _signatureData ) external view returns (bytes4) { ( - address[] memory signers, + address[] memory operators, bytes[] memory signatures, uint32 referenceBlock ) = abi.decode(_signatureData, (address[], bytes[], uint32)); - _checkSignatures(_dataHash, signers, signatures, referenceBlock); + _checkSignatures(_dataHash, operators, signatures, referenceBlock); return IERC1271Upgradeable.isValidSignature.selector; } @@ -127,6 +140,37 @@ contract ECDSAStakeRegistry is return _quorum; } + /** + * @notice Retrieves the latest signing key for a given operator. + * @param _operator The address of the operator. + * @return The latest signing key of the operator. + */ + function getLastestOperatorSigningKey( + address _operator + ) external view returns (address) { + return address(uint160(_operatorSigningKeyHistory[_operator].latest())); + } + + /** + * @notice Retrieves the latest signing key for a given operator at a specific block number. + * @param _operator The address of the operator. + * @param _blockNumber The block number to get the operator's signing key. + * @return The signing key of the operator at the given block. + */ + function getOperatorSigningKeyAtBlock( + address _operator, + uint256 _blockNumber + ) external view returns (address) { + return + address( + uint160( + _operatorSigningKeyHistory[_operator].getAtBlock( + _blockNumber + ) + ) + ); + } + /// @notice Retrieves the last recorded weight for a given operator. /// @param _operator The address of the operator. /// @return uint256 - The latest weight of the operator. @@ -312,9 +356,11 @@ contract ECDSAStakeRegistry is /// @dev registers an operator through a provided signature /// @param _operatorSignature Contains the operator's signature, salt, and expiry + /// @param _signingKey The signing key to add to the operator's history function _registerOperatorWithSig( address _operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature, + address _signingKey ) internal virtual { if (_operatorRegistered[_operator]) { revert OperatorAlreadyRegistered(); @@ -323,6 +369,7 @@ contract ECDSAStakeRegistry is _operatorRegistered[_operator] = true; int256 delta = _updateOperatorWeight(_operator); _updateTotalWeight(delta); + _updateOperatorSigningKey(_operator, _signingKey); IServiceManager(_serviceManager).registerOperatorToAVS( _operator, _operatorSignature @@ -330,6 +377,28 @@ contract ECDSAStakeRegistry is emit OperatorRegistered(_operator, _serviceManager); } + /// @dev Internal function to update an operator's signing key + /// @param _operator The address of the operator to update the signing key for + /// @param _newSigningKey The new signing key to set for the operator + function _updateOperatorSigningKey( + address _operator, + address _newSigningKey + ) internal { + address oldSigningKey = address( + uint160(_operatorSigningKeyHistory[_operator].latest()) + ); + if (_newSigningKey == oldSigningKey) { + return; + } + _operatorSigningKeyHistory[_operator].push(uint160(_newSigningKey)); + emit SigningKeyUpdate( + _operator, + block.number, + _newSigningKey, + oldSigningKey + ); + } + /// @notice Updates the weight of an operator and returns the previous and current weights. /// @param _operator The address of the operator to update the weight of. function _updateOperatorWeight( @@ -339,7 +408,7 @@ contract ECDSAStakeRegistry is uint256 newWeight; uint256 oldWeight = _operatorWeightHistory[_operator].latest(); if (!_operatorRegistered[_operator]) { - delta -= int(oldWeight); + delta -= int256(oldWeight); if (delta == 0) { return delta; } @@ -400,30 +469,33 @@ contract ECDSAStakeRegistry is /** * @notice Common logic to verify a batch of ECDSA signatures against a hash, using either last stake weight or at a specific block. * @param _dataHash The hash of the data the signers endorsed. - * @param _signers A collection of addresses that endorsed the data hash. + * @param _operators A collection of addresses that endorsed the data hash. * @param _signatures A collection of signatures matching the signers. * @param _referenceBlock The block number for evaluating stake weight; use max uint32 for latest weight. */ function _checkSignatures( bytes32 _dataHash, - address[] memory _signers, + address[] memory _operators, bytes[] memory _signatures, uint32 _referenceBlock ) internal view { - uint256 signersLength = _signers.length; - address lastSigner; + uint256 signersLength = _operators.length; + address currentOperator; + address lastOperator; + address signer; uint256 signedWeight; _validateSignaturesLength(signersLength, _signatures.length); for (uint256 i; i < signersLength; i++) { - address currentSigner = _signers[i]; + currentOperator = _operators[i]; + signer = _getOperatorSigningKey(currentOperator, _referenceBlock); - _validateSortedSigners(lastSigner, currentSigner); - _validateSignature(currentSigner, _dataHash, _signatures[i]); + _validateSortedSigners(lastOperator, currentOperator); + _validateSignature(signer, _dataHash, _signatures[i]); - lastSigner = currentSigner; + lastOperator = currentOperator; uint256 operatorWeight = _getOperatorWeight( - currentSigner, + currentOperator, _referenceBlock ); signedWeight += operatorWeight; @@ -473,6 +545,27 @@ contract ECDSAStakeRegistry is } } + /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. + /// @param _operator The operator to query their signing key history for + /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. + /// @return The weight of the operator. + function _getOperatorSigningKey( + address _operator, + uint32 _referenceBlock + ) internal view returns (address) { + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); + } + return + address( + uint160( + _operatorSigningKeyHistory[_operator].getAtBlock( + _referenceBlock + ) + ) + ); + } + /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. /// @param _signer The address of the signer whose weight is returned. /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. @@ -481,11 +574,10 @@ contract ECDSAStakeRegistry is address _signer, uint32 _referenceBlock ) internal view returns (uint256) { - if (_referenceBlock == type(uint32).max) { - return _operatorWeightHistory[_signer].latest(); - } else { - return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock); + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); } + return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock); } /// @notice Retrieve the total stake weight at a specific block or the latest if not specified. @@ -495,11 +587,10 @@ contract ECDSAStakeRegistry is function _getTotalWeight( uint32 _referenceBlock ) internal view returns (uint256) { - if (_referenceBlock == type(uint32).max) { - return _totalWeightHistory.latest(); - } else { - return _totalWeightHistory.getAtBlock(_referenceBlock); + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); } + return _totalWeightHistory.getAtBlock(_referenceBlock); } /// @notice Retrieves the threshold stake for a given reference block. @@ -509,11 +600,10 @@ contract ECDSAStakeRegistry is function _getThresholdStake( uint32 _referenceBlock ) internal view returns (uint256) { - if (_referenceBlock == type(uint32).max) { - return _thresholdWeightHistory.latest(); - } else { - return _thresholdWeightHistory.getAtBlock(_referenceBlock); + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); } + return _thresholdWeightHistory.getAtBlock(_referenceBlock); } /// @notice Validates that the cumulative stake of signed messages meets or exceeds the required threshold. diff --git a/src/unaudited/ECDSAStakeRegistryStorage.sol b/src/unaudited/ECDSAStakeRegistryStorage.sol index fe09b9d8..c285999c 100644 --- a/src/unaudited/ECDSAStakeRegistryStorage.sol +++ b/src/unaudited/ECDSAStakeRegistryStorage.sol @@ -5,7 +5,9 @@ import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/ import {CheckpointsUpgradeable} from "@openzeppelin-upgrades/contracts/utils/CheckpointsUpgradeable.sol"; import {ECDSAStakeRegistryEventsAndErrors, Quorum, StrategyParams} from "../interfaces/IECDSAStakeRegistryEventsAndErrors.sol"; -abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors { +abstract contract ECDSAStakeRegistryStorage is + ECDSAStakeRegistryEventsAndErrors +{ /// @notice Manages staking delegations through the DelegationManager interface IDelegationManager internal immutable DELEGATION_MANAGER; @@ -27,6 +29,10 @@ abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors /// @notice Defines the duration after which the stake's weight expires. uint256 internal _stakeExpiry; + /// @notice Maps an operator to their signing key history using checkpoints + mapping(address => CheckpointsUpgradeable.History) + internal _operatorSigningKeyHistory; + /// @notice Tracks the total stake history over time using checkpoints CheckpointsUpgradeable.History internal _totalWeightHistory; @@ -34,7 +40,8 @@ abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors CheckpointsUpgradeable.History internal _thresholdWeightHistory; /// @notice Maps operator addresses to their respective stake histories using checkpoints - mapping(address => CheckpointsUpgradeable.History) internal _operatorWeightHistory; + mapping(address => CheckpointsUpgradeable.History) + internal _operatorWeightHistory; /// @notice Maps an operator to their registration status mapping(address => bool) internal _operatorRegistered; @@ -47,5 +54,5 @@ abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors // slither-disable-next-line shadowing-state /// @dev Reserves storage slots for future upgrades // solhint-disable-next-line - uint256[42] private __gap; + uint256[39] private __gap; } diff --git a/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol b/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol index c5329ce2..ef2e691c 100644 --- a/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol +++ b/src/unaudited/examples/ECDSAStakeRegistryPermissioned.sol @@ -27,7 +27,9 @@ contract ECDSAStakeRegistryPermissioned is ECDSAStakeRegistry { /// @dev Custom error to signal that an operator is already allowlisted. error OperatorAlreadyAllowlisted(); - constructor(IDelegationManager _delegationManager) ECDSAStakeRegistry(_delegationManager) { + constructor( + IDelegationManager _delegationManager + ) ECDSAStakeRegistry(_delegationManager) { // _disableInitializers(); } @@ -63,38 +65,40 @@ contract ECDSAStakeRegistryPermissioned is ECDSAStakeRegistry { /// Doesn't register the operator into the operator set /// @param _operator The address of the operator to allowlist. function _permitOperator(address _operator) internal { - if (allowlistedOperators[_operator]){ + if (allowlistedOperators[_operator]) { revert OperatorAlreadyAllowlisted(); } allowlistedOperators[_operator] = true; emit OperatorPermitted(_operator); - } /// @dev Removes an operator from the allowlist. /// If the operator is registered, also deregisters the operator. /// @param _operator The address of the operator to be revoked. function _revokeOperator(address _operator) internal { - if (!allowlistedOperators[_operator]){ + if (!allowlistedOperators[_operator]) { revert OperatorNotAllowlisted(); } delete allowlistedOperators[_operator]; emit OperatorRevoked(_operator); - if (_operatorRegistered[_operator]){ + if (_operatorRegistered[_operator]) { _ejectOperator(_operator); } - } /// @inheritdoc ECDSAStakeRegistry function _registerOperatorWithSig( address _operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature, + address _operatorSigningKey ) internal override { - if (allowlistedOperators[_operator] != true){ + if (allowlistedOperators[_operator] != true) { revert OperatorNotAllowlisted(); } - super._registerOperatorWithSig(_operator, _operatorSignature); + super._registerOperatorWithSig( + _operator, + _operatorSignature, + _operatorSigningKey + ); } } - diff --git a/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol b/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol index 8775cd55..bc6337c5 100644 --- a/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryEqualWeightUnit.t.sol @@ -11,40 +11,80 @@ import {ECDSAStakeRegistryEqualWeight} from "../../src/unaudited/examples/ECDSAS contract EqualWeightECDSARegistry is ECDSAStakeRegistrySetup { ECDSAStakeRegistryEqualWeight internal fixedWeightRegistry; + function setUp() public virtual override { super.setUp(); - fixedWeightRegistry = new ECDSAStakeRegistryEqualWeight(IDelegationManager(address(mockDelegationManager))); + fixedWeightRegistry = new ECDSAStakeRegistryEqualWeight( + IDelegationManager(address(mockDelegationManager)) + ); IStrategy mockStrategy = IStrategy(address(0x1234)); Quorum memory quorum = Quorum({strategies: new StrategyParams[](1)}); - quorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 10000}); - fixedWeightRegistry.initialize(address(mockServiceManager), 100, quorum); + quorum.strategies[0] = StrategyParams({ + strategy: mockStrategy, + multiplier: 10000 + }); + fixedWeightRegistry.initialize( + address(mockServiceManager), + 100, + quorum + ); fixedWeightRegistry.permitOperator(operator1); fixedWeightRegistry.permitOperator(operator2); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - fixedWeightRegistry.registerOperatorWithSignature(operator1, operatorSignature); - fixedWeightRegistry.registerOperatorWithSignature(operator2, operatorSignature); + vm.prank(operator1); + fixedWeightRegistry.registerOperatorWithSignature( + operatorSignature, + operator1 + ); + vm.prank(operator2); + fixedWeightRegistry.registerOperatorWithSignature( + operatorSignature, + operator2 + ); } function test_FixedStakeUpdates() public { - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 1); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), + 1 + ); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), + 1 + ); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 2); vm.roll(block.number + 1); vm.prank(operator1); fixedWeightRegistry.deregisterOperator(); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 0); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), + 0 + ); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), + 1 + ); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 1); vm.roll(block.number + 1); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - fixedWeightRegistry.registerOperatorWithSignature(operator1, operatorSignature); + vm.prank(operator1); + fixedWeightRegistry.registerOperatorWithSignature( + operatorSignature, + operator1 + ); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 1); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), + 1 + ); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), + 1 + ); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 2); vm.roll(block.number + 1); @@ -53,8 +93,14 @@ contract EqualWeightECDSARegistry is ECDSAStakeRegistrySetup { operators[1] = operator2; fixedWeightRegistry.updateOperators(operators); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), 1); - assertEq(fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), 1); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator1), + 1 + ); + assertEq( + fixedWeightRegistry.getLastCheckpointOperatorWeight(operator2), + 1 + ); assertEq(fixedWeightRegistry.getLastCheckpointTotalWeight(), 2); } } diff --git a/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol b/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol index 57031b29..dffb9174 100644 --- a/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryPermissionedUnit.t.sol @@ -11,23 +11,41 @@ import {ECDSAStakeRegistryPermissioned} from "../../src/unaudited/examples/ECDSA contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { ECDSAStakeRegistryPermissioned internal permissionedRegistry; + function setUp() public virtual override { super.setUp(); - permissionedRegistry = new ECDSAStakeRegistryPermissioned(IDelegationManager(address(mockDelegationManager))); + permissionedRegistry = new ECDSAStakeRegistryPermissioned( + IDelegationManager(address(mockDelegationManager)) + ); IStrategy mockStrategy = IStrategy(address(0x1234)); Quorum memory quorum = Quorum({strategies: new StrategyParams[](1)}); - quorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 10000}); - permissionedRegistry.initialize(address(mockServiceManager), 100, quorum); + quorum.strategies[0] = StrategyParams({ + strategy: mockStrategy, + multiplier: 10000 + }); + permissionedRegistry.initialize( + address(mockServiceManager), + 100, + quorum + ); permissionedRegistry.permitOperator(operator1); permissionedRegistry.permitOperator(operator2); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - permissionedRegistry.registerOperatorWithSignature(operator1, operatorSignature); - permissionedRegistry.registerOperatorWithSignature(operator2, operatorSignature); + vm.prank(operator1); + permissionedRegistry.registerOperatorWithSignature( + operatorSignature, + operator1 + ); + vm.prank(operator2); + permissionedRegistry.registerOperatorWithSignature( + operatorSignature, + operator1 + ); } function test_RevertsWhen_NotOwner_PermitOperator() public { - address notOwner=address(0xBEEF); + address notOwner = address(0xBEEF); vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); permissionedRegistry.permitOperator(operator1); @@ -39,14 +57,14 @@ contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_RevertsWhen_NotOwner_RevokeOperator() public { - address notOwner=address(0xBEEF); + address notOwner = address(0xBEEF); vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); permissionedRegistry.revokeOperator(operator1); } function test_When_NotOperator_RevokeOperator() public { - address notOperator=address(0xBEEF); + address notOperator = address(0xBEEF); permissionedRegistry.permitOperator(notOperator); permissionedRegistry.revokeOperator(notOperator); @@ -57,14 +75,14 @@ contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { } function test_RevertsWhen_NotOwner_EjectOperator() public { - address notOwner=address(0xBEEF); + address notOwner = address(0xBEEF); vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); permissionedRegistry.ejectOperator(operator1); } function test_RevertsWhen_NotOperator_EjectOperator() public { - address notOperator=address(0xBEEF); + address notOperator = address(0xBEEF); vm.expectRevert(abi.encodeWithSelector(OperatorNotRegistered.selector)); permissionedRegistry.ejectOperator(notOperator); } @@ -77,26 +95,40 @@ contract PermissionedECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { address operator3 = address(0xBEEF); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - vm.expectRevert(abi.encodeWithSelector(ECDSAStakeRegistryPermissioned.OperatorNotAllowlisted.selector)); - permissionedRegistry.registerOperatorWithSignature(operator3, operatorSignature); - + vm.expectRevert( + abi.encodeWithSelector( + ECDSAStakeRegistryPermissioned.OperatorNotAllowlisted.selector + ) + ); + vm.prank(operator3); + permissionedRegistry.registerOperatorWithSignature( + operatorSignature, + operator3 + ); } function test_WhenAllowlisted_RegisterOperatorWithSig() public { address operator3 = address(0xBEEF); permissionedRegistry.permitOperator(operator3); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - permissionedRegistry.registerOperatorWithSignature(operator3, operatorSignature); + vm.prank(operator3); + permissionedRegistry.registerOperatorWithSignature( + operatorSignature, + operator3 + ); } function test_DeregisterOperator() public { address operator3 = address(0xBEEF); permissionedRegistry.permitOperator(operator3); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - permissionedRegistry.registerOperatorWithSignature(operator3, operatorSignature); + vm.prank(operator3); + permissionedRegistry.registerOperatorWithSignature( + operatorSignature, + operator3 + ); vm.prank(operator3); permissionedRegistry.deregisterOperator(); } - } diff --git a/test/unit/ECDSAStakeRegistryUnit.t.sol b/test/unit/ECDSAStakeRegistryUnit.t.sol index 7ffbb7d8..d374144d 100644 --- a/test/unit/ECDSAStakeRegistryUnit.t.sol +++ b/test/unit/ECDSAStakeRegistryUnit.t.sol @@ -10,7 +10,6 @@ import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy import {ECDSAStakeRegistry} from "../../src/unaudited/ECDSAStakeRegistry.sol"; import {ECDSAStakeRegistryEventsAndErrors, Quorum, StrategyParams} from "../../src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol"; - contract MockServiceManager { // solhint-disable-next-line function deregisterOperatorFromAVS(address) external {} @@ -26,9 +25,12 @@ contract MockDelegationManager { return 1000; // Return a dummy value for simplicity } - function getOperatorShares(address, address[] memory strategies) external pure returns (uint256[] memory) { - uint256[] memory response = new uint256[](strategies.length); - for (uint256 i; i < strategies.length; i++){ + function getOperatorShares( + address, + address[] memory strategies + ) external pure returns (uint256[] memory) { + uint256[] memory response = new uint256[](strategies.length); + for (uint256 i; i < strategies.length; i++) { response[i] = 1000; } return response; // Return a dummy value for simplicity @@ -38,6 +40,7 @@ contract MockDelegationManager { contract ECDSAStakeRegistrySetup is Test, ECDSAStakeRegistryEventsAndErrors { MockDelegationManager public mockDelegationManager; MockServiceManager public mockServiceManager; + ECDSAStakeRegistry public registry; address internal operator1; address internal operator2; uint256 internal operator1Pk; @@ -53,33 +56,36 @@ contract ECDSAStakeRegistrySetup is Test, ECDSAStakeRegistryEventsAndErrors { (operator2, operator2Pk) = makeAddrAndKey("Signer 2"); mockDelegationManager = new MockDelegationManager(); mockServiceManager = new MockServiceManager(); - - } -} - -contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup{ - ECDSAStakeRegistry public registry; - - function setUp() public virtual override { - super.setUp(); IStrategy mockStrategy = IStrategy(address(0x1234)); Quorum memory quorum = Quorum({strategies: new StrategyParams[](1)}); - quorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 10000}); - registry = new ECDSAStakeRegistry(IDelegationManager(address(mockDelegationManager))); + quorum.strategies[0] = StrategyParams({ + strategy: mockStrategy, + multiplier: 10_000 + }); + registry = new ECDSAStakeRegistry( + IDelegationManager(address(mockDelegationManager)) + ); registry.initialize(address(mockServiceManager), 100, quorum); ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; - registry.registerOperatorWithSignature(operator1, operatorSignature); - registry.registerOperatorWithSignature(operator2, operatorSignature); - + vm.prank(operator1); + registry.registerOperatorWithSignature(operatorSignature, operator1); + vm.prank(operator2); + registry.registerOperatorWithSignature(operatorSignature, operator2); + vm.roll(block.number + 1); } +} -function test_UpdateQuorumConfig() public { +contract ECDSAStakeRegistryTest is ECDSAStakeRegistrySetup { + function test_UpdateQuorumConfig() public { IStrategy mockStrategy = IStrategy(address(420)); Quorum memory oldQuorum = registry.quorum(); Quorum memory newQuorum = Quorum({strategies: new StrategyParams[](1)}); - newQuorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 10000}); - address[] memory operators = new address[](2); + newQuorum.strategies[0] = StrategyParams({ + strategy: mockStrategy, + multiplier: 10_000 + }); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; @@ -90,25 +96,34 @@ function test_UpdateQuorumConfig() public { } function test_RevertsWhen_InvalidQuorum_UpdateQuourmConfig() public { - Quorum memory invalidQuorum = Quorum({strategies: new StrategyParams[](1)}); + Quorum memory invalidQuorum = Quorum({ + strategies: new StrategyParams[](1) + }); invalidQuorum.strategies[0] = StrategyParams({ /// TODO: Make mock strategy strategy: IStrategy(address(420)), multiplier: 5000 // This should cause the update to revert as it's not the total required }); - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InvalidQuorum.selector); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InvalidQuorum.selector + ); registry.updateQuorumConfig(invalidQuorum, operators); } function test_RevertsWhen_NotOwner_UpdateQuorumConfig() public { - Quorum memory validQuorum = Quorum({strategies: new StrategyParams[](1)}); - validQuorum.strategies[0] = StrategyParams({strategy: IStrategy(address(420)), multiplier: 10000}); + Quorum memory validQuorum = Quorum({ + strategies: new StrategyParams[](1) + }); + validQuorum.strategies[0] = StrategyParams({ + strategy: IStrategy(address(420)), + multiplier: 10_000 + }); - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; @@ -121,7 +136,7 @@ function test_UpdateQuorumConfig() public { function test_RevertsWhen_SameQuorum_UpdateQuorumConfig() public { Quorum memory quorum = registry.quorum(); - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; @@ -130,58 +145,89 @@ function test_UpdateQuorumConfig() public { } function test_RevertSWhen_Duplicate_UpdateQuorumConfig() public { - Quorum memory validQuorum = Quorum({strategies: new StrategyParams[](2)}); - validQuorum.strategies[0] = StrategyParams({strategy: IStrategy(address(420)), multiplier: 5_000}); - address[] memory operators = new address[](2); + Quorum memory invalidQuorum = Quorum({ + strategies: new StrategyParams[](2) + }); + invalidQuorum.strategies[0] = StrategyParams({ + strategy: IStrategy(address(420)), + multiplier: 5000 + }); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; - validQuorum.strategies[1] = StrategyParams({strategy: IStrategy(address(420)), multiplier: 5_000}); + invalidQuorum.strategies[1] = StrategyParams({ + strategy: IStrategy(address(420)), + multiplier: 5000 + }); vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.NotSorted.selector); - registry.updateQuorumConfig(validQuorum, operators); + registry.updateQuorumConfig(invalidQuorum, operators); } function test_RevertSWhen_NotSorted_UpdateQuorumConfig() public { - Quorum memory validQuorum = Quorum({strategies: new StrategyParams[](2)}); - validQuorum.strategies[0] = StrategyParams({strategy: IStrategy(address(420)), multiplier: 5_000}); - address[] memory operators = new address[](2); + Quorum memory invalidQuorum = Quorum({ + strategies: new StrategyParams[](2) + }); + invalidQuorum.strategies[0] = StrategyParams({ + strategy: IStrategy(address(420)), + multiplier: 5000 + }); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; - validQuorum.strategies[1] = StrategyParams({strategy: IStrategy(address(419)), multiplier: 5_000}); + invalidQuorum.strategies[1] = StrategyParams({ + strategy: IStrategy(address(419)), + multiplier: 5000 + }); vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.NotSorted.selector); - registry.updateQuorumConfig(validQuorum, operators); + registry.updateQuorumConfig(invalidQuorum, operators); } function test_RevertSWhen_OverMultiplierTotal_UpdateQuorumConfig() public { - Quorum memory validQuorum = Quorum({strategies: new StrategyParams[](1)}); - validQuorum.strategies[0] = StrategyParams({strategy: IStrategy(address(420)), multiplier: 10001}); - address[] memory operators = new address[](2); + Quorum memory invalidQuorum = Quorum({ + strategies: new StrategyParams[](1) + }); + invalidQuorum.strategies[0] = StrategyParams({ + strategy: IStrategy(address(420)), + multiplier: 10_001 + }); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InvalidQuorum.selector); - registry.updateQuorumConfig(validQuorum,operators); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InvalidQuorum.selector + ); + registry.updateQuorumConfig(invalidQuorum, operators); } function test_RegisterOperatorWithSignature() public { address operator3 = address(0x125); ISignatureUtils.SignatureWithSaltAndExpiry memory signature; - registry.registerOperatorWithSignature(operator3, signature); + vm.prank(operator3); + registry.registerOperatorWithSignature(signature, operator3); assertTrue(registry.operatorRegistered(operator3)); assertEq(registry.getLastCheckpointOperatorWeight(operator3), 1000); } - function test_RevertsWhen_AlreadyRegistered_RegisterOperatorWithSignature() public { + function test_RevertsWhen_AlreadyRegistered_RegisterOperatorWithSignature() + public + { assertEq(registry.getLastCheckpointOperatorWeight(operator1), 1000); assertEq(registry.getLastCheckpointTotalWeight(), 2000); ISignatureUtils.SignatureWithSaltAndExpiry memory signature; - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.OperatorAlreadyRegistered.selector); - registry.registerOperatorWithSignature(operator1, signature); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.OperatorAlreadyRegistered.selector + ); + vm.prank(operator1); + registry.registerOperatorWithSignature(signature, operator1); } - function test_RevertsWhen_SignatureIsInvalid_RegisterOperatorWithSignature() public { + function test_RevertsWhen_SignatureIsInvalid_RegisterOperatorWithSignature() + public + { bytes memory signatureData; vm.mockCall( address(mockServiceManager), @@ -212,7 +258,9 @@ function test_UpdateQuorumConfig() public { function test_RevertsWhen_NotOperator_DeregisterOperator() public { address notOperator = address(0x2); vm.prank(notOperator); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.OperatorNotRegistered.selector); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.OperatorNotRegistered.selector + ); registry.deregisterOperator(); } @@ -229,7 +277,6 @@ function test_UpdateQuorumConfig() public { operators[2] = operator3; registry.updateOperators(operators); assertEq(registry.getLastCheckpointOperatorWeight(operator3), 0); - } function test_When_SingleOperator_UpdateOperators() public { @@ -237,7 +284,9 @@ function test_UpdateQuorumConfig() public { operators[0] = operator1; registry.updateOperators(operators); - uint256 updatedWeight = registry.getLastCheckpointOperatorWeight(operator1); + uint256 updatedWeight = registry.getLastCheckpointOperatorWeight( + operator1 + ); assertEq(updatedWeight, 1000); } @@ -269,8 +318,12 @@ function test_UpdateQuorumConfig() public { registry.updateOperators(operators); - uint256 updatedWeight1 = registry.getLastCheckpointOperatorWeight(operator1); - uint256 updatedWeight2 = registry.getLastCheckpointOperatorWeight(operator2); + uint256 updatedWeight1 = registry.getLastCheckpointOperatorWeight( + operator1 + ); + uint256 updatedWeight2 = registry.getLastCheckpointOperatorWeight( + operator2 + ); assertEq(updatedWeight1, 1000); assertEq(updatedWeight2, 1000); } @@ -282,7 +335,9 @@ function test_UpdateQuorumConfig() public { registry.updateOperators(operators); - uint256 updatedWeight = registry.getLastCheckpointOperatorWeight(operator1); + uint256 updatedWeight = registry.getLastCheckpointOperatorWeight( + operator1 + ); assertEq(updatedWeight, 1000); } @@ -291,8 +346,14 @@ function test_UpdateQuorumConfig() public { IStrategy mockStrategy2 = IStrategy(address(421)); Quorum memory quorum = Quorum({strategies: new StrategyParams[](2)}); - quorum.strategies[0] = StrategyParams({strategy: mockStrategy, multiplier: 5_000}); - quorum.strategies[1] = StrategyParams({strategy: mockStrategy2, multiplier: 5_000}); + quorum.strategies[0] = StrategyParams({ + strategy: mockStrategy, + multiplier: 5000 + }); + quorum.strategies[1] = StrategyParams({ + strategy: mockStrategy2, + multiplier: 5000 + }); address[] memory operators = new address[](2); operators[0] = operator1; @@ -302,20 +363,28 @@ function test_UpdateQuorumConfig() public { address[] memory strategies = new address[](2); uint256[] memory shares = new uint256[](2); - strategies[0]=address(mockStrategy); - strategies[1]=address(mockStrategy2); + strategies[0] = address(mockStrategy); + strategies[1] = address(mockStrategy2); shares[0] = 50; shares[1] = 1000; vm.mockCall( address(mockDelegationManager), - abi.encodeWithSelector(MockDelegationManager.getOperatorShares.selector, operator1, strategies), + abi.encodeWithSelector( + MockDelegationManager.getOperatorShares.selector, + operator1, + strategies + ), abi.encode(shares) ); registry.updateOperators(operators); - uint256 updatedWeight1 = registry.getLastCheckpointOperatorWeight(operator1); - uint256 updatedWeight2 = registry.getLastCheckpointOperatorWeight(operator2); + uint256 updatedWeight1 = registry.getLastCheckpointOperatorWeight( + operator1 + ); + uint256 updatedWeight2 = registry.getLastCheckpointOperatorWeight( + operator2 + ); assertEq(updatedWeight1, 525); assertEq(updatedWeight2, 1000); vm.roll(block.number + 1); @@ -327,7 +396,7 @@ function test_UpdateQuorumConfig() public { assertEq(initialMinimumWeight, 0); // Assuming initial state is 0 - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; registry.updateMinimumWeight(newMinimumWeight, operators); @@ -338,7 +407,7 @@ function test_UpdateQuorumConfig() public { function test_RevertsWhen_NotOwner_UpdateMinimumWeight() public { uint256 newMinimumWeight = 5000; - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; vm.prank(address(0xBEEF)); // An arbitrary non-owner address @@ -348,7 +417,7 @@ function test_UpdateQuorumConfig() public { function test_When_SameWeight_UpdateMinimumWeight() public { uint256 initialMinimumWeight = 5000; - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; registry.updateMinimumWeight(initialMinimumWeight, operators); @@ -359,7 +428,7 @@ function test_UpdateQuorumConfig() public { function test_When_Weight0_UpdateMinimumWeight() public { uint256 initialMinimumWeight = 5000; - address[] memory operators = new address[](2); + address[] memory operators = new address[](2); operators[0] = operator1; operators[1] = operator2; registry.updateMinimumWeight(initialMinimumWeight, operators); @@ -371,19 +440,21 @@ function test_UpdateQuorumConfig() public { uint256 updatedMinimumWeight = registry.minimumWeight(); assertEq(updatedMinimumWeight, newMinimumWeight); } + function testUpdateThresholdStake_UpdateThresholdStake() public { - uint256 thresholdWeight = 10000000000; + uint256 thresholdWeight = 10_000_000_000; vm.prank(registry.owner()); registry.updateStakeThreshold(thresholdWeight); } function test_RevertsWhen_NotOwner_UpdateThresholdStake() public { - uint256 thresholdWeight = 10000000000; + uint256 thresholdWeight = 10_000_000_000; address notOwner = address(0x123); vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); registry.updateStakeThreshold(thresholdWeight); } + function test_CheckSignatures() public { msgHash = keccak256("data"); signers = new address[](2); @@ -394,7 +465,10 @@ function test_UpdateQuorumConfig() public { (v, r, s) = vm.sign(operator2Pk, msgHash); signatures[1] = abi.encodePacked(r, s, v); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, type(uint32).max)); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevertsWhen_LengthMismatch_CheckSignatures() public { @@ -405,8 +479,13 @@ function test_UpdateQuorumConfig() public { (uint8 v, bytes32 r, bytes32 s) = vm.sign(operator1Pk, msgHash); signatures[0] = abi.encode(v, r, s); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.LengthMismatch.selector); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, type(uint32).max)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.LengthMismatch.selector + ); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevertsWhen_InvalidLength_CheckSignatures() public { @@ -414,8 +493,13 @@ function test_UpdateQuorumConfig() public { address[] memory signers = new address[](0); bytes[] memory signatures = new bytes[](0); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InvalidLength.selector); - registry.isValidSignature(dataHash, abi.encode(signers, signatures, type(uint32).max)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InvalidLength.selector + ); + registry.isValidSignature( + dataHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevertsWhen_NotSorted_CheckSignatures() public { @@ -430,14 +514,17 @@ function test_UpdateQuorumConfig() public { signatures[0] = abi.encodePacked(r, s, v); vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.NotSorted.selector); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, type(uint32).max)); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevertsWhen_Duplicates_CheckSignatures() public { msgHash = keccak256("data"); signers = new address[](2); - signers[1]=operator1; - signers[0]=operator1; + signers[1] = operator1; + signers[0] = operator1; /// Duplicate assertEq(signers[0], signers[1]); @@ -448,7 +535,10 @@ function test_UpdateQuorumConfig() public { signatures[1] = abi.encodePacked(r, s, v); vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.NotSorted.selector); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, type(uint32).max)); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevetsWhen_InvalidSignature_CheckSignatures() public { @@ -458,8 +548,13 @@ function test_UpdateQuorumConfig() public { bytes[] memory signatures = new bytes[](1); signatures[0] = "invalid-signature"; - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InvalidSignature.selector); - registry.isValidSignature(dataHash, abi.encode(signers, signatures, type(uint32).max)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InvalidSignature.selector + ); + registry.isValidSignature( + dataHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevertsWhen_InsufficientSignedStake_CheckSignatures() public { @@ -473,19 +568,27 @@ function test_UpdateQuorumConfig() public { (v, r, s) = vm.sign(operator2Pk, msgHash); signatures[1] = abi.encodePacked(r, s, v); - uint256 thresholdWeight = 10000000000; + uint256 thresholdWeight = 10_000_000_000; vm.prank(registry.owner()); registry.updateStakeThreshold(thresholdWeight); vm.roll(block.number + 1); vm.mockCall( address(registry), - abi.encodeWithSelector(ECDSAStakeRegistry.getLastCheckpointOperatorWeight.selector, operator1), + abi.encodeWithSelector( + ECDSAStakeRegistry.getLastCheckpointOperatorWeight.selector, + operator1 + ), abi.encode(50) ); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InsufficientSignedStake.selector); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, type(uint32).max)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InsufficientSignedStake.selector + ); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, block.number - 1) + ); } function test_RevertsWhen_LengthMismatch_CheckSignaturesAtBlock() public { @@ -496,8 +599,13 @@ function test_UpdateQuorumConfig() public { signers[1] = operator2; bytes[] memory signatures = new bytes[](1); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.LengthMismatch.selector); - registry.isValidSignature(dataHash, abi.encode(signers, signatures, referenceBlock)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.LengthMismatch.selector + ); + registry.isValidSignature( + dataHash, + abi.encode(signers, signatures, referenceBlock) + ); } function test_RevertsWhen_InvalidLength_CheckSignaturesAtBlock() public { @@ -506,8 +614,13 @@ function test_UpdateQuorumConfig() public { address[] memory signers = new address[](0); bytes[] memory signatures = new bytes[](0); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InvalidLength.selector); - registry.isValidSignature(dataHash, abi.encode(signers, signatures, referenceBlock)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InvalidLength.selector + ); + registry.isValidSignature( + dataHash, + abi.encode(signers, signatures, referenceBlock) + ); } function test_RevertsWhen_NotSorted_CheckSignaturesAtBlock() public { @@ -524,10 +637,15 @@ function test_UpdateQuorumConfig() public { signatures[0] = abi.encodePacked(r, s, v); vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.NotSorted.selector); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, referenceBlock)); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, referenceBlock) + ); } - function test_RevetsWhen_InsufficientSignedStake_CheckSignaturesAtBlock() public { + function test_RevetsWhen_InsufficientSignedStake_CheckSignaturesAtBlock() + public + { uint32 referenceBlock = 123; msgHash = keccak256("data"); signers = new address[](2); @@ -539,19 +657,28 @@ function test_UpdateQuorumConfig() public { (v, r, s) = vm.sign(operator2Pk, msgHash); signatures[1] = abi.encodePacked(r, s, v); - uint256 thresholdWeight = 10000000000; + uint256 thresholdWeight = 10_000_000_000; vm.prank(registry.owner()); registry.updateStakeThreshold(thresholdWeight); vm.roll(referenceBlock + 1); vm.mockCall( address(registry), - abi.encodeWithSelector(ECDSAStakeRegistry.getOperatorWeightAtBlock.selector, operator1, referenceBlock), + abi.encodeWithSelector( + ECDSAStakeRegistry.getOperatorWeightAtBlock.selector, + operator1, + referenceBlock + ), abi.encode(50) ); - vm.expectRevert(ECDSAStakeRegistryEventsAndErrors.InsufficientSignedStake.selector); - registry.isValidSignature(msgHash, abi.encode(signers, signatures, referenceBlock)); + vm.expectRevert( + ECDSAStakeRegistryEventsAndErrors.InsufficientSignedStake.selector + ); + registry.isValidSignature( + msgHash, + abi.encode(signers, signatures, referenceBlock) + ); } function test_Gas_UpdateOperators() public { @@ -564,14 +691,18 @@ function test_UpdateQuorumConfig() public { ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; address[] memory operators = new address[](30); - for (uint256 i; i< operators.length;i++) { - operators[i]=address(uint160(i)); - registry.registerOperatorWithSignature(operators[i], operatorSignature); + for (uint256 i; i < operators.length; i++) { + operators[i] = address(uint160(i)); + vm.prank(operators[i]); + registry.registerOperatorWithSignature( + operatorSignature, + operators[i] + ); } vm.resumeGasMetering(); registry.updateOperators(operators); - emit log_named_uint("Gas consumed",before - gasleft()); + emit log_named_uint("Gas consumed", before - gasleft()); } function test_Gas_CheckSignatures() public { @@ -589,23 +720,259 @@ function test_UpdateQuorumConfig() public { uint8 v; bytes32 r; bytes32 s; - for (uint256 i=1; i< operators.length+1;i++) { - operators[i-1]=address(vm.addr(i)); - registry.registerOperatorWithSignature(operators[i-1], operatorSignature); + for (uint256 i = 1; i < operators.length + 1; i++) { + operators[i - 1] = address(vm.addr(i)); + vm.prank(operators[i - 1]); + registry.registerOperatorWithSignature( + operatorSignature, + operators[i - 1] + ); (v, r, s) = vm.sign(i, msgHash); - signatures[i-1] = abi.encodePacked(r, s, v); + signatures[i - 1] = abi.encodePacked(r, s, v); } (operators, signatures) = _sort(operators, signatures); registry.updateOperators(operators); + vm.roll(block.number + 1); vm.resumeGasMetering(); - registry.isValidSignature(msgHash, abi.encode(operators, signatures, type(uint32).max)); - emit log_named_uint("Gas consumed",before - gasleft()); + registry.isValidSignature( + msgHash, + abi.encode(operators, signatures, block.number - 1) + ); + + emit log_named_uint("Gas consumed", before - gasleft()); + } + + // Define private and public keys for operator3 and signer + uint256 private operator3Pk = 3; + address private operator3 = address(vm.addr(operator3Pk)); + uint256 private signerPk = 4; + address private signer = address(vm.addr(signerPk)); + + function test_WhenUsingSigningKey_RegierOperatorWithSignature() public { + address operator = operator3; + + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + + // Register operator with a different signing key + vm.prank(operator); + registry.registerOperatorWithSignature(operatorSignature, signer); + + // Verify that the signing key has been successfully registered for the operator + address registeredSigningKey = registry.getLastestOperatorSigningKey( + operator + ); + assertEq( + registeredSigningKey, + signer, + "The registered signing key does not match the provided signing key" + ); + } + + function test_Twice_RegierOperatorWithSignature() public { + address operator = operator3; + + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + + // Register operator with a different signing key + vm.prank(operator); + registry.registerOperatorWithSignature(operatorSignature, signer); + + /// Register a second time + vm.prank(operator); + registry.updateOperatorSigningKey(address(420)); + + // Verify that the signing key has been successfully registered for the operator + address registeredSigningKey = registry.getLastestOperatorSigningKey( + operator + ); + + vm.roll(block.number + 1); + registeredSigningKey = registry.getOperatorSigningKeyAtBlock( + operator, + uint32(block.number - 1) + ); + assertEq( + registeredSigningKey, + address(420), + "The registered signing key does not match the provided signing key" + ); + } + + function test_WhenUsingSigningKey_CheckSignatures() public { + address operator = operator3; + + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + + // Register operator with a different signing key + vm.prank(operator); + registry.registerOperatorWithSignature(operatorSignature, signer); + vm.roll(block.number + 1); + + // Prepare data for signature + bytes32 dataHash = keccak256("data"); + address[] memory operators = new address[](1); + operators[0] = operator; + bytes[] memory signatures = new bytes[](1); + + // Generate signature using the signing key + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, dataHash); + signatures[0] = abi.encodePacked(r, s, v); + + // Check signatures using the registered signing key + registry.isValidSignature( + dataHash, + abi.encode(operators, signatures, block.number - 1) + ); } - function _sort(address[] memory operators, bytes[] memory signatures) internal pure returns (address[] memory, bytes[] memory) { - require(operators.length == signatures.length, "Operators and signatures length mismatch"); - + function test_WhenUsingSigningKey_CheckSignaturesAtBlock() public { + address operator = operator3; + address initialSigningKey = address(vm.addr(signerPk)); + address updatedSigningKey = address(vm.addr(signerPk + 1)); + + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + + // Register operator with the initial signing key + vm.prank(operator); + registry.registerOperatorWithSignature( + operatorSignature, + initialSigningKey + ); + vm.roll(block.number + 1); + + // Prepare data for signature with initial signing key + bytes32 dataHash = keccak256("data"); + address[] memory operators = new address[](1); + operators[0] = operator; + bytes[] memory signatures = new bytes[](1); + + // Generate signature using the initial signing key + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, dataHash); + signatures[0] = abi.encodePacked(r, s, v); + + // Check signatures using the initial registered signing key + registry.isValidSignature( + dataHash, + abi.encode(operators, signatures, block.number - 1) + ); + + // Increase block number + vm.roll(block.number + 10); + + // Update operator's signing key + vm.prank(operator); + registry.updateOperatorSigningKey(updatedSigningKey); + vm.roll(block.number + 1); + + // Generate signature using the updated signing key + (v, r, s) = vm.sign(signerPk + 1, dataHash); + signatures[0] = abi.encodePacked(r, s, v); + + // Check signatures using the updated registered signing key + registry.isValidSignature( + dataHash, + abi.encode(operators, signatures, block.number - 1) + ); + } + + function test_WhenUsingPriorSigningKey_CheckSignaturesAtBlock() public { + address operator = operator3; + address initialSigningKey = address(vm.addr(signerPk)); + address updatedSigningKey = address(vm.addr(signerPk + 1)); + + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature; + + // Register operator with the initial signing key + vm.prank(operator); + registry.registerOperatorWithSignature( + operatorSignature, + initialSigningKey + ); + vm.roll(block.number + 1); + + // Prepare data for signature with initial signing key + bytes32 dataHash = keccak256("data"); + address[] memory operators = new address[](1); + operators[0] = operator; + bytes[] memory signatures = new bytes[](1); + + // Generate signature using the initial signing key + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, dataHash); + signatures[0] = abi.encodePacked(r, s, v); + + // Increase block number + vm.roll(block.number + 10); + + // Update operator's signing key + vm.prank(operator); + registry.updateOperatorSigningKey(updatedSigningKey); + + // Check signatures using the initial registered signing key at the previous block + registry.isValidSignature( + dataHash, + abi.encode(operators, signatures, block.number - 10) + ); + } + + function test_RevertsWhen_SigningCurrentBlock_IsValidSignature() public { + address operator = operator1; + address signingKey = address(vm.addr(signerPk)); + bytes32 dataHash = keccak256(abi.encodePacked("test data")); + uint256 currentBlock = block.number; + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, dataHash); + bytes memory signature = abi.encodePacked(r, s, v); + address[] memory operators = new address[](1); + operators[0] = operator; + bytes[] memory signatures = new bytes[](1); + signatures[0] = signature; + + vm.expectRevert(abi.encodeWithSignature("InvalidReferenceBlock()")); + registry.isValidSignature( + dataHash, + abi.encode(operators, signatures, currentBlock) + ); + } + + function test_RevertsWhen_SigningKeyNotValidAtBlock_IsValidSignature() + public + { + address operator = operator1; + uint256 invalidSignerPk = signerPk + 1; + address updatedSigningKey = address(vm.addr(invalidSignerPk)); + /// Different key to simulate invalid signing key + bytes32 dataHash = keccak256(abi.encodePacked("test data")); + uint256 referenceBlock = block.number; + /// Past reference block where the signer update won't be valid + vm.roll(block.number + 1); + + vm.prank(operator); + registry.updateOperatorSigningKey(address(updatedSigningKey)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(invalidSignerPk, dataHash); + bytes memory signature = abi.encodePacked(r, s, v); + address[] memory operators = new address[](1); + operators[0] = operator; + bytes[] memory signatures = new bytes[](1); + signatures[0] = signature; + + vm.expectRevert(abi.encodeWithSignature("InvalidSignature()")); + registry.isValidSignature( + dataHash, + abi.encode(operators, signatures, referenceBlock) + ); + } + + function _sort( + address[] memory operators, + bytes[] memory signatures + ) internal pure returns (address[] memory, bytes[] memory) { + require( + operators.length == signatures.length, + "Operators and signatures length mismatch" + ); + uint256 length = operators.length; for (uint256 i = 0; i < length - 1; i++) { uint256 minIndex = i;