From 3a2b3b637dad0bda5b9ebdf82f0727c998805522 Mon Sep 17 00:00:00 2001 From: Michael Sun <35479365+8sunyuan@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:55:34 -0500 Subject: [PATCH] Test: StakeRegistry unit tests (#118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: refactor and add tests to bitmap unit * test: added tests and using asserts * chore: remove single quote * feat: addNumberToBitmap function * test: tree file and Alexs bitmap fix * test: add back updated stakeRegistry tests * test: config tests setMinimumStake tests addStrategies tests initializeQuorum tests * refactor: using MockAVSDeployer for test file * test: more config tests * refactor: stake registry harness weighting * fix: stake weighting refactor removed overriding weightOfOperator functions in StakeRegistry and fixed broken tests using the helper _setOperatorWeight() * test: refactor and add tests to bitmap unit * test: added tests and using asserts * chore: remove single quote * feat: addNumberToBitmap function * test: tree file and Alexs bitmap fix * test: add back updated stakeRegistry tests * test: config tests setMinimumStake tests addStrategies tests initializeQuorum tests * refactor: using MockAVSDeployer for test file * test: more config tests * refactor: stake registry harness weighting * fix: stake weighting refactor removed overriding weightOfOperator functions in StakeRegistry and fixed broken tests using the helper _setOperatorWeight() * fix: undo change * chore: rebase fixes * test: voteweighing tests * chore: updated eigenlayer-contract ref to m2-mainnet head (#119) * updated eigenlayer-contract ref to m2-mainnet head * fixed test - interface mismatch from previous update * docs: update reg coord to include pubkey registration and service man… (#116) * docs: update reg coord to include pubkey registration and service manager usage * docs: add service manager to tech docs intro * fix: update table of contents * docs: add docs for BLSSignatureChecker and OperatorStateRetriever * docs: address feedback * docs: add documentation for each registry (#125) * docs: standardize capitalization of Operator since thats what we do everywhere else * docs: add BLSApkRegistry docs * chore: remove old files - i've incorporated all the info from these files into the current docs, so i'm removing them * docs: add wip for IndexRegistry and StakeRegistry, and fix spacing in StakeRegistry * docs: add IndexRegistry docs * docs: Add StakeRegistry * docs: clarify wording * test: fix unit tests * feat: registry coordinator unit test improvements (#121) * feat: add tree diagram for RegistryManager * chore: reorder and rename tests * feat: add a couple simple tests note that the `test_createQuorum` test is currently failing because initialization already sets up the max number of quorums something will need to be adjusted here * docs: update reg coord to include pubkey registration and service man… (#116) * docs: update reg coord to include pubkey registration and service manager usage * docs: add service manager to tech docs intro * fix: update table of contents * docs: add docs for BLSSignatureChecker and OperatorStateRetriever * docs: address feedback * docs: add documentation for each registry (#125) * docs: standardize capitalization of Operator since thats what we do everywhere else * docs: add BLSApkRegistry docs * chore: remove old files - i've incorporated all the info from these files into the current docs, so i'm removing them * docs: add wip for IndexRegistry and StakeRegistry, and fix spacing in StakeRegistry * docs: add IndexRegistry docs * docs: Add StakeRegistry * docs: clarify wording * chore: fix tree file name * chore: add a couple post-checks on state also fix a couple test names * chore: fix breaking test set up one less than the max number of quorums in a single test's setup, rather than the full max number, so that the test will properly allow the creation of a new quorum also fix a typo in a test name * feat: add tree diagram for RegistryManager * chore: reorder and rename tests * feat: add a couple simple tests note that the `test_createQuorum` test is currently failing because initialization already sets up the max number of quorums something will need to be adjusted here * chore: fix tree file name * chore: add a couple post-checks on state also fix a couple test names * chore: fix breaking test set up one less than the max number of quorums in a single test's setup, rather than the full max number, so that the test will properly allow the creation of a new quorum also fix a typo in a test name * feat: add a test for partial deregistration also improve some formatting + fix some typos, and add a touch more documentation * feat: expose more internal functions in harnessed contract * feat: add some simple coverage for the internal `_registerOperator` fnc * feat: add test coverage for the internal `deregisterOperator` fnc also clarify somewhat ambiguous wording in the tree file * feat: add simple test coverage for the internal `_updateOperatorBitmap` fnc * chore: remove commitlint job that reviews all commits in PR from CI This was causing a *lot* of CI failures, including for merge commits With this commit, CI will still check the _latest_ commit for meeting conventions, it just won't run over all commits in a PR This may lead to a few more "unconventional" commits making it through, but the CI should still flag when someone is just not using conventional commits at all, which I think was the original goal. * feat: add some coverage for `updateOperators(ForQuorums)` fncs * feat: add testing for `updateOperatorsForQuorum` function * feat: add some coverage for complex view functions note: this commit also adds a TODO around a currently-failing test. I plan to discuss the correct path forwards here and then push another commit. * chore: clarify NatSpec comments * fix: make `getQuorumBitmapIndicesAtBlockNumber` revert if operator was registered the logic is now more in-line with the logic in the StakeRegistry -- for reference, see: https://github.com/layr-labs/eigenlayer-middleware/blob/ 98f884454d9e9de1e344bb6fba9a2cd3915e5b57/src/StakeRegistry.sol#L297-L299 * feat: add simple unit test for `getQuorumBitmapIndicesAtBlockNumber` also improve wording in the 'tree' file * chore: move `_getQuorumBitmapIndexAtBlockNumber` into the section with other internal fncs * chore: remove unnecessary require statement + improve code clarity * fix: correct a compiler error for implicit type conversion * feat: address TODOs in tests * feat: add tree diagram for RegistryManager * chore: fix tree file name * feat: add a test for partial deregistration also improve some formatting + fix some typos, and add a touch more documentation * feat: expose more internal functions in harnessed contract * feat: add some simple coverage for the internal `_registerOperator` fnc * feat: add test coverage for the internal `deregisterOperator` fnc also clarify somewhat ambiguous wording in the tree file * feat: add simple test coverage for the internal `_updateOperatorBitmap` fnc * chore: remove commitlint job that reviews all commits in PR from CI This was causing a *lot* of CI failures, including for merge commits With this commit, CI will still check the _latest_ commit for meeting conventions, it just won't run over all commits in a PR This may lead to a few more "unconventional" commits making it through, but the CI should still flag when someone is just not using conventional commits at all, which I think was the original goal. * feat: add some coverage for `updateOperators(ForQuorums)` fncs * feat: add testing for `updateOperatorsForQuorum` function * feat: add some coverage for complex view functions note: this commit also adds a TODO around a currently-failing test. I plan to discuss the correct path forwards here and then push another commit. * chore: clarify NatSpec comments * fix: make `getQuorumBitmapIndicesAtBlockNumber` revert if operator was registered the logic is now more in-line with the logic in the StakeRegistry -- for reference, see: https://github.com/layr-labs/eigenlayer-middleware/blob/ 98f884454d9e9de1e344bb6fba9a2cd3915e5b57/src/StakeRegistry.sol#L297-L299 * feat: add simple unit test for `getQuorumBitmapIndicesAtBlockNumber` also improve wording in the 'tree' file * chore: move `_getQuorumBitmapIndexAtBlockNumber` into the section with other internal fncs * chore: remove unnecessary require statement + improve code clarity * fix: correct a compiler error for implicit type conversion * feat: address TODOs in tests --------- Co-authored-by: Alex <18387287+wadealexc@users.noreply.github.com> * Test: bitmap utils unit tests (#101) * test: refactor and add tests to bitmap unit * test: added tests and using asserts * chore: remove single quote * feat: addNumberToBitmap function * chore: remove unused bitmap functions * feat: addNumberToBitmap function * test: tree file and Alexs bitmap fix * test: add back updated stakeRegistry tests * test: config tests setMinimumStake tests addStrategies tests initializeQuorum tests * refactor: using MockAVSDeployer for test file * test: more config tests * refactor: stake registry harness weighting * fix: stake weighting refactor removed overriding weightOfOperator functions in StakeRegistry and fixed broken tests using the helper _setOperatorWeight() * feat: addNumberToBitmap function * test: add back updated stakeRegistry tests * test: config tests setMinimumStake tests addStrategies tests initializeQuorum tests * refactor: using MockAVSDeployer for test file * test: more config tests * refactor: stake registry harness weighting * fix: stake weighting refactor removed overriding weightOfOperator functions in StakeRegistry and fixed broken tests using the helper _setOperatorWeight() * chore: rebase fixes * fix: undo change * test: voteweighing tests * test: fix unit tests * fix: rebase errors * fix: broken tests from rebase --------- Co-authored-by: Samuel Laferriere Co-authored-by: Alex <18387287+wadealexc@users.noreply.github.com> Co-authored-by: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> --- test/events/IStakeRegistryEvents.sol | 23 + test/harnesses/StakeRegistryHarness.sol | 53 +- test/integration/CoreRegistration.t.sol | 2 +- test/tree/StakeRegistryUnit.tree | 114 ++ test/unit/OperatorStateRetrieverUnit.t.sol | 7 +- test/unit/RegistryCoordinatorUnit.t.sol | 65 +- test/unit/StakeRegistryUnit.t.sol | 2085 ++++++++++++++++---- test/utils/MockAVSDeployer.sol | 23 +- 8 files changed, 1867 insertions(+), 505 deletions(-) create mode 100644 test/events/IStakeRegistryEvents.sol create mode 100644 test/tree/StakeRegistryUnit.tree diff --git a/test/events/IStakeRegistryEvents.sol b/test/events/IStakeRegistryEvents.sol new file mode 100644 index 00000000..6b048306 --- /dev/null +++ b/test/events/IStakeRegistryEvents.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.12; + +import {IStakeRegistry, IStrategy} from "src/interfaces/IStakeRegistry.sol"; + +interface IStakeRegistryEvents { + /// @notice emitted whenever the stake of `operator` is updated + event OperatorStakeUpdate( + bytes32 indexed operatorId, + uint8 quorumNumber, + uint96 stake + ); + /// @notice emitted when the minimum stake for a quorum is updated + event MinimumStakeForQuorumUpdated(uint8 indexed quorumNumber, uint96 minimumStake); + /// @notice emitted when a new quorum is created + event QuorumCreated(uint8 indexed quorumNumber); + /// @notice emitted when `strategy` has been added to the array at `strategyParams[quorumNumber]` + event StrategyAddedToQuorum(uint8 indexed quorumNumber, IStrategy strategy); + /// @notice emitted when `strategy` has removed from the array at `strategyParams[quorumNumber]` + event StrategyRemovedFromQuorum(uint8 indexed quorumNumber, IStrategy strategy); + /// @notice emitted when `strategy` has its `multiplier` updated in the array at `strategyParams[quorumNumber]` + event StrategyMultiplierUpdated(uint8 indexed quorumNumber, IStrategy strategy, uint256 multiplier); +} \ No newline at end of file diff --git a/test/harnesses/StakeRegistryHarness.sol b/test/harnesses/StakeRegistryHarness.sol index b24b54ff..b6c57321 100644 --- a/test/harnesses/StakeRegistryHarness.sol +++ b/test/harnesses/StakeRegistryHarness.sol @@ -5,8 +5,6 @@ import "../../src/StakeRegistry.sol"; // wrapper around the StakeRegistry contract that exposes the internal functions for unit testing. contract StakeRegistryHarness is StakeRegistry { - mapping(uint8 => mapping(address => uint96)) private __weightOfOperatorForQuorum; - constructor( IRegistryCoordinator _registryCoordinator, IDelegationManager _delegationManager @@ -21,54 +19,11 @@ contract StakeRegistryHarness is StakeRegistry { _recordTotalStakeUpdate(quorumNumber, stakeDelta); } - // mocked function so we can set this arbitrarily without having to mock other elements - function weightOfOperatorForQuorum(uint8 quorumNumber, address operator) public override view returns(uint96) { - return __weightOfOperatorForQuorum[quorumNumber][operator]; - } - - function _weightOfOperatorForQuorum(uint8 quorumNumber, address operator) internal override view returns(uint96, bool) { - uint96 weight = __weightOfOperatorForQuorum[quorumNumber][operator]; - return ( - weight, - weight >= minimumStakeForQuorum[quorumNumber] - ); - } - - // mocked function so we can set this arbitrarily without having to mock other elements - function setOperatorWeight(uint8 quorumNumber, address operator, uint96 weight) external { - __weightOfOperatorForQuorum[quorumNumber][operator] = weight; + function calculateDelta(uint96 prev, uint96 cur) external pure returns (int256) { + return _calculateDelta(prev, cur); } - // mocked function to register an operator without having to mock other elements - // This is just a copy/paste from `registerOperator`, since that no longer uses an internal method - function registerOperatorNonCoordinator(address operator, bytes32 operatorId, bytes calldata quorumNumbers) external returns (uint96[] memory, uint96[] memory) { - uint96[] memory currentStakes = new uint96[](quorumNumbers.length); - uint96[] memory totalStakes = new uint96[](quorumNumbers.length); - for (uint256 i = 0; i < quorumNumbers.length; i++) { - - uint8 quorumNumber = uint8(quorumNumbers[i]); - require(_quorumExists(quorumNumber), "StakeRegistry.registerOperator: quorum does not exist"); - - // Retrieve the operator's current weighted stake for the quorum, reverting if they have not met - // the minimum. - (uint96 currentStake, bool hasMinimumStake) = _weightOfOperatorForQuorum(quorumNumber, operator); - require( - hasMinimumStake, - "StakeRegistry.registerOperator: Operator does not meet minimum stake requirement for quorum" - ); - - // Update the operator's stake - int256 stakeDelta = _recordOperatorStakeUpdate({ - operatorId: operatorId, - quorumNumber: quorumNumber, - newStake: currentStake - }); - - // Update this quorum's total stake by applying the operator's delta - currentStakes[i] = currentStake; - totalStakes[i] = _recordTotalStakeUpdate(quorumNumber, stakeDelta); - } - - return (currentStakes, totalStakes); + function applyDelta(uint96 value, int256 delta) external pure returns (uint96) { + return _applyDelta(value, delta); } } diff --git a/test/integration/CoreRegistration.t.sol b/test/integration/CoreRegistration.t.sol index 66a9e31b..ff0a0bba 100644 --- a/test/integration/CoreRegistration.t.sol +++ b/test/integration/CoreRegistration.t.sol @@ -83,7 +83,7 @@ contract Test_CoreRegistration is MockAVSDeployer { // Set operator weight in single quorum bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(MAX_QUORUM_BITMAP); for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), operator, defaultStake); + _setOperatorWeight(operator, uint8(quorumNumbers[i]), defaultStake); } } diff --git a/test/tree/StakeRegistryUnit.tree b/test/tree/StakeRegistryUnit.tree new file mode 100644 index 00000000..fd790fb7 --- /dev/null +++ b/test/tree/StakeRegistryUnit.tree @@ -0,0 +1,114 @@ +. +├── StakeRegistry tree (*** denotes that integration tests are needed to validate path) +├── when any function is called (invariants) +│ └── when parameters contain uninitialized quorumNumbers +│ └── it should revert +├── when registerOperator is called +│ ├── given caller is not the registry coordinator +│ │ └── it should revert +│ ├── given quorum does not exist +│ │ └── it should revert +│ ├── given the operator does not meet the minimum stake for a quorum +│ │ └── it should revert +│ └── given that the above conditions are satisfied +│ ├── given the operator's stake is unchanged *** +│ │ └── it should not update the operator's stake +│ ├── given the operator does not have a stake update from the current block +│ │ └── it should push a new stake update for the operator +│ ├── given the operator does have a stake update for the current block *** +│ │ └── it should update the operator's last stake update with the new stake +│ ├── given the total stake history was not updated in the current block +│ │ └── it should push a new stake update for the total stake +│ └── given the total stake history was updated in the current block +│ └── it should update the last total stake update with the new stake +├── when deregisterOperator is called +│ ├── given caller is not the registry coordinator +│ │ └── it should revert +│ ├── given quorum does not exist +│ │ └── it should revert +│ └── given that the above conditions are satisfied +│ ├── given the operator's stake history is empty (although shouldn't be possible to register with empty stake history) +│ │ └── it should push a single entry with 0 stake +│ ├── given the operator's current stake is 0 and their history is nonempty +│ │ └── it should not update the operator's stake history or total history +│ ├── given the operator's current stake is nonzero and does not have a stake update for current block +│ │ └── it should push a new stake update for the operator with 0 stake +│ ├── given the operator's current stake is nonzero and was last updated in current block +│ │ └── it should update the operator's last stake update with 0 stake +│ ├── given the total stake history was not updated in the current block +│ │ └── it should push a new stake update for the total stake +│ └── given the total stake history was updated in the current block +│ └── it should update the last total stake update with the new stake +├── when updateOperatorStake is called +│ ├── given caller is not the registry coordinator +│ │ └── it should revert +│ ├── given quorum does not exist +│ │ └── it should revert +│ ├── given the operator currently meets the minimum stake for the quorum +│ │ ├── given they still do after updating +│ │ │ └── it should update their history and apply the change to the total history +│ │ └── given they no longer meet the minimum +│ │ └── it should set their stake to zero, remove it from the total stake, and add the quorum to the return bitmap +│ └── given the operator does not currently meet the minimum stake for the quorum +│ ├── given their updated stake does not meet the minimum +│ │ └── it should not perform any updates, and it should add the quorum to the return bitmap +│ └── given they meet the minimum after the update +│ └── it should update the operator and total history with the stake +├── when initializeQuorum is called +│ ├── given quorum already exists +│ │ └── it should revert +│ ├── given strategyParams is empty +│ │ └── it should revert +│ ├── given strategyParams length is > MAX_WEIGHING_FUNCTION_LENGTH +│ │ └── it should revert +│ └── given quorum doesn't already exist +│ └── it should call _addStrategyParams and _setMinimumStakeForQuorum (internal functions below) +│ └── given internal functions succeed +│ └── it should set the first _totalStakeHistory entry for the initialized quorumNumber +├── when setMinimumStakeForQuorum is called +│ ├── given caller is not the registry coordinator owner +│ │ └── it should revert +│ ├── given quorum does not exist +│ │ └── it should revert +│ └── it should set the minimum stake and emit MinimumStakeForQuorumUpdated +├── when addStrategies is called +│ ├── given caller is not the registry coordinator owner +│ │ └── it should revert +│ ├── given quorum does not exist +│ │ └── it should revert +│ └── it should call _addStrategyParams (internal function) +├── when _addStrategyParams is called +│ ├── given strategyParams is empty +│ │ └── it should revert +│ ├── given strategyParams length is > MAX_WEIGHING_FUNCTION_LENGTH +│ │ └── it should revert +│ ├── given a strategy being added already exists in quorum +│ │ └── it should revert +│ ├── given a multiplier being added is 0 +│ │ └── it should revert +│ └── given unique strategies and non-zero multipliers +│ └── it should add the strategies and multipliers to the quorum +├── when removeStrategies is called +│ ├── given caller is not the registry coordinator owner +│ │ └── it should revert +│ ├── given quorum does not exist +│ │ └── it should revert +│ ├── given indicesToRemove length is 0 +│ │ └── it should revert +│ ├── given an index in indicesToRemove is >= length of strategies in quorum +│ │ └── it should revert +│ ├── given valid indicesToRemove for an existing quorum but not in decreasing order +│ │ └── it should revert +│ └── given valid indicesToRemove for an existing quorum and in decreasing order +│ └── it should remove the desired strategies from the quorum +└── when modifyStrategyParams is called + ├── given caller is not the registry coordinator owner + │ └── it should revert + ├── given quorum does not exist + │ └── it should revert + ├── given strategyIndices length is 0 or has mismatch length with newMultipliers + │ └── it should revert + ├── given a index in strategyIndices is >= length of strategies in quorum + │ └── it should revert + └── given matching lengths and valid indices for an existing quorum + └── it should modify the weights of existing strategies in the quorum \ No newline at end of file diff --git a/test/unit/OperatorStateRetrieverUnit.t.sol b/test/unit/OperatorStateRetrieverUnit.t.sol index b332e2d3..eb85cc3f 100644 --- a/test/unit/OperatorStateRetrieverUnit.t.sol +++ b/test/unit/OperatorStateRetrieverUnit.t.sol @@ -178,7 +178,12 @@ contract OperatorStateRetrieverUnitTests is MockAVSDeployer { for (uint k = 0; k < operators[j].length; k++) { uint8 quorumNumber = uint8(quorumNumbers[j]); assertEq(operators[j][k].operatorId, operatorMetadatas[expectedOperatorOverallIndices[quorumNumber][k]].operatorId); - assertEq(operators[j][k].stake, operatorMetadatas[expectedOperatorOverallIndices[quorumNumber][k]].stakes[quorumNumber]); + // using assertApprox to account for rounding errors + assertApproxEqAbs( + operators[j][k].stake, + operatorMetadatas[expectedOperatorOverallIndices[quorumNumber][k]].stakes[quorumNumber], + 1 + ); } } } diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index bb0439dc..d46d337a 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -294,14 +294,14 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + uint96 actualStake = _setOperatorWeight(defaultOperator, defaultQuorumNumber, defaultStake); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); emit OperatorAddedToQuorums(defaultOperator, quorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, defaultStake); + emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, actualStake); cheats.expectEmit(true, true, true, true, address(indexRegistry)); emit QuorumIndexUpdate(defaultOperatorId, defaultQuorumNumber, 0); @@ -340,8 +340,9 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.assume(quorumBitmap != 0); bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); + uint96 actualStake; for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + actualStake = _setOperatorWeight(defaultOperator, uint8(quorumNumbers[i]), defaultStake); } cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -352,7 +353,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni for (uint i = 0; i < quorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[i]), defaultStake); + emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[i]), actualStake); } for (uint i = 0; i < quorumNumbers.length; i++) { @@ -395,7 +396,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.prank(defaultOperator); cheats.roll(registrationBlockNumber); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -403,13 +404,13 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni bytes memory newQuorumNumbers = new bytes(1); newQuorumNumbers[0] = bytes1(defaultQuorumNumber+1); - stakeRegistry.setOperatorWeight(uint8(newQuorumNumbers[0]), defaultOperator, defaultStake); + uint96 actualStake = _setOperatorWeight(defaultOperator, uint8(newQuorumNumbers[0]), defaultStake); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); emit OperatorAddedToQuorums(defaultOperator, newQuorumNumbers); cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, uint8(newQuorumNumbers[0]), defaultStake); + emit OperatorStakeUpdate(defaultOperatorId, uint8(newQuorumNumbers[0]), actualStake); cheats.expectEmit(true, true, true, true, address(indexRegistry)); emit QuorumIndexUpdate(defaultOperatorId, uint8(newQuorumNumbers[0]), 0); cheats.roll(nextRegistrationBlockNumber); @@ -469,7 +470,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni blsApkRegistry.setBLSPublicKey(operatorToRegister, operatorToRegisterPubKey); - stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, defaultStake); + _setOperatorWeight(operatorToRegister, defaultQuorumNumber, defaultStake); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator.registerOperator: operator count exceeds maximum"); @@ -484,7 +485,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.prank(defaultOperator); cheats.roll(registrationBlockNumber); @@ -521,7 +522,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); cheats.expectRevert("RegistryCoordinator._registerOperator: operator already registered for some quorums being registered for"); @@ -533,7 +534,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + defaultStake = _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); @@ -620,7 +621,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.startPrank(defaultOperator); @@ -673,7 +674,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[i]), defaultStake); } cheats.startPrank(defaultOperator); @@ -733,7 +734,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory registrationquorumNumbers = BitmapUtils.bitmapToBytesArray(registrationQuorumBitmap); for (uint i = 0; i < registrationquorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(registrationquorumNumbers[i]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(registrationquorumNumbers[i]), defaultStake); } cheats.startPrank(defaultOperator); @@ -937,7 +938,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.roll(registrationBlockNumber); cheats.startPrank(defaultOperator); @@ -964,7 +965,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.roll(registrationBlockNumber); cheats.startPrank(defaultOperator); @@ -998,7 +999,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes memory registrationquorumNumbers = BitmapUtils.bitmapToBytesArray(registrationQuorumBitmap); for (uint i = 0; i < registrationquorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(registrationquorumNumbers[i]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(registrationquorumNumbers[i]), defaultStake); } cheats.roll(registrationBlockNumber); @@ -1067,7 +1068,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.prank(defaultOperator); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -1102,7 +1103,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[i]), defaultStake); } cheats.prank(defaultOperator); @@ -1142,7 +1143,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.prank(defaultOperator); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -1166,7 +1167,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist uint32 registrationBlockNumber = 100; bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.roll(registrationBlockNumber); cheats.startPrank(defaultOperator); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -1311,7 +1312,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord blsApkRegistry.setBLSPublicKey(operatorToRegister, operatorToRegisterPubKey); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; - stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); + _setOperatorWeight(operatorToRegister, defaultQuorumNumber, registeringStake); cheats.roll(registrationBlockNumber); cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); @@ -1382,7 +1383,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); - stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, defaultStake); + _setOperatorWeight(operatorToRegister, defaultQuorumNumber, defaultStake); cheats.roll(registrationBlockNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = @@ -1414,7 +1415,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord // set the stake of the operator to register to the defaultKickBIPsOfOperatorStake multiple of the operatorToKickStake - stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, operatorToKickStake * defaultKickBIPsOfOperatorStake / 10000 + 1); + _setOperatorWeight(operatorToRegister, defaultQuorumNumber, operatorToKickStake * defaultKickBIPsOfOperatorStake / 10000 + 1); cheats.roll(registrationBlockNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = @@ -1443,7 +1444,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; - stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); + _setOperatorWeight(operatorToRegister, defaultQuorumNumber, registeringStake); cheats.roll(registrationBlockNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry; @@ -1476,7 +1477,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; - stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); + _setOperatorWeight(operatorToRegister, defaultQuorumNumber, registeringStake); cheats.roll(registrationBlockNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry = @@ -1513,7 +1514,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit uint32 registrationBlockNumber = 100; bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.startPrank(defaultOperator); cheats.roll(registrationBlockNumber); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -1537,7 +1538,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit uint32 registrationBlockNumber = 100; bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(registrationBitmap); for (uint256 i = 0; i < quorumNumbers.length; ++i) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[i]), defaultStake); } cheats.startPrank(defaultOperator); cheats.roll(registrationBlockNumber); @@ -1553,7 +1554,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit uint192 quorumBitmapToRemove = mockReturnData; bytes memory quorumNumbersToRemove = BitmapUtils.bitmapToBytesArray(quorumBitmapToRemove); for (uint256 i = 0; i < quorumNumbersToRemove.length; ++i) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbersToRemove[i]), defaultOperator, 0); + _setOperatorWeight(defaultOperator, uint8(quorumNumbersToRemove[i]), 0); } uint256 expectedQuorumBitmap = BitmapUtils.minus(quorumBitmapBefore, quorumBitmapToRemove); @@ -1625,7 +1626,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit uint32 registrationBlockNumber = 100; bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.startPrank(defaultOperator); cheats.roll(registrationBlockNumber); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -1700,7 +1701,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit uint32 registrationBlockNumber = 100; bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + _setOperatorWeight(defaultOperator, uint8(quorumNumbers[0]), defaultStake); cheats.startPrank(defaultOperator); cheats.roll(registrationBlockNumber); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); @@ -1810,4 +1811,4 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit }))) ); } -} \ No newline at end of file +} diff --git a/test/unit/StakeRegistryUnit.t.sol b/test/unit/StakeRegistryUnit.t.sol index 0560efed..88811dc6 100644 --- a/test/unit/StakeRegistryUnit.t.sol +++ b/test/unit/StakeRegistryUnit.t.sol @@ -3,107 +3,51 @@ pragma solidity =0.8.12; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "test/utils/MockAVSDeployer.sol"; -import {Slasher} from "eigenlayer-contracts/src/contracts/core/Slasher.sol"; -import {PauserRegistry} from "eigenlayer-contracts/src/contracts/permissions/PauserRegistry.sol"; -import {ISlasher} from "eigenlayer-contracts/src/contracts/interfaces/ISlasher.sol"; -import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol"; -import {IStakeRegistry} from "../../src/interfaces/IStakeRegistry.sol"; -import {IIndexRegistry} from "../../src/interfaces/IIndexRegistry.sol"; -import {IRegistryCoordinator} from "../../src/interfaces/IRegistryCoordinator.sol"; -import {IBLSApkRegistry} from "../../src/interfaces/IBLSApkRegistry.sol"; -import {IServiceManager} from "../../src/interfaces/IServiceManager.sol"; +import {StakeRegistry} from "src/StakeRegistry.sol"; +import {IStakeRegistry} from "src/interfaces/IStakeRegistry.sol"; +import {IStakeRegistryEvents} from "test/events/IStakeRegistryEvents.sol"; -import {BitmapUtils} from "../../src/libraries/BitmapUtils.sol"; +import "../utils/MockAVSDeployer.sol"; -import {StrategyManagerMock} from "eigenlayer-contracts/src/test/mocks/StrategyManagerMock.sol"; -import {EigenPodManagerMock} from "eigenlayer-contracts/src/test/mocks/EigenPodManagerMock.sol"; -import {DelegationManagerMock} from "eigenlayer-contracts/src/test/mocks/DelegationManagerMock.sol"; +contract StakeRegistryUnitTests is MockAVSDeployer, IStakeRegistryEvents { + using BitmapUtils for *; -import {StakeRegistryHarness} from "../harnesses/StakeRegistryHarness.sol"; -import {StakeRegistry} from "../../src/StakeRegistry.sol"; -import {RegistryCoordinatorHarness} from "../harnesses/RegistryCoordinatorHarness.t.sol"; + /// @notice Maximum length of dynamic arrays in the `strategiesConsideredAndMultipliers` mapping. + uint8 public constant MAX_WEIGHING_FUNCTION_LENGTH = 32; -import "forge-std/Test.sol"; + /** + * Tracker variables used as we initialize quorums and operators during tests + * (see _initializeQuorum and _selectNewOperator) + */ + uint8 nextQuorum = 0; + address nextOperator = address(1000); + bytes32 nextOperatorId = bytes32(uint256(1000)); -contract StakeRegistryUnitTests is Test { - Vm cheats = Vm(HEVM_ADDRESS); - - ProxyAdmin public proxyAdmin; - PauserRegistry public pauserRegistry; - - ISlasher public slasher = ISlasher(address(uint160(uint256(keccak256("slasher"))))); - - Slasher public slasherImplementation; - StakeRegistryHarness public stakeRegistryImplementation; - StakeRegistryHarness public stakeRegistry; - RegistryCoordinatorHarness public registryCoordinator; - IServiceManager public serviceManager; - - StrategyManagerMock public strategyManagerMock; - DelegationManagerMock public delegationMock; - EigenPodManagerMock public eigenPodManagerMock; - - address public registryCoordinatorOwner = address(uint160(uint256(keccak256("registryCoordinatorOwner")))); - address public pauser = address(uint160(uint256(keccak256("pauser")))); - address public unpauser = address(uint160(uint256(keccak256("unpauser")))); - address public apkRegistry = address(uint160(uint256(keccak256("apkRegistry")))); - address public indexRegistry = address(uint160(uint256(keccak256("indexRegistry")))); - - uint256 churnApproverPrivateKey = uint256(keccak256("churnApproverPrivateKey")); - address churnApprover = cheats.addr(churnApproverPrivateKey); - address ejector = address(uint160(uint256(keccak256("ejector")))); - - address defaultOperator = address(uint160(uint256(keccak256("defaultOperator")))); - bytes32 defaultOperatorId = keccak256("defaultOperatorId"); - uint8 defaultQuorumNumber = 0; - uint8 numQuorums = 192; - uint8 maxQuorumsToRegisterFor = 4; - - // Track initialized quorums so we can filter these out when fuzzing - mapping(uint8 => bool) initializedQuorums; + /** + * Fuzz input filters: + */ + uint192 initializedQuorumBitmap; + bytes initializedQuorumBytes; uint256 gasUsed; - /// @notice emitted whenever the stake of `operator` is updated - event OperatorStakeUpdate( - bytes32 indexed operatorId, - uint8 quorumNumber, - uint96 stake - ); + modifier fuzzOnlyInitializedQuorums(uint8 quorumNumber) { + cheats.assume(initializedQuorumBitmap.isSet(quorumNumber)); + _; + } function setUp() virtual public { - proxyAdmin = new ProxyAdmin(); - - address[] memory pausers = new address[](1); - pausers[0] = pauser; - pauserRegistry = new PauserRegistry(pausers, unpauser); - - delegationMock = new DelegationManagerMock(); - eigenPodManagerMock = new EigenPodManagerMock(); - strategyManagerMock = new StrategyManagerMock(); - slasherImplementation = new Slasher(strategyManagerMock, delegationMock); - slasher = Slasher( - address( - new TransparentUpgradeableProxy( - address(slasherImplementation), - address(proxyAdmin), - abi.encodeWithSelector(Slasher.initialize.selector, msg.sender, pauserRegistry, 0/*initialPausedStatus*/) - ) - ) - ); - - strategyManagerMock.setAddresses( - delegationMock, - eigenPodManagerMock, - slasher - ); + // Deploy contracts but with 0 quorums initialized, will initializeQuorums afterwards + _deployMockEigenLayerAndAVS(0); + // Make registryCoordinatorOwner the owner of the registryCoordinator contract cheats.startPrank(registryCoordinatorOwner); registryCoordinator = new RegistryCoordinatorHarness( serviceManager, stakeRegistry, - IBLSApkRegistry(apkRegistry), + IBLSApkRegistry(blsApkRegistry), IIndexRegistry(indexRegistry) ); @@ -123,444 +67,1747 @@ contract StakeRegistryUnitTests is Test { ); cheats.stopPrank(); - // Initialize quorums with dummy minimum stake and strategies - for (uint i = 0; i < maxQuorumsToRegisterFor; i++) { - uint96 minimumStake = uint96(i + 1); - IStakeRegistry.StrategyParams[] memory strategyParams = + // Initialize several quorums with varying minimum stakes + _initializeQuorum({ minimumStake: uint96(type(uint16).max) }); + _initializeQuorum({ minimumStake: uint96(type(uint24).max) }); + _initializeQuorum({ minimumStake: uint96(type(uint32).max) }); + _initializeQuorum({ minimumStake: uint96(type(uint64).max) }); + + _initializeQuorum({ minimumStake: uint96(type(uint16).max) + 1 }); + _initializeQuorum({ minimumStake: uint96(type(uint24).max) + 1 }); + _initializeQuorum({ minimumStake: uint96(type(uint32).max) + 1 }); + _initializeQuorum({ minimumStake: uint96(type(uint64).max) + 1 }); + } + + /******************************************************************************* + initializers + *******************************************************************************/ + + /** + * @dev Initialize a new quorum with `minimumStake` + * The new quorum's number is sequential, starting with `nextQuorum` + */ + function _initializeQuorum(uint96 minimumStake) internal { + uint8 quorumNumber = nextQuorum; + + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](1); - strategyParams[0] = IStakeRegistry.StrategyParams( - IStrategy(address(uint160(i))), - uint96(i+1) - ); + strategyParams[0] = IStakeRegistry.StrategyParams( + IStrategy(address(uint160(uint256(keccak256(abi.encodePacked(quorumNumber)))))), + uint96(WEIGHTING_DIVISOR) + ); + + nextQuorum++; + + cheats.prank(address(registryCoordinator)); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + + // Mark quorum initialized for other tests + initializedQuorumBitmap = uint192(initializedQuorumBitmap.setBit(quorumNumber)); + initializedQuorumBytes = initializedQuorumBitmap.bitmapToBytesArray(); + } - _initializeQuorum(uint8(defaultQuorumNumber + i), minimumStake, strategyParams); + /** + * @dev Initialize a new quorum with `minimumStake` and `numStrats` + * Create `numStrats` dummy strategies with multiplier of 1 for each. + * Returns quorumNumber that was just initialized + */ + function _initializeQuorum(uint96 minimumStake, uint256 numStrats) internal returns (uint8) { + uint8 quorumNumber = nextQuorum; + + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](numStrats); + for (uint256 i = 0; i < strategyParams.length; i++) { + strategyParams[i] = IStakeRegistry.StrategyParams( + IStrategy(address(uint160(uint256(keccak256(abi.encodePacked(quorumNumber, i)))))), + uint96(WEIGHTING_DIVISOR) + ); } - // Update the reg coord quorum count so updateStakes works - registryCoordinator.setQuorumCount(maxQuorumsToRegisterFor); + nextQuorum++; + + cheats.prank(address(registryCoordinator)); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + + // Mark quorum initialized for other tests + initializedQuorumBitmap = uint192(initializedQuorumBitmap.setBit(quorumNumber)); + initializedQuorumBytes = initializedQuorumBitmap.bitmapToBytesArray(); + + return quorumNumber; } - function testSetMinimumStakeForQuorum_NotFromCoordinatorOwner_Reverts() public { - cheats.expectRevert("StakeRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); - stakeRegistry.setMinimumStakeForQuorum(defaultQuorumNumber, 0); + /// @dev Return a new, unique operator/operatorId pair, guaranteed to be + /// unregistered from all quorums + function _selectNewOperator() internal returns (address, bytes32) { + address operator = nextOperator; + bytes32 operatorId = nextOperatorId; + nextOperator = _incrementAddress(nextOperator, 1); + nextOperatorId = _incrementBytes32(nextOperatorId, 1); + return (operator, operatorId); + } + + /******************************************************************************* + test setup methods + *******************************************************************************/ + + struct RegisterSetup { + address operator; + bytes32 operatorId; + bytes quorumNumbers; + uint96[] operatorWeights; + uint96[] minimumStakes; + IStakeRegistry.StakeUpdate[] prevOperatorStakes; + IStakeRegistry.StakeUpdate[] prevTotalStakes; } - function testSetMinimumStakeForQuorum_Valid(uint8 quorumNumber, uint96 minimumStakeForQuorum) public { - // filter out non-initialized quorums - cheats.assume(initializedQuorums[quorumNumber]); + /// @dev Utility function set up a new operator to be registered for some quorums + /// The operator's weight is set to the quorum's minimum, plus fuzzy_addtlStake (overflows are skipped) + /// This function guarantees at least one quorum, and any quorums returned are initialized + function _fuzz_setupRegisterOperator(uint192 fuzzy_Bitmap, uint16 fuzzy_addtlStake) internal returns (RegisterSetup memory) { + // Select an unused operator to register + (address operator, bytes32 operatorId) = _selectNewOperator(); - // set the minimum stake for quorum - cheats.prank(registryCoordinatorOwner); - stakeRegistry.setMinimumStakeForQuorum(quorumNumber, minimumStakeForQuorum); + // Pick quorums to register for and get each quorum's minimum stake + ( , bytes memory quorumNumbers) = _fuzz_getQuorums(fuzzy_Bitmap); + uint96[] memory minimumStakes = _getMinimumStakes(quorumNumbers); - // make sure the minimum stake for quorum is as expected - assertEq(stakeRegistry.minimumStakeForQuorum(quorumNumber), minimumStakeForQuorum); - } + // For each quorum, set the operator's weight as the minimum + addtlStake + uint96[] memory operatorWeights = new uint96[](quorumNumbers.length); + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); - function testRegisterOperator_NotFromRegistryCoordinator_Reverts() public { - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - cheats.expectRevert("StakeRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator"); - stakeRegistry.registerOperator(defaultOperator, defaultOperatorId, quorumNumbers); - } + unchecked { operatorWeights[i] = minimumStakes[i] + fuzzy_addtlStake; } + cheats.assume(operatorWeights[i] >= minimumStakes[i]); + cheats.assume(operatorWeights[i] >= fuzzy_addtlStake); - function testRegisterOperator_LessThanMinimumStakeForQuorum_Reverts( - uint96[] memory stakesForQuorum - ) public { - cheats.assume(stakesForQuorum.length > 0); + _setOperatorWeight(operator, quorumNumber, operatorWeights[i]); + } - // set the weights of the operator - // stakeRegistry.setOperatorWeight() + /// Get starting state + IStakeRegistry.StakeUpdate[] memory prevOperatorStakes = _getLatestStakeUpdates(operatorId, quorumNumbers); + IStakeRegistry.StakeUpdate[] memory prevTotalStakes = _getLatestTotalStakeUpdates(quorumNumbers); - bytes memory quorumNumbers = new bytes(stakesForQuorum.length > maxQuorumsToRegisterFor ? maxQuorumsToRegisterFor : stakesForQuorum.length); + // Ensure that the operator has not previously registered for (uint i = 0; i < quorumNumbers.length; i++) { - quorumNumbers[i] = bytes1(uint8(i)); + assertTrue(prevOperatorStakes[i].updateBlockNumber == 0, "operator already registered"); + assertTrue(prevOperatorStakes[i].stake == 0, "operator already has stake"); } - stakesForQuorum[stakesForQuorum.length - 1] = stakeRegistry.minimumStakeForQuorum(uint8(quorumNumbers.length - 1)) - 1; + return RegisterSetup({ + operator: operator, + operatorId: operatorId, + quorumNumbers: quorumNumbers, + operatorWeights: operatorWeights, + minimumStakes: minimumStakes, + prevOperatorStakes: prevOperatorStakes, + prevTotalStakes: prevTotalStakes + }); + } - // expect that it reverts when you register - cheats.expectRevert("StakeRegistry.registerOperator: Operator does not meet minimum stake requirement for quorum"); + function _fuzz_setupRegisterOperators(uint192 fuzzy_Bitmap, uint16 fuzzy_addtlStake, uint numOperators) internal returns (RegisterSetup[] memory) { + RegisterSetup[] memory setups = new RegisterSetup[](numOperators); + + for (uint i = 0; i < numOperators; i++) { + setups[i] = _fuzz_setupRegisterOperator(fuzzy_Bitmap, fuzzy_addtlStake); + } + + return setups; + } + + struct DeregisterSetup { + address operator; + bytes32 operatorId; + // registerOperator quorums and state after registration: + bytes registeredQuorumNumbers; + IStakeRegistry.StakeUpdate[] prevOperatorStakes; + IStakeRegistry.StakeUpdate[] prevTotalStakes; + // deregisterOperator info: + bytes quorumsToRemove; + uint192 quorumsToRemoveBitmap; + } + + /// @dev Utility function set up a new operator to be deregistered from some quorums + /// The operator's weight is set to the quorum's minimum, plus fuzzy_addtlStake (overflows are skipped) + /// This function guarantees at least one quorum, and any quorums returned are initialized + function _fuzz_setupDeregisterOperator( + uint192 registeredFor, + uint192 fuzzy_toRemove, + uint16 fuzzy_addtlStake + ) internal returns (DeregisterSetup memory) { + RegisterSetup memory registerSetup = _fuzz_setupRegisterOperator(registeredFor, fuzzy_addtlStake); + + // registerOperator cheats.prank(address(registryCoordinator)); - stakeRegistry.registerOperator(defaultOperator, defaultOperatorId, quorumNumbers); + stakeRegistry.registerOperator(registerSetup.operator, registerSetup.operatorId, registerSetup.quorumNumbers); + + // Get state after registering: + IStakeRegistry.StakeUpdate[] memory operatorStakes = _getLatestStakeUpdates(registerSetup.operatorId, registerSetup.quorumNumbers); + IStakeRegistry.StakeUpdate[] memory totalStakes = _getLatestTotalStakeUpdates(registerSetup.quorumNumbers); + + (uint192 quorumsToRemoveBitmap, bytes memory quorumsToRemove) = _fuzz_getQuorums(fuzzy_toRemove); + + return DeregisterSetup({ + operator: registerSetup.operator, + operatorId: registerSetup.operatorId, + registeredQuorumNumbers: registerSetup.quorumNumbers, + prevOperatorStakes: operatorStakes, + prevTotalStakes: totalStakes, + quorumsToRemove: quorumsToRemove, + quorumsToRemoveBitmap: quorumsToRemoveBitmap + }); } - function testRegisterFirstOperator_Valid( - uint256 quorumBitmap, - uint80[] memory stakesForQuorum - ) public { - // limit to maxQuorumsToRegisterFor quorums and register for quorum 0 - quorumBitmap = quorumBitmap & (1 << maxQuorumsToRegisterFor - 1) | 1; - uint96[] memory paddedStakesForQuorum = _registerOperatorValid(defaultOperator, defaultOperatorId, quorumBitmap, stakesForQuorum); - - uint8 quorumNumberIndex = 0; - for (uint8 i = 0; i < maxQuorumsToRegisterFor; i++) { - if (quorumBitmap >> i & 1 == 1) { - // check that the operator has 1 stake update in the quorum numbers they registered for - assertEq(stakeRegistry.getStakeHistoryLength(defaultOperatorId, i), 1); - // make sure that the stake update is as expected - IStakeRegistry.StakeUpdate memory stakeUpdate = - stakeRegistry.getStakeUpdateAtIndex(i, defaultOperatorId, 0); - emit log_named_uint("length of paddedStakesForQuorum", paddedStakesForQuorum.length); - assertEq(stakeUpdate.stake, paddedStakesForQuorum[quorumNumberIndex]); - assertEq(stakeUpdate.updateBlockNumber, uint32(block.number)); - assertEq(stakeUpdate.nextUpdateBlockNumber, 0); - - // make the analogous check for total stake history - assertEq(stakeRegistry.getTotalStakeHistoryLength(i), 1); - // make sure that the stake update is as expected - stakeUpdate = stakeRegistry.getTotalStakeUpdateAtIndex(i, 0); - assertEq(stakeUpdate.stake, paddedStakesForQuorum[quorumNumberIndex]); - assertEq(stakeUpdate.updateBlockNumber, uint32(block.number)); - assertEq(stakeUpdate.nextUpdateBlockNumber, 0); - - quorumNumberIndex++; + function _fuzz_setupDeregisterOperators( + uint192 registeredFor, + uint192 fuzzy_toRemove, + uint16 fuzzy_addtlStake, + uint numOperators + ) internal returns (DeregisterSetup[] memory) { + DeregisterSetup[] memory setups = new DeregisterSetup[](numOperators); + + for (uint i = 0; i < numOperators; i++) { + setups[i] = _fuzz_setupDeregisterOperator(registeredFor, fuzzy_toRemove, fuzzy_addtlStake); + } + + return setups; + } + + struct UpdateSetup { + address operator; + bytes32 operatorId; + bytes quorumNumbers; + uint96[] minimumStakes; + uint96[] endingWeights; + // absolute value of stake delta + uint96 stakeDeltaAbs; + } + + /// @dev Utility function to register a new, unique operator for `registeredFor` quorums, giving + /// the operator exactly the minimum weight required for the quorum. + /// After registering, and before returning, `fuzzy_Delta` is applied to the operator's weight + /// to place the operator's weight above or below the minimum stake. (or unchanged!) + /// The next time `updateOperatorStake` is called, this new weight will be used. + function _fuzz_setupUpdateOperatorStake(uint192 registeredFor, int8 fuzzy_Delta) internal returns (UpdateSetup memory) { + RegisterSetup memory registerSetup = _fuzz_setupRegisterOperator(registeredFor, 0); + + // registerOperator + cheats.prank(address(registryCoordinator)); + stakeRegistry.registerOperator(registerSetup.operator, registerSetup.operatorId, registerSetup.quorumNumbers); + + uint96[] memory minimumStakes = _getMinimumStakes(registerSetup.quorumNumbers); + uint96[] memory endingWeights = new uint96[](minimumStakes.length); + + for (uint i = 0; i < minimumStakes.length; i++) { + uint8 quorumNumber = uint8(registerSetup.quorumNumbers[i]); + + endingWeights[i] = _applyDelta(minimumStakes[i], int256(fuzzy_Delta)); + + // Sanity-check setup: + if (fuzzy_Delta > 0) { + assertGt(endingWeights[i], minimumStakes[i], "_fuzz_setupUpdateOperatorStake: overflow during setup"); + } else if (fuzzy_Delta < 0) { + assertLt(endingWeights[i], minimumStakes[i], "_fuzz_setupUpdateOperatorStake: underflow during setup"); } else { - // check that the operator has 0 stake updates in the quorum numbers they did not register for - assertEq(stakeRegistry.getStakeHistoryLength(defaultOperatorId, i), 0); - // make the analogous check for total stake history - assertEq(stakeRegistry.getTotalStakeHistoryLength(i), 1); + assertEq(endingWeights[i], minimumStakes[i], "_fuzz_setupUpdateOperatorStake: invalid delta during setup"); } + // Set operator weights. The next time we call `updateOperatorStake`, these new weights will be used + _setOperatorWeight(registerSetup.operator, quorumNumber, endingWeights[i]); } + + uint96 stakeDeltaAbs = + fuzzy_Delta < 0 ? + uint96(-int96(fuzzy_Delta)) : + uint96(int96(fuzzy_Delta)); + + return UpdateSetup({ + operator: registerSetup.operator, + operatorId: registerSetup.operatorId, + quorumNumbers: registerSetup.quorumNumbers, + minimumStakes: minimumStakes, + endingWeights: endingWeights, + stakeDeltaAbs: stakeDeltaAbs + }); } - function testRegisterManyOperators_Valid( - uint256 pseudoRandomNumber, - uint8 numOperators, - uint24[] memory blocksPassed - ) public { - cheats.assume(numOperators > 0 && numOperators <= 15); - // modulo so no overflow - pseudoRandomNumber = pseudoRandomNumber % type(uint128).max; + function _fuzz_setupUpdateOperatorStakes(uint8 numOperators, uint192 registeredFor, int8 fuzzy_Delta) internal returns (UpdateSetup[] memory) { + UpdateSetup[] memory setups = new UpdateSetup[](numOperators); - uint256[] memory quorumBitmaps = new uint256[](numOperators); + for (uint i = 0; i < numOperators; i++) { + setups[i] = _fuzz_setupUpdateOperatorStake(registeredFor, fuzzy_Delta); + } + + return setups; + } + + /******************************************************************************* + helpful getters + *******************************************************************************/ + + /// @notice Given a fuzzed bitmap input, returns a bitmap and array of quorum numbers + /// that are guaranteed to be initialized. + function _fuzz_getQuorums(uint192 fuzzy_Bitmap) internal view returns (uint192, bytes memory) { + fuzzy_Bitmap &= initializedQuorumBitmap; + cheats.assume(!fuzzy_Bitmap.isEmpty()); + + return (fuzzy_Bitmap, fuzzy_Bitmap.bitmapToBytesArray()); + } + + /// @notice Returns a list of initialized quorums ending in a non-initialized quorum + /// @param rand is used to determine how many legitimate quorums to insert, so we can + /// check this works for lists of varying lengths + function _fuzz_getInvalidQuorums(bytes32 rand) internal returns (bytes memory) { + uint length = _randUint({ rand: rand, min: 1, max: initializedQuorumBytes.length + 1 }); + bytes memory invalidQuorums = new bytes(length); + + // Create an invalid quorum number by incrementing the last initialized quorum + uint8 invalidQuorum = 1 + uint8(initializedQuorumBytes[initializedQuorumBytes.length - 1]); + + // Select real quorums up to the length, then insert an invalid quorum + for (uint8 quorum = 0; quorum < length - 1; quorum++) { + // sanity check test setup + assertTrue(initializedQuorumBitmap.isSet(quorum), "_fuzz_getInvalidQuorums: invalid quorum"); + invalidQuorums[quorum] = bytes1(quorum); + } + + invalidQuorums[length - 1] = bytes1(invalidQuorum); + return invalidQuorums; + } + + /// @notice Returns true iff two StakeUpdates are identical + function _isUnchanged( + IStakeRegistry.StakeUpdate memory prev, + IStakeRegistry.StakeUpdate memory cur + ) internal pure returns (bool) { + return ( + prev.stake == cur.stake && + prev.updateBlockNumber == cur.updateBlockNumber && + prev.nextUpdateBlockNumber == cur.nextUpdateBlockNumber + ); + } - // append to blocksPassed as needed - uint24[] memory appendedBlocksPassed = new uint24[](quorumBitmaps.length); - for (uint256 i = blocksPassed.length; i < quorumBitmaps.length; i++) { - appendedBlocksPassed[i] = 0; + /// @dev Return the minimum stakes required for a list of quorums + function _getMinimumStakes(bytes memory quorumNumbers) internal view returns (uint96[] memory) { + uint96[] memory minimumStakes = new uint96[](quorumNumbers.length); + + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + minimumStakes[i] = stakeRegistry.minimumStakeForQuorum(quorumNumber); } - blocksPassed = appendedBlocksPassed; + + return minimumStakes; + } + + /// @dev Return the most recent stake update history entries for an operator + function _getLatestStakeUpdates( + bytes32 operatorId, + bytes memory quorumNumbers + ) internal view returns (IStakeRegistry.StakeUpdate[] memory) { + IStakeRegistry.StakeUpdate[] memory stakeUpdates = + new IStakeRegistry.StakeUpdate[](quorumNumbers.length); - uint32 initialBlockNumber = 100; - cheats.roll(initialBlockNumber); - uint32 cumulativeBlockNumber = initialBlockNumber; + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + stakeUpdates[i] = stakeRegistry.getLatestStakeUpdate(operatorId, quorumNumber); + } + + return stakeUpdates; + } + + /// @dev Return the most recent total stake update history entries + function _getLatestTotalStakeUpdates( + bytes memory quorumNumbers + ) internal view returns (IStakeRegistry.StakeUpdate[] memory) { + IStakeRegistry.StakeUpdate[] memory stakeUpdates = + new IStakeRegistry.StakeUpdate[](quorumNumbers.length); + + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); - uint96[][] memory paddedStakesForQuorums = new uint96[][](quorumBitmaps.length); - for (uint256 i = 0; i < quorumBitmaps.length; i++) { - (quorumBitmaps[i], paddedStakesForQuorums[i]) = _registerOperatorRandomValid(_incrementAddress(defaultOperator, i), _incrementBytes32(defaultOperatorId, i), pseudoRandomNumber + i); + uint historyLength = stakeRegistry.getTotalStakeHistoryLength(quorumNumber); + stakeUpdates[i] = stakeRegistry.getTotalStakeUpdateAtIndex(quorumNumber, historyLength - 1); + } + + return stakeUpdates; + } - cumulativeBlockNumber += blocksPassed[i]; - cheats.roll(cumulativeBlockNumber); + /// @dev Return the lengths of the operator stake update history for each quorum + function _getStakeHistoryLengths( + bytes32 operatorId, + bytes memory quorumNumbers + ) internal view returns (uint256[] memory) { + uint256[] memory operatorStakeHistoryLengths = new uint256[](quorumNumbers.length); + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + + operatorStakeHistoryLengths[i] = stakeRegistry.getStakeHistoryLength(operatorId, quorumNumber); } + + return operatorStakeHistoryLengths; + } + + /// @dev Return the lengths of the total stake update history + function _getTotalStakeHistoryLengths( + bytes memory quorumNumbers + ) internal view returns (uint256[] memory) { + uint256[] memory historyLengths = new uint256[](quorumNumbers.length); + + for (uint256 i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + + historyLengths[i] = stakeRegistry.getTotalStakeHistoryLength(quorumNumber); + } + + return historyLengths; + } + + function _calculateDelta(uint96 prev, uint96 cur) internal view returns (int256) { + return stakeRegistry.calculateDelta({ + prev: prev, + cur: cur + }); + } + + function _applyDelta(uint96 value, int256 delta) internal view returns (uint96) { + return stakeRegistry.applyDelta({ + value: value, + delta: delta + }); + } + + /// @dev Uses `rand` to return a random uint, with a range given by `min` and `max` (inclusive) + /// @return `min` <= result <= `max` + function _randUint(bytes32 rand, uint min, uint max) internal pure returns (uint) { + // hashing makes for more uniform randomness + rand = keccak256(abi.encodePacked(rand)); - // for each bit in each quorumBitmap, increment the number of operators in that quorum - uint32[] memory numOperatorsInQuorum = new uint32[](maxQuorumsToRegisterFor); - for (uint256 i = 0; i < quorumBitmaps.length; i++) { - for (uint256 j = 0; j < maxQuorumsToRegisterFor; j++) { - if (quorumBitmaps[i] >> j & 1 == 1) { - numOperatorsInQuorum[j]++; + uint range = max - min + 1; + + // calculate the number of bits needed for the range + uint bitsNeeded = 0; + uint tempRange = range; + while (tempRange > 0) { + bitsNeeded++; + tempRange >>= 1; + } + + // create a mask for the required number of bits + // and extract the value from the hash + uint mask = (1 << bitsNeeded) - 1; + uint value = uint(rand) & mask; + + // in case value is out of range, wrap around or retry + while (value >= range) { + value = (value - range) & mask; + } + + return min + value; + } + + /// @dev Sort to ensure that the array is in desscending order for removeStrategies + function _sortArrayDesc(uint256[] memory arr) internal pure returns (uint256[] memory) { + uint256 l = arr.length; + for(uint256 i = 0; i < l; i++) { + for(uint256 j = i+1; j < l ;j++) { + if(arr[i] < arr[j]) { + uint256 temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; } } } + return arr; + } +} - // operatorQuorumIndices is an array of iindices within the quorum numbers that each operator registered for - // used for accounting in the next loops - uint32[] memory operatorQuorumIndices = new uint32[](quorumBitmaps.length); +/// @notice Tests for any nonstandard/permissioned methods +contract StakeRegistryUnitTests_Config is StakeRegistryUnitTests { - // for each quorum - for (uint8 i = 0; i < maxQuorumsToRegisterFor; i++) { - uint32 operatorCount = 0; - // reset the cumulative block number - cumulativeBlockNumber = initialBlockNumber; + /******************************************************************************* + initializeQuorum + *******************************************************************************/ - uint96 cumulativeStake = 0; - // for each operator - for (uint256 j = 0; j < quorumBitmaps.length; j++) { - // if the operator is in the quorum - if (quorumBitmaps[j] >> i & 1 == 1) { - cumulativeStake += paddedStakesForQuorums[j][operatorQuorumIndices[j]]; + function testFuzz_initializeQuorum_Revert_WhenNotRegistryCoordinator( + uint8 quorumNumber, + uint96 minimumStake, + IStakeRegistry.StrategyParams[] memory strategyParams + ) public { + cheats.expectRevert("StakeRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator"); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + } - operatorQuorumIndices[j]++; - operatorCount++; - } - cumulativeBlockNumber += blocksPassed[j]; + function testFuzz_initializeQuorum_Revert_WhenQuorumAlreadyExists( + uint8 quorumNumber, + uint96 minimumStake, + IStakeRegistry.StrategyParams[] memory strategyParams + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.expectRevert("StakeRegistry.initializeQuorum: quorum already exists"); + cheats.prank(address(registryCoordinator)); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + } + + function testFuzz_initializeQuorum_Revert_WhenInvalidArrayLengths( + uint8 quorumNumber, + uint96 minimumStake + ) public { + cheats.assume(quorumNumber >= nextQuorum); + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](0); + cheats.expectRevert("StakeRegistry._addStrategyParams: no strategies provided"); + cheats.prank(address(registryCoordinator)); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + + strategyParams = new IStakeRegistry.StrategyParams[](MAX_WEIGHING_FUNCTION_LENGTH + 1); + for (uint256 i = 0; i < strategyParams.length; i++) { + strategyParams[i] = IStakeRegistry.StrategyParams( + IStrategy(address(uint160(uint256(keccak256(abi.encodePacked(i)))))), + uint96(1) + ); + } + cheats.expectRevert("StakeRegistry._addStrategyParams: exceed MAX_WEIGHING_FUNCTION_LENGTH"); + cheats.prank(address(registryCoordinator)); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + } + + /** + * @dev Initializes a quorum with StrategyParams with fuzzed multipliers inputs and corresponding + * strategy addresses. + */ + function testFuzz_initializeQuorum( + uint8 quorumNumber, + uint96 minimumStake, + uint96[] memory multipliers + ) public { + cheats.assume(quorumNumber >= nextQuorum); + cheats.assume(0 < multipliers.length && multipliers.length <= MAX_WEIGHING_FUNCTION_LENGTH); + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](multipliers.length); + for (uint256 i = 0; i < strategyParams.length; i++) { + cheats.assume(multipliers[i] > 0); + strategyParams[i] = IStakeRegistry.StrategyParams( + IStrategy(address(uint160(uint256(keccak256(abi.encodePacked(i)))))), + multipliers[i] + ); + } + quorumNumber = nextQuorum; + cheats.prank(address(registryCoordinator)); + stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); + + IStakeRegistry.StakeUpdate memory initialStakeUpdate = stakeRegistry.getTotalStakeUpdateAtIndex(quorumNumber, 0); + assertEq(stakeRegistry.minimumStakeForQuorum(quorumNumber), minimumStake, "invalid minimum stake"); + assertEq(stakeRegistry.getTotalStakeHistoryLength(quorumNumber), 1, "invalid total stake history length"); + assertEq(initialStakeUpdate.stake, 0, "invalid stake update"); + assertEq(initialStakeUpdate.updateBlockNumber, uint32(block.number), "invalid updateBlockNumber stake update"); + assertEq(initialStakeUpdate.nextUpdateBlockNumber, 0, "invalid nextUpdateBlockNumber stake update"); + assertEq(stakeRegistry.strategyParamsLength(quorumNumber), strategyParams.length, "invalid strategy params length"); + for (uint256 i = 0; i < strategyParams.length; i++) { + (IStrategy strategy , uint96 multiplier) = stakeRegistry.strategyParams(quorumNumber, i); + assertEq(address(strategy), address(strategyParams[i].strategy), "invalid strategy"); + assertEq(multiplier, strategyParams[i].multiplier, "invalid multiplier"); + } + } + + /******************************************************************************* + setMinimumStakeForQuorum + *******************************************************************************/ + + function testFuzz_setMinimumStakeForQuorum_Revert_WhenNotRegistryCoordinatorOwner( + uint8 quorumNumber, + uint96 minimumStakeForQuorum + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.expectRevert("StakeRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); + stakeRegistry.setMinimumStakeForQuorum(quorumNumber, minimumStakeForQuorum); + } + + function testFuzz_setMinimumStakeForQuorum_Revert_WhenInvalidQuorum( + uint8 quorumNumber, + uint96 minimumStakeForQuorum + ) public { + // quorums [0,nextQuorum) are initialized, so use an invalid quorumNumber + cheats.assume(quorumNumber >= nextQuorum); + cheats.expectRevert("StakeRegistry.quorumExists: quorum does not exist"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.setMinimumStakeForQuorum(quorumNumber, minimumStakeForQuorum); + } + + /// @dev Fuzzes initialized quorum numbers and minimum stakes to set to + function testFuzz_setMinimumStakeForQuorum( + uint8 quorumNumber, + uint96 minimumStakeForQuorum + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.prank(registryCoordinatorOwner); + stakeRegistry.setMinimumStakeForQuorum(quorumNumber, minimumStakeForQuorum); + assertEq(stakeRegistry.minimumStakeForQuorum(quorumNumber), minimumStakeForQuorum, "invalid minimum stake"); + } + + /******************************************************************************* + addStrategies + *******************************************************************************/ + + function testFuzz_addStrategies_Revert_WhenNotRegistryCoordinatorOwner( + uint8 quorumNumber, + IStakeRegistry.StrategyParams[] memory strategyParams + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.expectRevert("StakeRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); + stakeRegistry.addStrategies(quorumNumber, strategyParams); + } + + function testFuzz_addStrategies_Revert_WhenInvalidQuorum( + uint8 quorumNumber, + IStakeRegistry.StrategyParams[] memory strategyParams + ) public { + // quorums [0,nextQuorum) are initialized, so use an invalid quorumNumber + cheats.assume(quorumNumber >= nextQuorum); + cheats.expectRevert("StakeRegistry.quorumExists: quorum does not exist"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.addStrategies(quorumNumber, strategyParams); + } + + function test_addStrategies_Revert_WhenDuplicateStrategies() public { + uint8 quorumNumber = _initializeQuorum(uint96(type(uint16).max), 1); + + IStrategy strat = IStrategy(address(uint160(uint256(keccak256(abi.encodePacked("duplicate strat")))))); + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](2); + strategyParams[0] = IStakeRegistry.StrategyParams( + strat, + uint96(WEIGHTING_DIVISOR) + ); + strategyParams[1] = IStakeRegistry.StrategyParams( + strat, + uint96(WEIGHTING_DIVISOR) + ); + + cheats.expectRevert("StakeRegistry._addStrategyParams: cannot add same strategy 2x"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.addStrategies(quorumNumber, strategyParams); + } + + function test_addStrategies_Revert_WhenZeroWeight() public { + uint8 quorumNumber = _initializeQuorum(uint96(type(uint16).max), 1); + + IStrategy strat = IStrategy(address(uint160(uint256(keccak256(abi.encodePacked("duplicate strat")))))); + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](2); + strategyParams[0] = IStakeRegistry.StrategyParams( + strat, + 0 + ); + + cheats.expectRevert("StakeRegistry._addStrategyParams: cannot add strategy with zero weight"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.addStrategies(quorumNumber, strategyParams); + } + + /** + * @dev Fuzzes initialized quorum numbers and using multipliers to create StrategyParams to add to + * quorumNumber. + */ + function testFuzz_addStrategies( + uint8 quorumNumber, + uint96[] memory multipliers + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + uint256 currNumStrategies = stakeRegistry.strategyParamsLength(quorumNumber); + // Assume nonzero multipliers, and total added strategies length is less than MAX_WEIGHING_FUNCTION_LENGTH + cheats.assume(0 < multipliers.length && multipliers.length <= MAX_WEIGHING_FUNCTION_LENGTH - currNumStrategies); + for (uint256 i = 0; i < multipliers.length; i++) { + cheats.assume(multipliers[i] > 0); + } + // Expected events emitted + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](multipliers.length); + for (uint256 i = 0; i < strategyParams.length; i++) { + IStrategy strat = IStrategy(address(uint160(uint256(keccak256(abi.encodePacked(i)))))); + strategyParams[i] = IStakeRegistry.StrategyParams( + strat, + multipliers[i] + ); + + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit StrategyAddedToQuorum(quorumNumber, strat); + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit StrategyMultiplierUpdated(quorumNumber, strat, multipliers[i]); + } + + // addStrategies() call and expected assertions + cheats.prank(registryCoordinatorOwner); + stakeRegistry.addStrategies(quorumNumber, strategyParams); + assertEq(stakeRegistry.strategyParamsLength(quorumNumber), strategyParams.length + 1, "invalid strategy params length"); + for (uint256 i = 0; i < strategyParams.length; i++) { + (IStrategy strategy , uint96 multiplier) = stakeRegistry.strategyParams(quorumNumber, i + 1); + assertEq(address(strategy), address(strategyParams[i].strategy), "invalid strategy"); + assertEq(multiplier, strategyParams[i].multiplier, "invalid multiplier"); + } + } + + /******************************************************************************* + removeStrategies + *******************************************************************************/ + function testFuzz_removeStrategies_Revert_WhenNotRegistryCoordinatorOwner( + uint8 quorumNumber, + uint256[] memory indicesToRemove + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.expectRevert("StakeRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); + stakeRegistry.removeStrategies(quorumNumber, indicesToRemove); + } + + function testFuzz_removeStrategies_Revert_WhenInvalidQuorum( + uint8 quorumNumber, + uint256[] memory indicesToRemove + ) public { + // quorums [0,nextQuorum) are initialized, so use an invalid quorumNumber + cheats.assume(quorumNumber >= nextQuorum); + cheats.expectRevert("StakeRegistry.quorumExists: quorum does not exist"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.removeStrategies(quorumNumber, indicesToRemove); + } + + function testFuzz_removeStrategies_Revert_WhenIndexOutOfBounds( + uint96 minimumStake, + uint8 numStrategiesToAdd, + uint8 indexToRemove + ) public { + cheats.assume(0 < numStrategiesToAdd && numStrategiesToAdd <= MAX_WEIGHING_FUNCTION_LENGTH); + cheats.assume(numStrategiesToAdd <= indexToRemove); + uint8 quorumNumber = _initializeQuorum(minimumStake, numStrategiesToAdd); + + uint256[] memory indicesToRemove = new uint256[](1); + indicesToRemove[0] = indexToRemove; + // index will be >= length of strategy params so should revert from index out of bounds + cheats.expectRevert(); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.removeStrategies(quorumNumber, indicesToRemove); + } + + function testFuzz_removeStrategies_Revert_WhenEmptyStrategiesToRemove( + uint96 minimumStake, + uint8 numStrategiesToAdd + ) public { + cheats.assume(0 < numStrategiesToAdd && numStrategiesToAdd <= MAX_WEIGHING_FUNCTION_LENGTH); + uint8 quorumNumber = _initializeQuorum(minimumStake, numStrategiesToAdd); + + uint256[] memory indicesToRemove = new uint256[](0); + cheats.expectRevert("StakeRegistry.removeStrategies: no indices to remove provided"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.removeStrategies(quorumNumber, indicesToRemove); + } + + /** + * @dev Fuzzes `numStrategiesToAdd` strategies to a quorum and then removes `numStrategiesToRemove` strategies + * Ensures the indices for `numStrategiesToRemove` are random within bounds, and are sorted desc. + */ + function testFuzz_removeStrategies( + uint96 minimumStake, + uint8 numStrategiesToAdd, + uint8 numStrategiesToRemove + ) public { + cheats.assume(0 < numStrategiesToAdd && numStrategiesToAdd <= MAX_WEIGHING_FUNCTION_LENGTH); + cheats.assume(0 < numStrategiesToRemove && numStrategiesToRemove <= numStrategiesToAdd); + uint8 quorumNumber = _initializeQuorum(minimumStake, numStrategiesToAdd); + + // Create array of indicesToRemove, sort desc, and assume no duplicates + uint256[] memory indicesToRemove = new uint256[](numStrategiesToRemove); + for (uint256 i = 0; i < numStrategiesToRemove; i++) { + indicesToRemove[i] = _randUint({ rand: bytes32(i), min: 0, max: numStrategiesToAdd - 1 }); + } + indicesToRemove = _sortArrayDesc(indicesToRemove); + uint256 prevIndex = indicesToRemove[0]; + for (uint256 i = 0; i < indicesToRemove.length; i++) { + if (i > 0) { + cheats.assume(indicesToRemove[i] < prevIndex); + prevIndex = indicesToRemove[i]; } + } + + // Expected events emitted + for (uint256 i = 0; i < indicesToRemove.length; i++) { + (IStrategy strategy, ) = stakeRegistry.strategyParams(quorumNumber, indicesToRemove[i]); + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit StrategyRemovedFromQuorum(quorumNumber, strategy); + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit StrategyMultiplierUpdated(quorumNumber, strategy, 0); + } + + // Remove strategies and do assertions + cheats.prank(registryCoordinatorOwner); + stakeRegistry.removeStrategies(quorumNumber, indicesToRemove); + assertEq( + stakeRegistry.strategyParamsLength(quorumNumber), + numStrategiesToAdd - indicesToRemove.length, + "invalid strategy params length" + ); + } + + /******************************************************************************* + modifyStrategyParams + *******************************************************************************/ + function testFuzz_modifyStrategyParams_Revert_WhenNotRegistryCoordinatorOwner( + uint8 quorumNumber, + uint256[] calldata strategyIndices, + uint96[] calldata newMultipliers + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.expectRevert("StakeRegistry.onlyCoordinatorOwner: caller is not the owner of the registryCoordinator"); + stakeRegistry.modifyStrategyParams(quorumNumber, strategyIndices, newMultipliers); + } - uint historyLength = stakeRegistry.getTotalStakeHistoryLength(i); + function testFuzz_modifyStrategyParams_Revert_WhenInvalidQuorum( + uint8 quorumNumber, + uint256[] calldata strategyIndices, + uint96[] calldata newMultipliers + ) public { + // quorums [0,nextQuorum) are initialized, so use an invalid quorumNumber + cheats.assume(quorumNumber >= nextQuorum); + cheats.expectRevert("StakeRegistry.quorumExists: quorum does not exist"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.modifyStrategyParams(quorumNumber, strategyIndices, newMultipliers); + } + + function testFuzz_modifyStrategyParams_Revert_WhenEmptyArray( + uint8 quorumNumber + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + uint256[] memory strategyIndices = new uint256[](0); + uint96[] memory newMultipliers = new uint96[](0); + cheats.expectRevert("StakeRegistry.modifyStrategyParams: no strategy indices provided"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.modifyStrategyParams(quorumNumber, strategyIndices, newMultipliers); + } + + function testFuzz_modifyStrategyParams_Revert_WhenInvalidArrayLengths( + uint8 quorumNumber, + uint256[] calldata strategyIndices, + uint96[] calldata newMultipliers + ) public fuzzOnlyInitializedQuorums(quorumNumber) { + cheats.assume(strategyIndices.length != newMultipliers.length); + cheats.assume(strategyIndices.length > 0); + cheats.expectRevert("StakeRegistry.modifyStrategyParams: input length mismatch"); + cheats.prank(registryCoordinatorOwner); + stakeRegistry.modifyStrategyParams(quorumNumber, strategyIndices, newMultipliers); + } - // If we don't have stake history, it should be because there is no stake - if (historyLength == 0) { - assertEq(cumulativeStake, 0); - continue; + /** + * @dev Fuzzes initialized quorum with random indices of strategies to modify with new multipliers. + * Checks for events emitted and new multipliers are updated + */ + function testFuzz_modifyStrategyParams( + uint8 numStrategiesToAdd, + uint8 numStrategiesToModify + ) public { + cheats.assume(0 < numStrategiesToAdd && numStrategiesToAdd <= MAX_WEIGHING_FUNCTION_LENGTH); + cheats.assume(0 < numStrategiesToModify && numStrategiesToModify <= numStrategiesToAdd); + uint256 prevIndex; + uint256[] memory strategyIndices = new uint256[](numStrategiesToModify); + uint96[] memory newMultipliers = new uint96[](numStrategiesToModify); + // create array of indices to modify, assume no duplicates, and create array of multipliers for each index + for (uint256 i = 0; i < numStrategiesToModify; i++) { + strategyIndices[i] = _randUint({ rand: bytes32(i), min: 0, max: numStrategiesToAdd - 1 }); + newMultipliers[i] = uint96(_randUint({ rand: bytes32(i), min: 1, max: type(uint96).max })); + // ensure no duplicate indices + if (i == 0) { + prevIndex = strategyIndices[0]; + } else if (i > 0) { + cheats.assume(strategyIndices[i] < prevIndex); + prevIndex = strategyIndices[i]; } + } + + // Expected events emitted + uint8 quorumNumber = _initializeQuorum(0 /* minimumStake */, numStrategiesToAdd); + for (uint256 i = 0; i < strategyIndices.length; i++) { + (IStrategy strategy, ) = stakeRegistry.strategyParams(quorumNumber, strategyIndices[i]); + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit StrategyMultiplierUpdated(quorumNumber, strategy, newMultipliers[i]); + } + + // modifyStrategyParams() call and expected assertions + cheats.prank(registryCoordinatorOwner); + stakeRegistry.modifyStrategyParams(quorumNumber, strategyIndices, newMultipliers); + for (uint256 i = 0; i < strategyIndices.length; i++) { + (, uint96 multiplier) = stakeRegistry.strategyParams(quorumNumber, strategyIndices[i]); + assertEq(multiplier, newMultipliers[i], "invalid multiplier"); + } + } +} + +/// @notice Tests for StakeRegistry.registerOperator +contract StakeRegistryUnitTests_Register is StakeRegistryUnitTests { + + /******************************************************************************* + registerOperator + *******************************************************************************/ + + function test_registerOperator_Revert_WhenNotRegistryCoordinator() public { + (address operator, bytes32 operatorId) = _selectNewOperator(); - // make sure that the stake update is as expected - IStakeRegistry.StakeUpdate memory totalStakeUpdate = - stakeRegistry.getTotalStakeUpdateAtIndex(i, historyLength-1); + cheats.expectRevert("StakeRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator"); + stakeRegistry.registerOperator(operator, operatorId, initializedQuorumBytes); + } + + function testFuzz_Revert_WhenQuorumDoesNotExist(bytes32 rand) public { + RegisterSetup memory setup = _fuzz_setupRegisterOperator(initializedQuorumBitmap, 0); + + // Get a list of valid quorums ending in an invalid quorum number + bytes memory invalidQuorums = _fuzz_getInvalidQuorums(rand); + + cheats.expectRevert("StakeRegistry.registerOperator: quorum does not exist"); + cheats.prank(address(registryCoordinator)); + stakeRegistry.registerOperator(setup.operator, setup.operatorId, invalidQuorums); + } - assertEq(totalStakeUpdate.stake, cumulativeStake); - // make sure that the next update block number of the previous stake update is as expected - if (historyLength >= 2) { - IStakeRegistry.StakeUpdate memory prevTotalStakeUpdate = - stakeRegistry.getTotalStakeUpdateAtIndex(i, historyLength-2); - assertEq(prevTotalStakeUpdate.nextUpdateBlockNumber, cumulativeBlockNumber); + /// @dev Attempt to register for all quorums, selecting one quorum to attempt with + /// insufficient stake + function testFuzz_registerOperator_Revert_WhenInsufficientStake( + uint8 failingQuorum + ) public fuzzOnlyInitializedQuorums(failingQuorum) { + (address operator, bytes32 operatorId) = _selectNewOperator(); + bytes memory quorumNumbers = initializedQuorumBytes; + uint96[] memory minimumStakes = _getMinimumStakes(quorumNumbers); + + // Set the operator's weight to the minimum stake for each quorum + // ... except the failing quorum, which gets minimum stake - 1 + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + uint96 operatorWeight; + + if (quorumNumber == failingQuorum) { + unchecked { operatorWeight = minimumStakes[i] - 1; } + assertTrue(operatorWeight < minimumStakes[i], "minimum stake underflow"); + } else { + operatorWeight = minimumStakes[i]; } + + _setOperatorWeight(operator, quorumNumber, operatorWeight); } + + // Attempt to register + cheats.expectRevert("StakeRegistry.registerOperator: Operator does not meet minimum stake requirement for quorum"); + cheats.prank(address(registryCoordinator)); + stakeRegistry.registerOperator(operator, operatorId, quorumNumbers); } - function testDeregisterOperator_Valid( - uint256 pseudoRandomNumber, - uint256 quorumBitmap, - uint256 deregistrationQuorumsFlag, - uint80[] memory stakesForQuorum + /** + * @dev Registers an operator for some initialized quorums, adding `additionalStake` + * to the minimum stake for each quorum. + * + * Checks the end result of stake updates rather than the entire history + */ + function testFuzz_registerOperator_SingleOperator_SingleBlock( + uint192 quorumBitmap, + uint16 additionalStake ) public { - // modulo so no overflow - pseudoRandomNumber = pseudoRandomNumber % type(uint128).max; - // limit to maxQuorumsToRegisterFor quorums and register for quorum 0 - quorumBitmap = quorumBitmap & (1 << maxQuorumsToRegisterFor - 1) | 1; - // register a bunch of operators - cheats.roll(100); - uint32 cumulativeBlockNumber = 100; - - uint8 numOperatorsRegisterBefore = 5; - uint256 numOperators = 1 + 2*numOperatorsRegisterBefore; - uint256[] memory quorumBitmaps = new uint256[](numOperators); - - // register - for (uint i = 0; i < numOperatorsRegisterBefore; i++) { - (quorumBitmaps[i],) = _registerOperatorRandomValid(_incrementAddress(defaultOperator, i), _incrementBytes32(defaultOperatorId, i), pseudoRandomNumber + i); + /// Setup - select a new operator and set their weight to each quorum's minimum plus some additional + RegisterSetup memory setup = _fuzz_setupRegisterOperator(quorumBitmap, additionalStake); + + /// registerOperator + cheats.prank(address(registryCoordinator)); + (uint96[] memory resultingStakes, uint96[] memory totalStakes) = + stakeRegistry.registerOperator(setup.operator, setup.operatorId, setup.quorumNumbers); + + /// Read ending state + IStakeRegistry.StakeUpdate[] memory newOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.quorumNumbers); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(setup.quorumNumbers); + uint256[] memory operatorStakeHistoryLengths = _getStakeHistoryLengths(setup.operatorId, setup.quorumNumbers); + + /// Check results + assertTrue(resultingStakes.length == setup.quorumNumbers.length, "invalid return length for operator stakes"); + assertTrue(totalStakes.length == setup.quorumNumbers.length, "invalid return length for total stakes"); + + for (uint i = 0; i < setup.quorumNumbers.length; i++) { + IStakeRegistry.StakeUpdate memory newOperatorStake = newOperatorStakes[i]; + IStakeRegistry.StakeUpdate memory newTotalStake = newTotalStakes[i]; + + // Check return value against weights, latest state read, and minimum stake + assertEq(resultingStakes[i], setup.operatorWeights[i], "stake registry did not return correct stake"); + assertEq(resultingStakes[i], newOperatorStake.stake, "invalid latest operator stake update"); + assertTrue(resultingStakes[i] != 0, "registered operator with zero stake"); + assertTrue(resultingStakes[i] >= setup.minimumStakes[i], "stake registry did not return correct stake"); - cumulativeBlockNumber += 1; - cheats.roll(cumulativeBlockNumber); + // Check stake increase from fuzzed input + assertEq(resultingStakes[i], newOperatorStake.stake, "did not add additional stake to operator correctly"); + assertEq(resultingStakes[i], newTotalStake.stake, "did not add additional stake to total correctly"); + + // Check that we had an update this block + assertEq(newOperatorStake.updateBlockNumber, uint32(block.number), ""); + assertEq(newOperatorStake.nextUpdateBlockNumber, 0, ""); + assertEq(newTotalStake.updateBlockNumber, uint32(block.number), ""); + assertEq(newTotalStake.nextUpdateBlockNumber, 0, ""); + + // Check this is the first entry in the operator stake history + assertEq(operatorStakeHistoryLengths[i], 1, "invalid total stake history length"); } + } + + // Track total stake added for each quorum as we register operators + mapping(uint8 => uint96) _totalStakeAdded; - // register the operator to be deregistered - quorumBitmaps[numOperatorsRegisterBefore] = quorumBitmap; - bytes32 operatorIdToDeregister = _incrementBytes32(defaultOperatorId, numOperatorsRegisterBefore); - uint96[] memory paddedStakesForQuorum; - { - address operatorToDeregister = _incrementAddress(defaultOperator, numOperatorsRegisterBefore); - paddedStakesForQuorum = _registerOperatorValid(operatorToDeregister, operatorIdToDeregister, quorumBitmap, stakesForQuorum); + /** + * @dev Register multiple unique operators for the same quorums during a single block, + * each with a weight of minimumStake + additionalStake. + * + * Checks the end result of stake updates rather than the entire history + */ + function testFuzz_registerOperator_MultiOperator_SingleBlock( + uint8 numOperators, + uint192 quorumBitmap, + uint16 additionalStake + ) public { + cheats.assume(numOperators > 1 && numOperators < 20); + + RegisterSetup[] memory setups = _fuzz_setupRegisterOperators(quorumBitmap, additionalStake, numOperators); + + // Register each operator one at a time, and check results: + for (uint i = 0; i < numOperators; i++) { + RegisterSetup memory setup = setups[i]; + + cheats.prank(address(registryCoordinator)); + (uint96[] memory resultingStakes, uint96[] memory totalStakes) = + stakeRegistry.registerOperator(setup.operator, setup.operatorId, setup.quorumNumbers); + + /// Read ending state + IStakeRegistry.StakeUpdate[] memory newOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.quorumNumbers); + uint256[] memory operatorStakeHistoryLengths = _getStakeHistoryLengths(setup.operatorId, setup.quorumNumbers); + + // Sum stakes in `_totalStakeAdded` to be checked later + _tallyTotalStakeAdded(setup.quorumNumbers, resultingStakes); + /// Check results + assertTrue(resultingStakes.length == setup.quorumNumbers.length, "invalid return length for operator stakes"); + assertTrue(totalStakes.length == setup.quorumNumbers.length, "invalid return length for total stakes"); + for (uint j = 0; j < setup.quorumNumbers.length; j++) { + // Check result against weights and latest state read + assertEq(resultingStakes[j], setup.operatorWeights[j], "stake registry did not return correct stake"); + assertEq(resultingStakes[j], newOperatorStakes[j].stake, "invalid latest operator stake update"); + assertTrue(resultingStakes[j] != 0, "registered operator with zero stake"); + + // Check result against minimum stake + assertTrue(resultingStakes[j] >= setup.minimumStakes[j], "stake registry did not return correct stake"); + + // Check stake increase from fuzzed input + assertEq(resultingStakes[j], newOperatorStakes[j].stake, "did not add additional stake to operator correctly"); + // Check this is the first entry in the operator stake history + assertEq(operatorStakeHistoryLengths[j], 1, "invalid total stake history length"); + } } - // register the rest of the operators - for (uint i = numOperatorsRegisterBefore + 1; i < 2*numOperatorsRegisterBefore; i++) { - cumulativeBlockNumber += 1; - cheats.roll(cumulativeBlockNumber); - (quorumBitmaps[i],) = _registerOperatorRandomValid(_incrementAddress(defaultOperator, i), _incrementBytes32(defaultOperatorId, i), pseudoRandomNumber + i); + // Check total stake results + bytes memory quorumNumbers = initializedQuorumBytes; + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(quorumNumbers); + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + assertEq(newTotalStakes[i].stake, _totalStakeAdded[quorumNumber], "incorrect latest total stake"); + assertEq(newTotalStakes[i].nextUpdateBlockNumber, 0, "incorrect total stake next update block"); + assertEq(newTotalStakes[i].updateBlockNumber, uint32(block.number), "incorrect total stake next update block"); } + } - cumulativeBlockNumber += 1; - cheats.roll(cumulativeBlockNumber); + /** + * @dev Register multiple unique operators all initialized quorums over multiple blocks, + * each with a weight equal to the minimum + additionalStake. + * + * Since these updates occur over multiple blocks, this is primarily to test + * that the total stake history is updated correctly over time. + * @param operatorsPerBlock The number of unique operators registering during a single block + * @param totalBlocks The number of times we'll register `operatorsPerBlock` (we only move 1 block each time) + */ + function testFuzz_registerOperator_MultiOperator_MultiBlock( + uint8 operatorsPerBlock, + uint8 totalBlocks, + uint16 additionalStake + ) public { + // We want between [1, 4] unique operators to register for all quorums each block, + // and we want to test this for [2, 5] blocks + cheats.assume(operatorsPerBlock >= 1 && operatorsPerBlock <= 4); + cheats.assume(totalBlocks >= 2 && totalBlocks <= 5); + + uint startBlock = block.number; + for (uint i = 1; i <= totalBlocks; i++) { + // Move to current block number + uint currBlock = startBlock + i; + cheats.roll(currBlock); + + RegisterSetup[] memory setups = + _fuzz_setupRegisterOperators(initializedQuorumBitmap, additionalStake, operatorsPerBlock); + + // Get prior total stake updates + bytes memory quorumNumbers = setups[0].quorumNumbers; + uint[] memory prevHistoryLengths = _getTotalStakeHistoryLengths(quorumNumbers); + + for (uint j = 0; j < operatorsPerBlock; j++) { + RegisterSetup memory setup = setups[j]; - // deregister the operator from a subset of the quorums - uint256 deregistrationQuroumBitmap = quorumBitmap & deregistrationQuorumsFlag; - _deregisterOperatorValid(operatorIdToDeregister, deregistrationQuroumBitmap); + cheats.prank(address(registryCoordinator)); + (uint96[] memory resultingStakes, ) = + stakeRegistry.registerOperator(setup.operator, setup.operatorId, setup.quorumNumbers); + + // Sum stakes in `_totalStakeAdded` to be checked later + _tallyTotalStakeAdded(setup.quorumNumbers, resultingStakes); + } - // for each bit in each quorumBitmap, increment the number of operators in that quorum - uint32[] memory numOperatorsInQuorum = new uint32[](maxQuorumsToRegisterFor); - for (uint256 i = 0; i < quorumBitmaps.length; i++) { - for (uint256 j = 0; j < maxQuorumsToRegisterFor; j++) { - if (quorumBitmaps[i] >> j & 1 == 1) { - numOperatorsInQuorum[j]++; - } + // Get new total stake updates + uint[] memory newHistoryLengths = _getTotalStakeHistoryLengths(quorumNumbers); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(quorumNumbers); + + for (uint j = 0; j < quorumNumbers.length; j++) { + uint8 quorumNumber = uint8(quorumNumbers[j]); + + // Check that we've added 1 to total stake history length + assertEq(prevHistoryLengths[j] + 1, newHistoryLengths[j], "total history should have a new entry"); + // Validate latest entry correctness + assertEq(newTotalStakes[j].stake, _totalStakeAdded[quorumNumber], "latest update should match total stake added"); + assertEq(newTotalStakes[j].updateBlockNumber, currBlock, "latest update should be from current block"); + assertEq(newTotalStakes[j].nextUpdateBlockNumber, 0, "latest update should not have next update block"); + + // Validate previous entry was updated correctly + IStakeRegistry.StakeUpdate memory prevUpdate = + stakeRegistry.getTotalStakeUpdateAtIndex(quorumNumber, prevHistoryLengths[j]-1); + assertTrue(prevUpdate.stake < newTotalStakes[j].stake, "previous update should have lower stake than latest"); + assertEq(prevUpdate.updateBlockNumber + 1, newTotalStakes[j].updateBlockNumber, "prev entry should be from last block"); + assertEq(prevUpdate.nextUpdateBlockNumber, newTotalStakes[j].updateBlockNumber, "prev entry.next should be latest.cur"); } } + } + + function _tallyTotalStakeAdded(bytes memory quorumNumbers, uint96[] memory stakeAdded) internal { + for (uint i = 0; i < quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(quorumNumbers[i]); + _totalStakeAdded[quorumNumber] += stakeAdded[i]; + } + } +} + +/// @notice Tests for StakeRegistry.deregisterOperator +contract StakeRegistryUnitTests_Deregister is StakeRegistryUnitTests { + + using BitmapUtils for *; + + /******************************************************************************* + deregisterOperator + *******************************************************************************/ + + function test_deregisterOperator_Revert_WhenNotRegistryCoordinator() public { + DeregisterSetup memory setup = _fuzz_setupDeregisterOperator({ + registeredFor: initializedQuorumBitmap, + fuzzy_toRemove: initializedQuorumBitmap, + fuzzy_addtlStake: 0 + }); + + cheats.expectRevert("StakeRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator"); + stakeRegistry.deregisterOperator(setup.operatorId, setup.quorumsToRemove); + } + + function testFuzz_deregisterOperator_Revert_WhenQuorumDoesNotExist(bytes32 rand) public { + // Create a new operator registered for all quorums + DeregisterSetup memory setup = _fuzz_setupDeregisterOperator({ + registeredFor: initializedQuorumBitmap, + fuzzy_toRemove: initializedQuorumBitmap, + fuzzy_addtlStake: 0 + }); + + // Get a list of valid quorums ending in an invalid quorum number + bytes memory invalidQuorums = _fuzz_getInvalidQuorums(rand); + + cheats.expectRevert("StakeRegistry.registerOperator: quorum does not exist"); + cheats.prank(address(registryCoordinator)); + stakeRegistry.registerOperator(setup.operator, setup.operatorId, invalidQuorums); + } + + /** + * @dev Registers an operator for each initialized quorum, adding `additionalStake` + * to the minimum stake for each quorum. Tests deregistering the operator for + * a subset of these quorums. + */ + function testFuzz_deregisterOperator_SingleOperator_SingleBlock( + uint192 quorumsToRemove, + uint16 additionalStake + ) public { + // Select a new operator, set their weight equal to the minimum plus some additional, + // then register them for all initialized quorums and prepare to deregister from some subset + DeregisterSetup memory setup = _fuzz_setupDeregisterOperator({ + registeredFor: initializedQuorumBitmap, + fuzzy_toRemove: quorumsToRemove, + fuzzy_addtlStake: additionalStake + }); + + // deregisterOperator + cheats.prank(address(registryCoordinator)); + stakeRegistry.deregisterOperator(setup.operatorId, setup.quorumsToRemove); + + IStakeRegistry.StakeUpdate[] memory newOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.registeredQuorumNumbers); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(setup.registeredQuorumNumbers); + + for (uint i = 0; i < setup.registeredQuorumNumbers.length; i++) { + uint8 registeredQuorum = uint8(setup.registeredQuorumNumbers[i]); + + IStakeRegistry.StakeUpdate memory prevOperatorStake = setup.prevOperatorStakes[i]; + IStakeRegistry.StakeUpdate memory prevTotalStake = setup.prevTotalStakes[i]; - uint8 quorumNumberIndex = 0; - for (uint8 i = 0; i < maxQuorumsToRegisterFor; i++) { - if (deregistrationQuroumBitmap >> i & 1 == 1) { - // check that the operator has 2 stake updates in the quorum numbers they registered for - assertEq(stakeRegistry.getStakeHistoryLength(operatorIdToDeregister, i), 2, "testDeregisterFirstOperator_Valid_0"); - // make sure that the last stake update is as expected - IStakeRegistry.StakeUpdate memory lastStakeUpdate = - stakeRegistry.getStakeUpdateAtIndex(i, operatorIdToDeregister, 1); - assertEq(lastStakeUpdate.stake, 0, "testDeregisterFirstOperator_Valid_1"); - assertEq(lastStakeUpdate.updateBlockNumber, cumulativeBlockNumber, "testDeregisterFirstOperator_Valid_2"); - assertEq(lastStakeUpdate.nextUpdateBlockNumber, 0, "testDeregisterFirstOperator_Valid_3"); - - // Get history length for quorum - uint historyLength = stakeRegistry.getTotalStakeHistoryLength(i); - // make sure that the last stake update is as expected - IStakeRegistry.StakeUpdate memory lastTotalStakeUpdate - = stakeRegistry.getTotalStakeUpdateAtIndex(i, historyLength-1); - assertEq(lastTotalStakeUpdate.stake, - stakeRegistry.getTotalStakeUpdateAtIndex(i, historyLength-2).stake // the previous total stake - - paddedStakesForQuorum[quorumNumberIndex], // minus the stake that was deregistered - "testDeregisterFirstOperator_Valid_5" - ); - assertEq(lastTotalStakeUpdate.updateBlockNumber, cumulativeBlockNumber, "testDeregisterFirstOperator_Valid_6"); - assertEq(lastTotalStakeUpdate.nextUpdateBlockNumber, 0, "testDeregisterFirstOperator_Valid_7"); - quorumNumberIndex++; - } else if (quorumBitmap >> i & 1 == 1) { - assertEq(stakeRegistry.getStakeHistoryLength(operatorIdToDeregister, i), 1, "testDeregisterFirstOperator_Valid_8"); - assertEq(stakeRegistry.getTotalStakeHistoryLength(i), numOperatorsInQuorum[i] + 1, "testDeregisterFirstOperator_Valid_9"); - quorumNumberIndex++; + IStakeRegistry.StakeUpdate memory newOperatorStake = newOperatorStakes[i]; + IStakeRegistry.StakeUpdate memory newTotalStake = newTotalStakes[i]; + + // Whether the operator was deregistered from this quorum + bool deregistered = setup.quorumsToRemoveBitmap.isSet(registeredQuorum); + + if (deregistered) { + // Check that operator's stake was removed from both operator and total + assertEq(newOperatorStake.stake, 0, "failed to remove stake"); + assertEq(newTotalStake.stake + prevOperatorStake.stake, prevTotalStake.stake, "failed to remove stake from total"); + + // Check that we had an update this block + assertEq(newOperatorStake.updateBlockNumber, uint32(block.number), "operator stake has incorrect update block"); + assertEq(newOperatorStake.nextUpdateBlockNumber, 0, "operator stake has incorrect next update block"); + assertEq(newTotalStake.updateBlockNumber, uint32(block.number), "total stake has incorrect update block"); + assertEq(newTotalStake.nextUpdateBlockNumber, 0, "total stake has incorrect next update block"); } else { - // check that the operator has 0 stake updates in the quorum numbers they did not register for - assertEq(stakeRegistry.getStakeHistoryLength(operatorIdToDeregister, i), 0, "testDeregisterFirstOperator_Valid_10"); + // Ensure no change to operator or total stakes + assertTrue(_isUnchanged(prevOperatorStake, newOperatorStake), "operator stake incorrectly updated"); + assertTrue(_isUnchanged(prevTotalStake, newTotalStake), "total stake incorrectly updated"); } } } + + // Track total stake removed from each quorum as we deregister operators + mapping(uint8 => uint96) _totalStakeRemoved; - function testUpdateOperatorStake_Valid( - uint24[] memory blocksPassed, - uint96[] memory stakes + /** + * @dev Registers multiple operators for each initialized quorum, adding `additionalStake` + * to the minimum stake for each quorum. Tests deregistering the operators for + * a subset of these quorums. + */ + function testFuzz_deregisterOperator_MultiOperator_SingleBlock( + uint8 numOperators, + uint192 quorumsToRemove, + uint16 additionalStake ) public { - cheats.assume(blocksPassed.length > 0); - cheats.assume(blocksPassed.length <= stakes.length); - // initialize at a non-zero block number - uint32 intialBlockNumber = 100; - cheats.roll(intialBlockNumber); - uint32 cumulativeBlockNumber = intialBlockNumber; - // loop through each one of the blocks passed, roll that many blocks, set the weight in the given quorum to the stake, and trigger a stake update - uint i = 0; - for (; i < blocksPassed.length; i++) { - uint96 weight = stakes[i]; - uint96 minimum = stakeRegistry.minimumStakeForQuorum(uint8(defaultQuorumNumber)); - emit log_named_uint("set weight: ", weight); - emit log_named_uint("minimum: ", minimum); - stakeRegistry.setOperatorWeight(defaultQuorumNumber, defaultOperator, stakes[i]); - - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - cheats.prank(address(registryCoordinator)); - stakeRegistry.updateOperatorStake(defaultOperator, defaultOperatorId, quorumNumbers); + cheats.assume(numOperators > 1 && numOperators < 20); - uint96 curWeight = stakeRegistry.getCurrentStake(defaultOperatorId, defaultQuorumNumber); - emit log_named_uint("new weight: ", curWeight); + // Select multiple new operators, set their weight equal to the minimum plus some additional, + // then register them for all initialized quorums and prepare to deregister from some subset + DeregisterSetup[] memory setups = _fuzz_setupDeregisterOperators({ + numOperators: numOperators, + registeredFor: initializedQuorumBitmap, + fuzzy_toRemove: quorumsToRemove, + fuzzy_addtlStake: additionalStake + }); - cumulativeBlockNumber += blocksPassed[i]; - cheats.roll(cumulativeBlockNumber); + bytes memory registeredQuorums = initializedQuorumBytes; + uint192 quorumsToRemoveBitmap = setups[0].quorumsToRemoveBitmap; + + IStakeRegistry.StakeUpdate[] memory prevTotalStakes = _getLatestTotalStakeUpdates(registeredQuorums); + + // Deregister operators one at a time and check results + for (uint i = 0; i < numOperators; i++) { + DeregisterSetup memory setup = setups[i]; + bytes32 operatorId = setup.operatorId; + + cheats.prank(address(registryCoordinator)); + stakeRegistry.deregisterOperator(setup.operatorId, setup.quorumsToRemove); + + IStakeRegistry.StakeUpdate[] memory newOperatorStakes = _getLatestStakeUpdates(operatorId, registeredQuorums); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(registeredQuorums); + + // Check results for each quorum + for (uint j = 0; j < registeredQuorums.length; j++) { + uint8 registeredQuorum = uint8(registeredQuorums[j]); + + IStakeRegistry.StakeUpdate memory prevOperatorStake = setup.prevOperatorStakes[j]; + IStakeRegistry.StakeUpdate memory prevTotalStake = prevTotalStakes[j]; + + IStakeRegistry.StakeUpdate memory newOperatorStake = newOperatorStakes[j]; + IStakeRegistry.StakeUpdate memory newTotalStake = newTotalStakes[j]; + + // Whether the operator was deregistered from this quorum + bool deregistered = setup.quorumsToRemoveBitmap.isSet(registeredQuorum); + + if (deregistered) { + _totalStakeRemoved[registeredQuorum] += prevOperatorStake.stake; + + // Check that operator's stake was removed from both operator and total + assertEq(newOperatorStake.stake, 0, "failed to remove stake"); + assertEq(newTotalStake.stake + _totalStakeRemoved[registeredQuorum], prevTotalStake.stake, "failed to remove stake from total"); + + // Check that we had an update this block + assertEq(newOperatorStake.updateBlockNumber, uint32(block.number), "operator stake has incorrect update block"); + assertEq(newOperatorStake.nextUpdateBlockNumber, 0, "operator stake has incorrect next update block"); + assertEq(newTotalStake.updateBlockNumber, uint32(block.number), "total stake has incorrect update block"); + assertEq(newTotalStake.nextUpdateBlockNumber, 0, "total stake has incorrect next update block"); + } else { + // Ensure no change to operator stake + assertTrue(_isUnchanged(prevOperatorStake, newOperatorStake), "operator stake incorrectly updated"); + } + } } - // make sure that the last stake update is as expected - IStakeRegistry.StakeUpdate memory lastOperatorStakeUpdate = stakeRegistry.getLatestStakeUpdate(defaultOperatorId, defaultQuorumNumber); - assertEq(lastOperatorStakeUpdate.stake, stakes[i - 1], "1"); - assertEq(lastOperatorStakeUpdate.nextUpdateBlockNumber, uint32(0), "2"); + // Now that we've deregistered all the operators, check the final results + // For the quorums we chose to deregister from, the total stake should be zero + IStakeRegistry.StakeUpdate[] memory finalTotalStakes = _getLatestTotalStakeUpdates(registeredQuorums); + for (uint i = 0; i < registeredQuorums.length; i++) { + uint8 registeredQuorum = uint8(registeredQuorums[i]); + + // Whether or not we deregistered operators from this quorum + bool deregistered = quorumsToRemoveBitmap.isSet(registeredQuorum); + + if (deregistered) { + assertEq(finalTotalStakes[i].stake, 0, "failed to remove all stake from quorum"); + assertEq(finalTotalStakes[i].updateBlockNumber, uint32(block.number), "failed to remove all stake from quorum"); + assertEq(finalTotalStakes[i].nextUpdateBlockNumber, 0, "failed to remove all stake from quorum"); + } else { + assertTrue(_isUnchanged(finalTotalStakes[i], prevTotalStakes[i]), "incorrectly updated total stake history for unmodified quorum"); + } + } } - function testRecordTotalStakeUpdate_Valid( - uint24 blocksPassed, - uint96[] memory stakes + /** + * @dev Registers multiple operators for all initialized quorums, each with a weight + * equal to the minimum + additionalStake. This step is done in a single block. + * + * Then, deregisters operators for all quorums over multiple blocks and + * tests that total stake history is updated correctly over time. + * @param operatorsPerBlock The number of unique operators to deregister during each block + * @param totalBlocks The number of times we'll deregister `operatorsPerBlock` (we only move 1 block each time) + */ + function testFuzz_deregisterOperator_MultiOperator_MultiBlock( + uint8 operatorsPerBlock, + uint8 totalBlocks, + uint16 additionalStake ) public { - // initialize at a non-zero block number - uint32 intialBlockNumber = 100; - cheats.roll(intialBlockNumber); - uint32 cumulativeBlockNumber = intialBlockNumber; - // loop through each one of the blocks passed, roll that many blocks, create an Operator Stake Update for total stake, and trigger a total stake update - for (uint256 i = 0; i < stakes.length; i++) { - int256 stakeDelta; - if (i == 0) { - stakeDelta = _calculateDelta({prev: 0, cur: stakes[i]}); - } else { - stakeDelta = _calculateDelta({prev: stakes[i-1], cur: stakes[i]}); + /// We want between [1, 4] unique operators to register for all quorums each block, + /// and we want to test this for [2, 5] blocks + cheats.assume(operatorsPerBlock >= 1 && operatorsPerBlock <= 4); + cheats.assume(totalBlocks >= 2 && totalBlocks <= 5); + + uint numOperators = operatorsPerBlock * totalBlocks; + uint operatorIdx; // track index in setups over test + + // Select multiple new operators, set their weight equal to the minimum plus some additional, + // then register them for all initialized quorums + DeregisterSetup[] memory setups = _fuzz_setupDeregisterOperators({ + numOperators: numOperators, + registeredFor: initializedQuorumBitmap, + fuzzy_toRemove: initializedQuorumBitmap, + fuzzy_addtlStake: additionalStake + }); + + // For all operators, we're going to register for and then deregister from all initialized quorums + bytes memory registeredQuorums = initializedQuorumBytes; + + IStakeRegistry.StakeUpdate[] memory prevTotalStakes = _getLatestTotalStakeUpdates(registeredQuorums); + uint startBlock = block.number; + + for (uint i = 1; i <= totalBlocks; i++) { + // Move to current block number + uint currBlock = startBlock + i; + cheats.roll(currBlock); + + uint[] memory prevHistoryLengths = _getTotalStakeHistoryLengths(registeredQuorums); + + // Within this block: deregister some operators for all quorums and add the stake removed + // to `_totalStakeRemoved` for later checks + for (uint j = 0; j < operatorsPerBlock; j++) { + DeregisterSetup memory setup = setups[operatorIdx]; + operatorIdx++; + + cheats.prank(address(registryCoordinator)); + stakeRegistry.deregisterOperator(setup.operatorId, setup.quorumsToRemove); + + for (uint k = 0; k < registeredQuorums.length; k++) { + uint8 quorumNumber = uint8(registeredQuorums[k]); + _totalStakeRemoved[quorumNumber] += setup.prevOperatorStakes[k].stake; + } } - - // Perform the update - stakeRegistry.recordTotalStakeUpdate(defaultQuorumNumber, stakeDelta); - - IStakeRegistry.StakeUpdate memory newStakeUpdate; - uint historyLength = stakeRegistry.getTotalStakeHistoryLength(defaultQuorumNumber); - if (historyLength != 0) { - newStakeUpdate = stakeRegistry.getTotalStakeUpdateAtIndex(defaultQuorumNumber, historyLength-1); + uint[] memory newHistoryLengths = _getTotalStakeHistoryLengths(registeredQuorums); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(registeredQuorums); + + // Validate the sum of all updates this block: + // Each quorum should have a new historical entry with the correct update block pointers + // ... and each quorum's stake should have decreased by `_totalStakeRemoved[quorum]` + for (uint j = 0; j < registeredQuorums.length; j++) { + uint8 quorumNumber = uint8(registeredQuorums[j]); + + // Check that we've added 1 to total stake history length + assertEq(prevHistoryLengths[j] + 1, newHistoryLengths[j], "total history should have a new entry"); + + // Validate latest entry correctness + assertEq(newTotalStakes[j].stake + _totalStakeRemoved[quorumNumber], prevTotalStakes[j].stake, "stake not removed correctly from total stake"); + assertEq(newTotalStakes[j].updateBlockNumber, currBlock, "latest update should be from current block"); + assertEq(newTotalStakes[j].nextUpdateBlockNumber, 0, "latest update should not have next update block"); + + IStakeRegistry.StakeUpdate memory prevUpdate = + stakeRegistry.getTotalStakeUpdateAtIndex(quorumNumber, prevHistoryLengths[j]-1); + // Validate previous entry was updated correctly + assertTrue(prevUpdate.stake > newTotalStakes[j].stake, "previous update should have higher stake than latest"); + assertEq(prevUpdate.updateBlockNumber + 1, newTotalStakes[j].updateBlockNumber, "prev entry should be from last block"); + assertEq(prevUpdate.nextUpdateBlockNumber, newTotalStakes[j].updateBlockNumber, "prev entry.next should be latest.cur"); } - // Check that the most recent entry reflects the correct stake - assertEq(newStakeUpdate.stake, stakes[i]); + } - cumulativeBlockNumber += blocksPassed; - cheats.roll(cumulativeBlockNumber); + // Now that we've deregistered all the operators, check the final results + // Each quorum's stake should be zero + IStakeRegistry.StakeUpdate[] memory finalTotalStakes = _getLatestTotalStakeUpdates(registeredQuorums); + for (uint i = 0; i < registeredQuorums.length; i++) { + assertEq(finalTotalStakes[i].stake, 0, "failed to remove all stake from quorum"); } } +} - function _initializeQuorum( - uint8 quorumNumber, - uint96 minimumStake, - IStakeRegistry.StrategyParams[] memory strategyParams - ) internal { +/// @notice Tests for StakeRegistry.updateOperatorStake +contract StakeRegistryUnitTests_StakeUpdates is StakeRegistryUnitTests { + + using BitmapUtils for *; + + function test_updateOperatorStake_Revert_WhenNotRegistryCoordinator() public { + UpdateSetup memory setup = _fuzz_setupUpdateOperatorStake({ + registeredFor: initializedQuorumBitmap, + fuzzy_Delta: 0 + }); + + cheats.expectRevert("StakeRegistry.onlyRegistryCoordinator: caller is not the RegistryCoordinator"); + stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); + } + + function testFuzz_updateOperatorStake_Revert_WhenQuorumDoesNotExist(bytes32 rand) public { + // Create a new operator registered for all quorums + UpdateSetup memory setup = _fuzz_setupUpdateOperatorStake({ + registeredFor: initializedQuorumBitmap, + fuzzy_Delta: 0 + }); + + // Get a list of valid quorums ending in an invalid quorum number + bytes memory invalidQuorums = _fuzz_getInvalidQuorums(rand); + + cheats.expectRevert("StakeRegistry.updateOperatorStake: quorum does not exist"); cheats.prank(address(registryCoordinator)); + stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, invalidQuorums); + } - stakeRegistry.initializeQuorum(quorumNumber, minimumStake, strategyParams); - initializedQuorums[quorumNumber] = true; + /** + * @dev Registers an operator for all initialized quorums, giving them exactly the minimum stake + * for each quorum. Then applies `stakeDelta` to their current weight, adding or removing some + * stake from each quorum. + * + * updateOperatorStake should then update the operator's stake using the new weight - we test + * what happens when the operator remains at/above minimum stake, vs dipping below + */ + function testFuzz_updateOperatorStake_SingleOperator_SingleBlock(int8 stakeDelta) public { + UpdateSetup memory setup = _fuzz_setupUpdateOperatorStake({ + registeredFor: initializedQuorumBitmap, + fuzzy_Delta: stakeDelta + }); + + // Get starting state + IStakeRegistry.StakeUpdate[] memory prevOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.quorumNumbers); + IStakeRegistry.StakeUpdate[] memory prevTotalStakes = _getLatestTotalStakeUpdates(setup.quorumNumbers); + + // updateOperatorStake + cheats.prank(address(registryCoordinator)); + uint192 quorumsToRemove = + stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); + + // Get ending state + IStakeRegistry.StakeUpdate[] memory newOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.quorumNumbers); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(setup.quorumNumbers); + + // Check results for each quorum + for (uint i = 0; i < setup.quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(setup.quorumNumbers[i]); + + uint96 minimumStake = setup.minimumStakes[i]; + uint96 endingWeight = setup.endingWeights[i]; + + IStakeRegistry.StakeUpdate memory prevOperatorStake = prevOperatorStakes[i]; + IStakeRegistry.StakeUpdate memory prevTotalStake = prevTotalStakes[i]; + + IStakeRegistry.StakeUpdate memory newOperatorStake = newOperatorStakes[i]; + IStakeRegistry.StakeUpdate memory newTotalStake = newTotalStakes[i]; + + // Sanity-check setup - operator should start with minimumStake + assertTrue(prevOperatorStake.stake == minimumStake, "operator should start with nonzero stake"); + + if (endingWeight > minimumStake) { + // Check updating an operator who has added stake above the minimum: + + // Only updates should be stake added to operator/total stakes + uint96 stakeAdded = setup.stakeDeltaAbs; + assertEq(prevOperatorStake.stake + stakeAdded, newOperatorStake.stake, "failed to add delta to operator stake"); + assertEq(prevTotalStake.stake + stakeAdded, newTotalStake.stake, "failed to add delta to total stake"); + // Return value should be empty since we're still above the minimum + assertTrue(quorumsToRemove.isEmpty(), "positive stake delta should not remove any quorums"); + } else if (endingWeight < minimumStake) { + // Check updating an operator who is now below the minimum: + + // Stake should now be zero, regardless of stake delta + uint96 stakeRemoved = minimumStake; + assertEq(prevOperatorStake.stake - stakeRemoved, newOperatorStake.stake, "failed to remove delta from operator stake"); + assertEq(prevTotalStake.stake - stakeRemoved, newTotalStake.stake, "failed to remove delta from total stake"); + assertEq(newOperatorStake.stake, 0, "operator stake should now be zero"); + // Quorum should be added to return bitmap + assertTrue(quorumsToRemove.isSet(quorumNumber), "quorum should be in removal bitmap"); + } else { + // Check that no update occurs if weight remains the same + assertTrue(_isUnchanged(prevOperatorStake, newOperatorStake), "neutral stake delta should not have changed operator stake history"); + assertTrue(_isUnchanged(prevTotalStake, newTotalStake), "neutral stake delta should not have changed total stake history"); + // Check that return value is empty - we're still at the minimum, so no quorums should be removed + assertTrue(quorumsToRemove.isEmpty(), "neutral stake delta should not remove any quorums"); + } + } } - // utility function for registering an operator with a valid quorumBitmap and stakesForQuorum using provided randomness - function _registerOperatorRandomValid( - address operator, - bytes32 operatorId, - uint256 psuedoRandomNumber - ) internal returns(uint256, uint96[] memory){ - // generate uint256 quorumBitmap from psuedoRandomNumber and limit to maxQuorumsToRegisterFor quorums and register for quorum 0 - uint256 quorumBitmap = uint256(keccak256(abi.encodePacked(psuedoRandomNumber, "quorumBitmap"))) & (1 << maxQuorumsToRegisterFor - 1) | 1; - // generate uint80[] stakesForQuorum from psuedoRandomNumber - uint80[] memory stakesForQuorum = new uint80[](BitmapUtils.countNumOnes(quorumBitmap)); - for(uint i = 0; i < stakesForQuorum.length; i++) { - stakesForQuorum[i] = uint80(uint256(keccak256(abi.encodePacked(psuedoRandomNumber, i, "stakesForQuorum")))); + /** + * @dev Registers multiple operators for all initialized quorums, giving them exactly the minimum stake + * for each quorum. Then applies `stakeDelta` to their current weight, adding or removing some + * stake from each quorum. + * + * updateOperatorStake should then update each operator's stake using the new weight - we test + * what happens to the total stake history after all stakes have been updated + */ + function testFuzz_updateOperatorStake_MultiOperator_SingleBlock( + uint8 numOperators, + int8 stakeDelta + ) public { + cheats.assume(numOperators > 1 && numOperators < 20); + + // Select multiple new operators, register each for all quorums with weight equal + // to the quorum's minimum, and then apply `stakeDelta` to their current weight. + UpdateSetup[] memory setups = _fuzz_setupUpdateOperatorStakes({ + numOperators: numOperators, + registeredFor: initializedQuorumBitmap, + fuzzy_Delta: stakeDelta + }); + + bytes memory quorumNumbers = initializedQuorumBytes; + // Get initial total history state + uint[] memory initialHistoryLengths = _getTotalStakeHistoryLengths(quorumNumbers); + IStakeRegistry.StakeUpdate[] memory initialTotalStakes = _getLatestTotalStakeUpdates(quorumNumbers); + + // Call `updateOperatorStake` one by one + for (uint i = 0; i < numOperators; i++) { + UpdateSetup memory setup = setups[i]; + + // updateOperatorStake + cheats.prank(address(registryCoordinator)); + stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); } - return (quorumBitmap, _registerOperatorValid(operator, operatorId, quorumBitmap, stakesForQuorum)); - } + // Check final results for each quorum + uint[] memory finalHistoryLengths = _getTotalStakeHistoryLengths(quorumNumbers); + IStakeRegistry.StakeUpdate[] memory finalTotalStakes = _getLatestTotalStakeUpdates(quorumNumbers); - // utility function for registering an operator - function _registerOperatorValid( - address operator, - bytes32 operatorId, - uint256 quorumBitmap, - uint80[] memory stakesForQuorum - ) internal returns(uint96[] memory){ - cheats.assume(quorumBitmap != 0); - - bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); - - // pad the stakesForQuorum array with the minimum stake for the quorums - uint96[] memory paddedStakesForQuorum = new uint96[](BitmapUtils.countNumOnes(quorumBitmap)); - for(uint i = 0; i < paddedStakesForQuorum.length; i++) { - uint96 minimumStakeForQuorum = stakeRegistry.minimumStakeForQuorum(uint8(quorumNumbers[i])); - // make sure the operator has at least the mininmum stake in each quorum it is registering for - if (i >= stakesForQuorum.length || stakesForQuorum[i] < minimumStakeForQuorum) { - paddedStakesForQuorum[i] = minimumStakeForQuorum; + for (uint i = 0; i < quorumNumbers.length; i++) { + IStakeRegistry.StakeUpdate memory initialTotalStake = initialTotalStakes[i]; + IStakeRegistry.StakeUpdate memory finalTotalStake = finalTotalStakes[i]; + + uint96 minimumStake = setups[0].minimumStakes[i]; + uint96 endingWeight = setups[0].endingWeights[i]; + uint96 stakeDeltaAbs = setups[0].stakeDeltaAbs; + + // Sanity-check setup: previous total stake should be minimumStake * numOperators + assertEq(initialTotalStake.stake, minimumStake * numOperators, "quorum should start with minimum stake from all operators"); + + // history lengths should be unchanged + assertEq(initialHistoryLengths[i], finalHistoryLengths[i], "history lengths should remain unchanged"); + + if (endingWeight > minimumStake) { + // All operators had their stake increased by stakeDelta + uint96 stakeAdded = numOperators * stakeDeltaAbs; + assertEq(initialTotalStake.stake + stakeAdded, finalTotalStake.stake, "failed to add delta for all operators"); + } else if (endingWeight < minimumStake) { + // All operators had their entire stake removed + uint96 stakeRemoved = numOperators * minimumStake; + assertEq(initialTotalStake.stake - stakeRemoved, finalTotalStake.stake, "failed to remove delta from total stake"); + assertEq(finalTotalStake.stake, 0, "final total stake should be zero"); } else { - paddedStakesForQuorum[i] = stakesForQuorum[i]; + // No change in stake for any operator + assertTrue(_isUnchanged(initialTotalStake, finalTotalStake), "neutral stake delta should result in no change"); + } + } + } + + /** + * @dev Registers an operator for all initialized quorums, giving them exactly the minimum stake + * for each quorum. + * + * Then over multiple blocks, derives a random stake delta and applies it to their weight, testing + * the result on the operator and total stake histories. + */ + function testFuzz_updateOperatorStake_SingleOperator_MultiBlocknumberChecks( + uint8 totalBlocks, + int8 stakeDelta + ) public { + cheats.assume(totalBlocks >= 2 && totalBlocks <= 8); + + uint256 startBlock = block.number; + for (uint256 j = 1; j <= totalBlocks; j++) { + UpdateSetup memory setup = _fuzz_setupUpdateOperatorStake({ + registeredFor: initializedQuorumBitmap, + fuzzy_Delta: stakeDelta + }); + + // Get starting state + IStakeRegistry.StakeUpdate[] memory prevOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.quorumNumbers); + IStakeRegistry.StakeUpdate[] memory prevTotalStakes = _getLatestTotalStakeUpdates(setup.quorumNumbers); + uint256[] memory prevOperatorHistoryLengths = _getStakeHistoryLengths(setup.operatorId, setup.quorumNumbers); + + // Move to current block number + uint256 currBlock = startBlock + j; + cheats.roll(currBlock); + + // updateOperatorStake + cheats.prank(address(registryCoordinator)); + uint192 quorumsToRemove = + stakeRegistry.updateOperatorStake(setup.operator, setup.operatorId, setup.quorumNumbers); + + // Get ending state + IStakeRegistry.StakeUpdate[] memory newOperatorStakes = _getLatestStakeUpdates(setup.operatorId, setup.quorumNumbers); + IStakeRegistry.StakeUpdate[] memory newTotalStakes = _getLatestTotalStakeUpdates(setup.quorumNumbers); + uint256[] memory newOperatorHistoryLengths = _getStakeHistoryLengths(setup.operatorId, setup.quorumNumbers); + + // Check results for each quorum + for (uint i = 0; i < setup.quorumNumbers.length; i++) { + uint8 quorumNumber = uint8(setup.quorumNumbers[i]); + + uint96 minimumStake = setup.minimumStakes[i]; + uint96 endingWeight = setup.endingWeights[i]; + + IStakeRegistry.StakeUpdate memory prevOperatorStake = prevOperatorStakes[i]; + // IStakeRegistry.StakeUpdate memory prevTotalStake = prevTotalStakes[i]; + + IStakeRegistry.StakeUpdate memory newOperatorStake = newOperatorStakes[i]; + // IStakeRegistry.StakeUpdate memory newTotalStake = newTotalStakes[i]; + + // Sanity-check setup - operator should start with minimumStake + assertTrue(prevOperatorStake.stake == minimumStake, "operator should start with nonzero stake"); + + if (endingWeight > minimumStake) { + // Check updating an operator who has added stake above the minimum: + uint96 stakeAdded = setup.stakeDeltaAbs; + assertEq(prevOperatorStake.stake + stakeAdded, newOperatorStake.stake, "failed to add delta to operator stake"); + assertEq(prevTotalStakes[i].stake + stakeAdded, newTotalStakes[i].stake, "failed to add delta to total stake"); + // Return value should be empty since we're still above the minimum + assertTrue(quorumsToRemove.isEmpty(), "positive stake delta should not remove any quorums"); + assertEq(prevOperatorHistoryLengths[i] + 1, newOperatorHistoryLengths[i], "operator should have a new pushed update"); + } else if (endingWeight < minimumStake) { + // Check updating an operator who is now below the minimum: + + // Stake should now be zero, regardless of stake delta + uint96 stakeRemoved = minimumStake; + assertEq(prevOperatorStake.stake - stakeRemoved, newOperatorStake.stake, "failed to remove delta from operator stake"); + // assertEq(prevTotalStake.stake - stakeRemoved, newTotalStake.stake, "failed to remove delta from total stake"); + assertEq(newOperatorStake.stake, 0, "operator stake should now be zero"); + // Quorum should be added to return bitmap + assertTrue(quorumsToRemove.isSet(quorumNumber), "quorum should be in removal bitmap"); + if (prevOperatorStake.stake >= minimumStake) { + // Total stakes and operator history should be updated + assertEq(prevOperatorHistoryLengths[i] + 1, newOperatorHistoryLengths[i], "operator should have a new pushed update"); + assertEq(prevTotalStakes[i].stake, newTotalStakes[i].stake + prevOperatorStake.stake, "failed to remove from total stake"); + } else { + // Total stakes and history should remain unchanged + assertEq(prevOperatorHistoryLengths[i], newOperatorHistoryLengths[i], "history lengths should remain unchanged"); + assertEq(prevTotalStakes[i].stake, newTotalStakes[i].stake, "total stake should remain unchanged"); + } + } else { + // Check that no update occurs if weight remains the same + assertTrue(_isUnchanged(prevOperatorStake, newOperatorStake), "neutral stake delta should not have changed operator stake history"); + assertTrue(_isUnchanged(prevTotalStakes[i], newTotalStakes[i]), "neutral stake delta should not have changed total stake history"); + // Check that return value is empty - we're still at the minimum, so no quorums should be removed + assertTrue(quorumsToRemove.isEmpty(), "neutral stake delta should not remove any quorums"); + assertEq(prevOperatorHistoryLengths[i], newOperatorHistoryLengths[i], "history lengths should remain unchanged"); + } } } + } +} - // set the weights of the operator - for(uint i = 0; i < paddedStakesForQuorum.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), operator, paddedStakesForQuorum[i]); +/// @notice Tests for StakeRegistry.weightOfOperatorForQuorum view function +contract StakeRegistryUnitTests_weightOfOperatorForQuorum is StakeRegistryUnitTests { + using BitmapUtils for *; + + /** + * @dev Initialize a new quorum with fuzzed multipliers and corresponding shares for an operator. + * The minimum stake for the quorum is 0 so that any fuzzed input shares will register the operator + * successfully and return a value for weightOfOperatorForQuorum. Fuzz test sets the operator shares + * and asserts that the summed weight of the operator is correct. + */ + function test_weightOfOperatorForQuorum( + address operator, + uint96[] memory multipliers, + uint96[] memory shares + ) public { + cheats.assume(0 < multipliers.length && multipliers.length <= MAX_WEIGHING_FUNCTION_LENGTH); + cheats.assume(shares.length >= multipliers.length); + cheats.assume(multipliers.length > 3); + + // Initialize quorum with strategies of fuzzed multipliers. + // Bound multipliers and shares max values to prevent overflows + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](3); + for (uint i = 0; i < strategyParams.length; i++) { + multipliers[i] = uint96(_randUint({rand: bytes32(uint256(multipliers[i])), min: 0, max: 1000*WEIGHTING_DIVISOR })); + shares[i] = uint96(_randUint({rand: bytes32(uint256(shares[i])), min: 0, max: 10e20 })); + + IStrategy strat = IStrategy(address(uint160(uint256(keccak256(abi.encodePacked("Voteweighing test", i)))))); + strategyParams[i] = IStakeRegistry.StrategyParams( + strat, + uint96(WEIGHTING_DIVISOR) + multipliers[i] + ); } + cheats.prank(address(registryCoordinator)); + uint8 quorumNumber = nextQuorum; + stakeRegistry.initializeQuorum(quorumNumber, 0 /* minimumStake */, strategyParams); - // register operator - uint256 gasleftBefore = gasleft(); + // set the operator shares + for (uint i = 0; i < strategyParams.length; i++) { + delegationMock.setOperatorShares(operator, strategyParams[i].strategy, shares[i]); + } + + // registerOperator + uint256 operatorBitmap = uint256(0).setBit(quorumNumber); + bytes memory quorumNumbers = operatorBitmap.bitmapToBytesArray(); cheats.prank(address(registryCoordinator)); - stakeRegistry.registerOperator(operator, operatorId, quorumNumbers); - gasUsed = gasleftBefore - gasleft(); - - return paddedStakesForQuorum; + stakeRegistry.registerOperator(operator, defaultOperatorId, quorumNumbers); + + + // assert weight of the operator + uint96 expectedWeight = 0; + for (uint i = 0; i < strategyParams.length; i++) { + expectedWeight += uint96(uint256(shares[i]) * uint256(strategyParams[i].multiplier) / WEIGHTING_DIVISOR); + } + assertEq(stakeRegistry.weightOfOperatorForQuorum(quorumNumber, operator), expectedWeight); } - // utility function for deregistering an operator - function _deregisterOperatorValid( - bytes32 operatorId, - uint256 quorumBitmap - ) internal { - bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); + /// @dev consider multipliers for 3 strategies + function test_weightOfOperatorForQuorum_3Strategies( + address operator, + uint96[3] memory shares + ) public { + // 3 LST Strat multipliers, rETH, stETH, ETH + uint96[] memory multipliers = new uint96[](3); + multipliers[0] = uint96(1070136092289993178); + multipliers[1] = uint96(1071364636818145808); + multipliers[2] = uint96(1000000000000000000); + + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](3); + for (uint i = 0; i < strategyParams.length; i++) { + shares[i] = uint96(_randUint({rand: bytes32(uint256(shares[i])), min: 0, max: 1e24 })); + IStrategy strat = IStrategy(address(uint160(uint256(keccak256(abi.encodePacked("Voteweighing test", i)))))); + strategyParams[i] = IStakeRegistry.StrategyParams( + strat, + multipliers[i] + ); + } - // deregister operator + // create a valid quorum cheats.prank(address(registryCoordinator)); - stakeRegistry.deregisterOperator(operatorId, quorumNumbers); - } + uint8 quorumNumber = nextQuorum; + stakeRegistry.initializeQuorum(quorumNumber, 0 /* minimumStake */, strategyParams); - function _incrementAddress(address start, uint256 inc) internal pure returns(address) { - return address(uint160(uint256(uint160(start) + inc))); - } + // set the operator shares + for (uint i = 0; i < strategyParams.length; i++) { + delegationMock.setOperatorShares(operator, strategyParams[i].strategy, shares[i]); + } + + // registerOperator + uint256 operatorBitmap = uint256(0).setBit(quorumNumber); + bytes memory quorumNumbers = operatorBitmap.bitmapToBytesArray(); + cheats.prank(address(registryCoordinator)); + stakeRegistry.registerOperator(operator, defaultOperatorId, quorumNumbers); - function _incrementBytes32(bytes32 start, uint256 inc) internal pure returns(bytes32) { - return bytes32(uint256(start) + inc); - } - function _calculateDelta(uint96 prev, uint96 cur) internal pure returns (int256) { - return int256(uint256(cur)) - int256(uint256(prev)); + // assert weight of the operator + uint96 expectedWeight = 0; + for (uint i = 0; i < strategyParams.length; i++) { + expectedWeight += uint96(uint256(shares[i]) * uint256(strategyParams[i].multiplier) / WEIGHTING_DIVISOR); + } + assertEq(stakeRegistry.weightOfOperatorForQuorum(quorumNumber, operator), expectedWeight); } } diff --git a/test/utils/MockAVSDeployer.sol b/test/utils/MockAVSDeployer.sol index 236fed40..bc3e2c0c 100644 --- a/test/utils/MockAVSDeployer.sol +++ b/test/utils/MockAVSDeployer.sol @@ -66,6 +66,9 @@ contract MockAVSDeployer is Test { DelegationMock public delegationMock; EigenPodManagerMock public eigenPodManagerMock; + /// @notice StakeRegistry, Constant used as a divisor in calculating weights. + uint256 public constant WEIGHTING_DIVISOR = 1e18; + address public proxyAdminOwner = address(uint160(uint256(keccak256("proxyAdminOwner")))); address public registryCoordinatorOwner = address(uint160(uint256(keccak256("registryCoordinatorOwner")))); address public pauser = address(uint160(uint256(keccak256("pauser")))); @@ -255,7 +258,7 @@ contract MockAVSDeployer is Test { quorumStrategiesConsideredAndMultipliers[i] = new IStakeRegistry.StrategyParams[](1); quorumStrategiesConsideredAndMultipliers[i][0] = IStakeRegistry.StrategyParams( IStrategy(address(uint160(i))), - uint96(i+1) + uint96(WEIGHTING_DIVISOR) ); } @@ -316,7 +319,7 @@ contract MockAVSDeployer is Test { bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), operator, stake); + _setOperatorWeight(operator, uint8(quorumNumbers[i]), stake); } ISignatureUtils.SignatureWithSaltAndExpiry memory emptySignatureAndExpiry; @@ -335,7 +338,7 @@ contract MockAVSDeployer is Test { bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), operator, stakes[uint8(quorumNumbers[i])]); + _setOperatorWeight(operator, uint8(quorumNumbers[i]), stakes[uint8(quorumNumbers[i])]); } ISignatureUtils.SignatureWithSaltAndExpiry memory emptySignatureAndExpiry; @@ -387,6 +390,20 @@ contract MockAVSDeployer is Test { return (operatorMetadatas, expectedOperatorOverallIndices); } + /** + * @dev Set the operator weight for a given quorum. Note we have to do this by setting delegationMock operatorShares + * Given each quorum must have at least one strategy, we set operatorShares for this strategy to this weight + * Returns actual weight calculated set for operator shares in DelegationMock since multiplier and WEIGHTING_DIVISOR calculations + * can give small rounding errors. + */ + function _setOperatorWeight(address operator, uint8 quorumNumber, uint96 weight) internal returns (uint96) { + // Set StakeRegistry operator weight by setting DelegationManager operator shares + (IStrategy strategy, uint96 multiplier) = stakeRegistry.strategyParams(quorumNumber, 0); + uint256 actualWeight = ((uint256(weight) * WEIGHTING_DIVISOR) / uint256(multiplier)); + delegationMock.setOperatorShares(operator, strategy, actualWeight); + return uint96(actualWeight); + } + function _incrementAddress(address start, uint256 inc) internal pure returns(address) { return address(uint160(uint256(uint160(start) + inc))); }