From 8c2d784f6c513a343a2766b6e5e30d8cc024adf5 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 12:07:35 +0000 Subject: [PATCH 01/60] feat(byov): add allocation-based validator deposit with strict validation --- contracts/src/OperatorsRegistry.1.sol | 123 ++-- contracts/src/River.1.sol | 8 +- .../ConsensusLayerDepositManager.1.sol | 44 +- .../src/interfaces/IOperatorRegistry.1.sol | 28 +- .../IConsensusLayerDepositManager.1.sol | 20 +- contracts/test/Firewall.t.sol | 20 +- contracts/test/OperatorsRegistry.1.t.sol | 564 +++++++++++++++--- contracts/test/River.1.t.sol | 118 +++- .../ConsensusLayerDepositManager.1.t.sol | 128 +++- 9 files changed, 798 insertions(+), 255 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 9e10f3a8..94f6bc8b 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -24,9 +24,6 @@ import "./state/migration/OperatorsRegistry_FundedKeyEventRebroadcasting_Operato /// @author Alluvial Finance Inc. /// @notice This contract handles the list of operators and their keys contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrable, IProtocolVersion { - /// @notice Maximum validators given to an operator per selection loop round - uint256 internal constant MAX_VALIDATOR_ATTRIBUTION_PER_ROUND = 5; - /// @inheritdoc IOperatorsRegistryV1 function initOperatorsRegistryV1(address _admin, address _river) external init(0) { _setAdmin(_admin); @@ -211,7 +208,7 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab } /// @inheritdoc IOperatorsRegistryV1 - function getNextValidatorsToDepositFromActiveOperators(uint256 _count) + function getNextValidatorsToDepositFromActiveOperators(OperatorAllocation[] memory _allocations) external view returns (bytes[] memory publicKeys, bytes[] memory signatures) @@ -219,11 +216,21 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab (OperatorsV2.CachedOperator[] memory operators, uint256 fundableOperatorCount) = OperatorsV2.getAllFundable(); if (fundableOperatorCount == 0) { - return (publicKeys, signatures); + return (new bytes[](0), new bytes[](0)); } + uint256 len = _allocations.length; - _updateCountOfPickedValidatorsForEachOperator(operators, fundableOperatorCount, _count); - + // First pass: validate ordering and update picked count for each operator + for (uint256 i = 0; i < len; ++i) { + uint256 operatorIndex = _allocations[i].operatorIndex; + if (i > 0 && !(operatorIndex > _allocations[i - 1].operatorIndex)) { + revert UnorderedOperatorList(); + } + if (_allocations[i].validatorCount == 0) { + revert AllocationWithZeroValidatorCount(); + } + _updateCountOfPickedValidatorsForEachOperator(operators, operatorIndex, _allocations[i].validatorCount); + } // we loop on all operators for (uint256 idx = 0; idx < fundableOperatorCount; ++idx) { // if we picked keys on any operator, we extract the keys from storage and concatenate them in the result @@ -449,12 +456,12 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab } /// @inheritdoc IOperatorsRegistryV1 - function pickNextValidatorsToDeposit(uint256 _count) + function pickNextValidatorsToDeposit(OperatorAllocation[] calldata _allocations) external onlyRiver returns (bytes[] memory publicKeys, bytes[] memory signatures) { - return _pickNextValidatorsToDepositFromActiveOperators(_count); + return _pickNextValidatorsToDepositFromActiveOperators(_allocations); } /// @inheritdoc IOperatorsRegistryV1 @@ -654,13 +661,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab return res; } - /// @notice Internal utility to verify if an operator has fundable keys during the selection process - /// @param _operator The Operator structure in memory - /// @return True if at least one fundable key is available - function _hasFundableKeys(OperatorsV2.CachedOperator memory _operator) internal pure returns (bool) { - return (_operator.funded + _operator.picked) < _operator.limit; - } - /// @notice Internal utility to retrieve the actual stopped validator count of an operator from the reported array /// @param _operatorIndex The operator index /// @return The count of stopped validators @@ -668,17 +668,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab return OperatorsV2._getStoppedValidatorCountAtIndex(OperatorsV2.getStoppedValidators(), _operatorIndex); } - /// @notice Internal utility to get the count of active validators during the deposit selection process - /// @param _operator The Operator structure in memory - /// @return The count of active validators for the operator - function _getActiveValidatorCountForDeposits(OperatorsV2.CachedOperator memory _operator) - internal - view - returns (uint256) - { - return (_operator.funded + _operator.picked) - _getStoppedValidatorsCount(_operator.index); - } - /// @notice Internal utility to retrieve _count or lower fundable keys /// @dev The selection process starts by retrieving the full list of active operators with at least one fundable key. /// @dev @@ -699,10 +688,10 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab /// @dev /// @dev When we reach the requested key count or when all available keys are used, we perform a final loop on all the operators and extract keys /// @dev if any operator has a positive picked count. We then update the storage counters and return the arrays with the public keys and signatures. - /// @param _count Amount of keys required. Contract is expected to send _count or lower. + /// @param _allocations The operator allocations specifying how many validators per operator sorted by operator index /// @return publicKeys An array of fundable public keys /// @return signatures An array of signatures linked to the public keys - function _pickNextValidatorsToDepositFromActiveOperators(uint256 _count) + function _pickNextValidatorsToDepositFromActiveOperators(OperatorAllocation[] memory _allocations) internal returns (bytes[] memory publicKeys, bytes[] memory signatures) { @@ -712,8 +701,19 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab return (new bytes[](0), new bytes[](0)); } - _updateCountOfPickedValidatorsForEachOperator(operators, fundableOperatorCount, _count); + uint256 len = _allocations.length; + // Iterate over the allocations, validate ordering and update the picked count for each operator + for (uint256 i = 0; i < len; ++i) { + uint256 operatorIndex = _allocations[i].operatorIndex; + if (i > 0 && !(operatorIndex > _allocations[i - 1].operatorIndex)) { + revert UnorderedOperatorList(); + } + if (_allocations[i].validatorCount == 0) { + revert AllocationWithZeroValidatorCount(); + } + _updateCountOfPickedValidatorsForEachOperator(operators, operatorIndex, _allocations[i].validatorCount); + } // we loop on all operators for (uint256 idx = 0; idx < fundableOperatorCount; ++idx) { // if we picked keys on any operator, we extract the keys from storage and concatenate them in the result @@ -731,56 +731,25 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab function _updateCountOfPickedValidatorsForEachOperator( OperatorsV2.CachedOperator[] memory operators, - uint256 fundableOperatorCount, - uint256 _count - ) internal view { - while (_count > 0) { - // loop on operators to find the first that has fundable keys, taking into account previous loop round attributions - uint256 selectedOperatorIndex = 0; - for (; selectedOperatorIndex < fundableOperatorCount;) { - if (_hasFundableKeys(operators[selectedOperatorIndex])) { - break; - } - unchecked { - ++selectedOperatorIndex; - } - } - - // if we reach the end, we have allocated all keys - if (selectedOperatorIndex == fundableOperatorCount) { - break; - } - - // we start from the next operator and we try to find one that has fundable keys but a lower (funded + picked) - stopped value - for (uint256 idx = selectedOperatorIndex + 1; idx < fundableOperatorCount;) { - if ( - _getActiveValidatorCountForDeposits(operators[idx]) - < _getActiveValidatorCountForDeposits(operators[selectedOperatorIndex]) - && _hasFundableKeys(operators[idx]) - ) { - selectedOperatorIndex = idx; - } - unchecked { - ++idx; + uint256 _operatorIndex, + uint256 _validatorCount + ) internal pure { + // Find the operator in the compacted array by matching .index + for (uint256 i = 0; i < operators.length; ++i) { + if (operators[i].index == _operatorIndex) { + // we take the smallest value between limit - (funded + picked), _validatorCount + uint256 availableKeys = operators[i].limit - (operators[i].funded + operators[i].picked); + + if (_validatorCount > availableKeys) { + revert OperatorDoesNotHaveEnoughFundableKeys(_operatorIndex, _validatorCount, availableKeys); } + // we update the cached picked amount + operators[i].picked += uint32(_validatorCount); + return; } - - // we take the smallest value between limit - (funded + picked), _requestedAmount and MAX_VALIDATOR_ATTRIBUTION_PER_ROUND - uint256 pickedKeyCount = LibUint256.min( - LibUint256.min( - operators[selectedOperatorIndex].limit - - (operators[selectedOperatorIndex].funded + operators[selectedOperatorIndex].picked), - MAX_VALIDATOR_ATTRIBUTION_PER_ROUND - ), - _count - ); - - // we update the cached picked amount - operators[selectedOperatorIndex].picked += uint32(pickedKeyCount); - - // we update the requested amount count - _count -= pickedKeyCount; } + // Operator not found in fundable list + revert InactiveOperator(_operatorIndex); } /// @notice Internal utility to get the count of active validators during the exit selection process diff --git a/contracts/src/River.1.sol b/contracts/src/River.1.sol index 3d606f20..2cfc89ff 100644 --- a/contracts/src/River.1.sol +++ b/contracts/src/River.1.sol @@ -320,16 +320,16 @@ contract RiverV1 is } } - /// @notice Overridden handler called whenever a deposit to the consensus layer is made. Should retrieve _requestedAmount or lower keys - /// @param _requestedAmount Amount of keys required. Contract is expected to send _requestedAmount or lower. + /// @notice Overridden handler called whenever a deposit to the consensus layer is made based on node operator allocations. + /// @param _allocations Node operator allocations /// @return publicKeys Array of fundable public keys /// @return signatures Array of signatures linked to the public keys - function _getNextValidators(uint256 _requestedAmount) + function _getNextValidators(IOperatorsRegistryV1.OperatorAllocation[] memory _allocations) internal override returns (bytes[] memory publicKeys, bytes[] memory signatures) { - return IOperatorsRegistryV1(OperatorsRegistryAddress.get()).pickNextValidatorsToDeposit(_requestedAmount); + return IOperatorsRegistryV1(OperatorsRegistryAddress.get()).pickNextValidatorsToDeposit(_allocations); } /// @notice Overridden handler to pull funds from the execution layer fee recipient to River and return the delta in the balance diff --git a/contracts/src/components/ConsensusLayerDepositManager.1.sol b/contracts/src/components/ConsensusLayerDepositManager.1.sol index 6563a865..e508ac5f 100644 --- a/contracts/src/components/ConsensusLayerDepositManager.1.sol +++ b/contracts/src/components/ConsensusLayerDepositManager.1.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.20; import "../interfaces/components/IConsensusLayerDepositManager.1.sol"; +import "../interfaces/IOperatorRegistry.1.sol"; import "../interfaces/IDepositContract.sol"; import "../libraries/LibBytes.sol"; @@ -20,7 +21,7 @@ import "../state/river/KeeperAddress.sol"; /// @notice Whenever a deposit to the consensus layer is requested, this contract computed the amount of keys /// @notice that could be deposited depending on the amount available in the contract. It then tries to retrieve /// @notice validator keys by calling its internal virtual method _getNextValidators. This method should be -/// @notice overridden by the implementing contract to provide [0; _keyCount] keys when invoked. +/// @notice overridden by the implementing contract to provide keys based on the allocation when invoked. abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManagerV1 { /// @notice Size of a BLS Public key in bytes uint256 public constant PUBLIC_KEY_LENGTH = 48; @@ -37,10 +38,12 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage /// @param newCommittedBalance The new committed balance value function _setCommittedBalance(uint256 newCommittedBalance) internal virtual; - /// @notice Internal helper to retrieve validator keys ready to be funded + /// @notice Internal helper to retrieve validator keys based on node operator allocations /// @dev Must be overridden - /// @param _keyCount The amount of keys (or less) to return. - function _getNextValidators(uint256 _keyCount) + /// @param _allocations Node operator allocations + /// @return publicKeys An array of fundable public keys + /// @return signatures An array of signatures linked to the public keys + function _getNextValidators(IOperatorsRegistryV1.OperatorAllocation[] memory _allocations) internal virtual returns (bytes[] memory publicKeys, bytes[] memory signatures); @@ -88,7 +91,10 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage } /// @inheritdoc IConsensusLayerDepositManagerV1 - function depositToConsensusLayerWithDepositRoot(uint256 _maxCount, bytes32 _depositRoot) external { + function depositToConsensusLayerWithDepositRoot( + IOperatorsRegistryV1.OperatorAllocation[] calldata _allocations, + bytes32 _depositRoot + ) external { if (msg.sender != KeeperAddress.get()) { revert OnlyKeeper(); } @@ -98,15 +104,23 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage } uint256 committedBalance = CommittedBalance.get(); - uint256 keyToDepositCount = LibUint256.min(committedBalance / DEPOSIT_SIZE, _maxCount); + uint256 maxDepositableCount = committedBalance / DEPOSIT_SIZE; + // Calculate total requested from allocations + uint256 totalRequested = 0; + for (uint256 i = 0; i < _allocations.length; ++i) { + if (_allocations[i].validatorCount == 0) { + revert IOperatorsRegistryV1.AllocationWithZeroValidatorCount(); + } + totalRequested += _allocations[i].validatorCount; + } - if (keyToDepositCount == 0) { - revert NotEnoughFunds(); + // Check if the total requested number of validators exceeds the maximum number of validators that can be funded + if (totalRequested > maxDepositableCount) { + revert OperatorAllocationsExceedCommittedBalance(); } - // it's up to the internal overriden _getNextValidators method to provide two array of the same - // size for the publicKeys and the signatures - (bytes[] memory publicKeys, bytes[] memory signatures) = _getNextValidators(keyToDepositCount); + // Get validator keys using provided allocations + (bytes[] memory publicKeys, bytes[] memory signatures) = _getNextValidators(_allocations); uint256 receivedPublicKeyCount = publicKeys.length; @@ -114,7 +128,8 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage revert NoAvailableValidatorKeys(); } - if (receivedPublicKeyCount > keyToDepositCount) { + // Check if the number of received public keys is valid + if (receivedPublicKeyCount > maxDepositableCount || receivedPublicKeyCount != totalRequested) { revert InvalidPublicKeyCount(); } @@ -124,11 +139,8 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage revert InvalidWithdrawalCredentials(); } - for (uint256 idx = 0; idx < receivedPublicKeyCount;) { + for (uint256 idx = 0; idx < receivedPublicKeyCount; ++idx) { _depositValidator(publicKeys[idx], signatures[idx], withdrawalCredentials); - unchecked { - ++idx; - } } _setCommittedBalance(committedBalance - DEPOSIT_SIZE * receivedPublicKeyCount); uint256 currentDepositedValidatorCount = DepositedValidatorCount.get(); diff --git a/contracts/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index 0283e12d..ffe763cf 100644 --- a/contracts/src/interfaces/IOperatorRegistry.1.sol +++ b/contracts/src/interfaces/IOperatorRegistry.1.sol @@ -7,6 +7,13 @@ import "../state/operatorsRegistry/Operators.2.sol"; /// @author Alluvial Finance Inc. /// @notice This interface exposes methods to handle the list of operators and their keys interface IOperatorsRegistryV1 { + /// @notice Structure representing an operator allocation for deposits or exits + /// @param operatorIndex The index of the operator + /// @param validatorCount The number of validators to deposit/exit for this operator + struct OperatorAllocation { + uint256 operatorIndex; + uint256 validatorCount; + } /// @notice A new operator has been added to the registry /// @param index The operator index /// @param name The operator display name @@ -159,6 +166,15 @@ interface IOperatorsRegistryV1 { /// @notice The provided list of operators is not in increasing order error UnorderedOperatorList(); + /// @notice Thrown when an invalid operator allocation is provided + /// @param operatorIndex The operator index + /// @param requested The requested count + /// @param available The available count + error OperatorDoesNotHaveEnoughFundableKeys(uint256 operatorIndex, uint256 requested, uint256 available); + + /// @notice Thrown when an allocation with zero validator count is provided + error AllocationWithZeroValidatorCount(); + /// @notice Thrown when an invalid empty stopped validator array is provided error InvalidEmptyStoppedValidatorCountsArray(); @@ -240,11 +256,11 @@ interface IOperatorsRegistryV1 { view returns (bytes memory publicKey, bytes memory signature, bool funded); - /// @notice Get the next validators that would be funded - /// @param _count Count of validators that would be funded next + /// @notice Validate allocations and retrieve validator keys that will be funded + /// @param _allocations The proposed allocations to validate /// @return publicKeys An array of fundable public keys /// @return signatures An array of signatures linked to the public keys - function getNextValidatorsToDepositFromActiveOperators(uint256 _count) + function getNextValidatorsToDepositFromActiveOperators(OperatorAllocation[] memory _allocations) external view returns (bytes[] memory publicKeys, bytes[] memory signatures); @@ -320,11 +336,11 @@ interface IOperatorsRegistryV1 { /// @param _indexes The indexes of the keys to remove function removeValidators(uint256 _index, uint256[] calldata _indexes) external; - /// @notice Retrieve validator keys based on operator statuses - /// @param _count Max amount of keys requested + /// @notice Retrieve validator keys based on explicit operator allocations + /// @param _allocations Node operator allocations specifying how many validators per operator /// @return publicKeys An array of public keys /// @return signatures An array of signatures linked to the public keys - function pickNextValidatorsToDeposit(uint256 _count) + function pickNextValidatorsToDeposit(OperatorAllocation[] calldata _allocations) external returns (bytes[] memory publicKeys, bytes[] memory signatures); diff --git a/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol b/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol index 7b4df8cd..9a04ea80 100644 --- a/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol +++ b/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol @@ -1,6 +1,8 @@ //SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.20; +import "../IOperatorRegistry.1.sol"; + /// @title Consensys Layer Deposit Manager Interface (v1) /// @author Alluvial Finance Inc. /// @notice This interface exposes methods to handle the interactions with the official deposit contract @@ -18,9 +20,6 @@ interface IConsensusLayerDepositManagerV1 { /// @param newDepositedValidatorCount The new deposited validator count value event SetDepositedValidatorCount(uint256 oldDepositedValidatorCount, uint256 newDepositedValidatorCount); - /// @notice Not enough funds to deposit one validator - error NotEnoughFunds(); - /// @notice The length of the BLS Public key is invalid during deposit error InconsistentPublicKeys(); @@ -33,9 +32,6 @@ interface IConsensusLayerDepositManagerV1 { /// @notice The received count of public keys to deposit is invalid error InvalidPublicKeyCount(); - /// @notice The received count of signatures to deposit is invalid - error InvalidSignatureCount(); - /// @notice The withdrawal credentials value is null error InvalidWithdrawalCredentials(); @@ -48,6 +44,9 @@ interface IConsensusLayerDepositManagerV1 { // @notice Not keeper error OnlyKeeper(); + /// @notice The operator allocations exceed the committed balance + error OperatorAllocationsExceedCommittedBalance(); + /// @notice Returns the amount of ETH not yet committed for deposit /// @return The amount of ETH not yet committed for deposit function getBalanceToDeposit() external view returns (uint256); @@ -68,8 +67,11 @@ interface IConsensusLayerDepositManagerV1 { /// @return The keeper address function getKeeper() external view returns (address); - /// @notice Deposits current balance to the Consensus Layer by batches of 32 ETH - /// @param _maxCount The maximum amount of validator keys to fund + /// @notice Deposits current balance to the Consensus Layer based on explicit operator allocations + /// @param _allocations The operator allocations specifying how many validators per operator /// @param _depositRoot The root of the deposit tree - function depositToConsensusLayerWithDepositRoot(uint256 _maxCount, bytes32 _depositRoot) external; + function depositToConsensusLayerWithDepositRoot( + IOperatorsRegistryV1.OperatorAllocation[] calldata _allocations, + bytes32 _depositRoot + ) external; } diff --git a/contracts/test/Firewall.t.sol b/contracts/test/Firewall.t.sol index 566532ae..ce6e8a5c 100644 --- a/contracts/test/Firewall.t.sol +++ b/contracts/test/Firewall.t.sol @@ -282,26 +282,32 @@ contract FirewallTests is BytesGenerator, Test { vm.stopPrank(); } + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + function testGovernorCannotdepositToConsensusLayerWithDepositRoot() public { - // Assert this by expecting NotEnoughFunds, NOT Unauthorized + // Assert this by expecting OperatorAllocationsExceedCommittedBalance, NOT Unauthorized vm.startPrank(riverGovernorDAO); - vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); - firewalledRiver.depositToConsensusLayerWithDepositRoot(10, bytes32(0)); + vm.expectRevert(abi.encodeWithSignature("OperatorAllocationsExceedCommittedBalance()")); + firewalledRiver.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); vm.stopPrank(); } function testExecutorCannotdepositToConsensusLayerWithDepositRoot() public { - // Assert this by expecting NotEnoughFunds, NOT Unauthorized + // Assert this by expecting OperatorAllocationsExceedCommittedBalance, NOT Unauthorized vm.startPrank(executor); - vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); - firewalledRiver.depositToConsensusLayerWithDepositRoot(10, bytes32(0)); + vm.expectRevert(abi.encodeWithSignature("OperatorAllocationsExceedCommittedBalance()")); + firewalledRiver.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); vm.stopPrank(); } function testRandomCallerCannotdepositToConsensusLayerWithDepositRoot() public { vm.startPrank(joe); vm.expectRevert(unauthJoe); - firewalledRiver.depositToConsensusLayerWithDepositRoot(10, bytes32(0)); + firewalledRiver.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); vm.stopPrank(); } diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 649c4b24..434e265a 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -17,17 +17,6 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { operator.funded = _funded; } - function debugGetNextValidatorsToDepositFromActiveOperators(uint256 _requestedAmount) - external - returns (bytes[] memory publicKeys, bytes[] memory signatures) - { - return _pickNextValidatorsToDepositFromActiveOperators(_requestedAmount); - } - - function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { - return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); - } - function sudoSetKeys(uint256 _operatorIndex, uint32 _keyCount) external { OperatorsV2.setKeys(_operatorIndex, _keyCount); } @@ -41,6 +30,53 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { { _setStoppedValidatorCounts(stoppedValidatorCount, depositedValidatorCount); } + + /// @notice Debug function to simulate deposits by directly updating funded count + /// @param _allocations The operator allocations specifying how many validators per operator + function debugGetNextValidatorsToDepositFromActiveOperators(IOperatorsRegistryV1 + .OperatorAllocation[] memory _allocations) + external + returns (bytes[] memory publicKeys, bytes[] memory signatures) + { + return _pickNextValidatorsToDepositFromActiveOperators(_allocations); + } + + /// @notice Debug function to simulate deposits with equal distribution across all active operators + /// @param _requestedAmount The total number of validators to fund + function debugGetNextValidatorsToDepositFromActiveOperators(uint256 _requestedAmount) + external + returns (bytes[] memory publicKeys, bytes[] memory signatures) + { + uint256 operatorCount = OperatorsV2.getCount(); + if (operatorCount == 0) return (publicKeys, signatures); + + uint256 perOperator = _requestedAmount / operatorCount; + uint256 remainder = _requestedAmount % operatorCount; + + for (uint256 i = 0; i < operatorCount; ++i) { + OperatorsV2.Operator storage operator = OperatorsV2.get(i); + if (!operator.active) continue; + + uint256 toFund = perOperator + (i < remainder ? 1 : 0); + uint256 fundableKeys = operator.limit - operator.funded; + toFund = toFund > fundableKeys ? fundableKeys : toFund; + + if (toFund == 0) continue; + + (bytes[] memory _publicKeys, bytes[] memory _signatures) = ValidatorKeys.getKeys(i, operator.funded, toFund); + emit FundedValidatorKeys(i, _publicKeys, false); + publicKeys = _concatenateByteArrays(publicKeys, _publicKeys); + signatures = _concatenateByteArrays(signatures, _signatures); + operator.funded += uint32(toFund); + } + + return (publicKeys, signatures); + } + + /// @notice Debug function to simulate exit requests + function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { + return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); + } } contract RiverMock { @@ -109,6 +145,30 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.initOperatorsRegistryV1(admin, river); } + function _createAllocation(uint256 opIndex, uint256 count) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndex, validatorCount: count}); + return allocations; + } + + function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](opIndexes.length); + for (uint256 i = 0; i < opIndexes.length; ++i) { + allocations[i] = + IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndexes[i], validatorCount: counts[i]}); + } + return allocations; + } + function testInitializeTwice() public { vm.expectRevert(abi.encodeWithSignature("InvalidInitialization(uint256,uint256)", 0, 1)); operatorsRegistry.initOperatorsRegistryV1(admin, river); @@ -563,7 +623,8 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator vm.stopPrank(); vm.startPrank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(10); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(index, 10)); vm.stopPrank(); assert(publicKeys.length == 10); assert(keccak256(publicKeys[0]) == keccak256(LibBytes.slice(tenKeys, 0, 48))); @@ -591,7 +652,17 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator vm.stopPrank(); vm.startPrank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(10); + // Request 10 but limit is 5, so should revert with InvalidOperatorAllocation + vm.expectRevert( + abi.encodeWithSignature("OperatorDoesNotHaveEnoughFundableKeys(uint256,uint256,uint256)", index, 10, 5) + ); + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(index, 10)); + vm.stopPrank(); + + // Request within limit + vm.startPrank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(index, 5)); vm.stopPrank(); assert(publicKeys.length == 5); assert(keccak256(publicKeys[0]) == keccak256(LibBytes.slice(tenKeys, 0, 48))); @@ -641,8 +712,17 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator limits[2] = 50; vm.prank(admin); operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Create allocation for 2 validators from each of 3 operators = 6 total + uint32[] memory allocationCounts = new uint32[](3); + allocationCounts[0] = 2; + allocationCounts[1] = 2; + allocationCounts[2] = 2; + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = _createMultiAllocation(indexes, allocationCounts); + vm.prank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(6); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(allocation); assert(publicKeys.length == 6); assert(signatures.length == 6); @@ -650,7 +730,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); assert(op.limit == 50); - assert(op.funded == 5); + assert(op.funded == 2); assert(op.keys == 50); assert(op.requestedExits == 0); } @@ -658,7 +738,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(1); assert(op.limit == 50); - assert(op.funded == 1); + assert(op.funded == 2); assert(op.keys == 50); assert(op.requestedExits == 0); } @@ -666,12 +746,14 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(2); assert(op.limit == 50); - assert(op.funded == 0); + assert(op.funded == 2); assert(op.keys == 50); assert(op.requestedExits == 0); } + + // Second allocation: 2 more from each = 6 total vm.prank(river); - (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(6); + (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation); assert(publicKeys.length == 6); assert(signatures.length == 6); @@ -679,7 +761,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); assert(op.limit == 50); - assert(op.funded == 5); + assert(op.funded == 4); assert(op.keys == 50); assert(op.requestedExits == 0); } @@ -687,7 +769,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(1); assert(op.limit == 50); - assert(op.funded == 2); + assert(op.funded == 4); assert(op.keys == 50); assert(op.requestedExits == 0); } @@ -695,29 +777,35 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(2); assert(op.limit == 50); - assert(op.funded == 5); + assert(op.funded == 4); assert(op.keys == 50); assert(op.requestedExits == 0); } + // Third allocation: 20 from each operator = 60 total + allocationCounts[0] = 20; + allocationCounts[1] = 20; + allocationCounts[2] = 20; + IOperatorsRegistryV1.OperatorAllocation[] memory largeAllocation = + _createMultiAllocation(indexes, allocationCounts); + vm.prank(river); - (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(64); + (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(largeAllocation); - assert(publicKeys.length == 64); - assert(signatures.length == 64); + assert(publicKeys.length == 60); + assert(signatures.length == 60); { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); assert(op.limit == 50); - assert(op.funded == 25); + assert(op.funded == 24); assert(op.keys == 50); assert(op.requestedExits == 0); } { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(1); - assert(op.limit == 50); - assert(op.funded == 26); + assert(op.funded == 24); assert(op.keys == 50); assert(op.requestedExits == 0); } @@ -725,16 +813,23 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(2); assert(op.limit == 50); - assert(op.funded == 25); + assert(op.funded == 24); assert(op.keys == 50); assert(op.requestedExits == 0); } + // Fourth allocation: remaining validators (26 from each = 78 total) + allocationCounts[0] = 26; + allocationCounts[1] = 26; + allocationCounts[2] = 26; + IOperatorsRegistryV1.OperatorAllocation[] memory finalAllocation = + _createMultiAllocation(indexes, allocationCounts); + vm.prank(river); - (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(74); + (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(finalAllocation); - assert(publicKeys.length == 74); - assert(signatures.length == 74); + assert(publicKeys.length == 78); + assert(signatures.length == 78); { OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); @@ -798,15 +893,19 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator } function testGetKeysAsRiverNoKeys() public { + // Create an allocation for an operator that doesn't exist or has no keys + // This should succeed but return empty arrays since count is 0 for non-existent operators + IOperatorsRegistryV1.OperatorAllocation[] memory emptyAllocation = + new IOperatorsRegistryV1.OperatorAllocation[](0); vm.startPrank(river); - (bytes[] memory publicKeys,) = operatorsRegistry.pickNextValidatorsToDeposit(10); + (bytes[] memory publicKeys,) = operatorsRegistry.pickNextValidatorsToDeposit(emptyAllocation); vm.stopPrank(); assert(publicKeys.length == 0); } function testGetKeysAsUnauthorized() public { vm.expectRevert(abi.encodeWithSignature("Unauthorized(address)", address(this))); - operatorsRegistry.pickNextValidatorsToDeposit(10); + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(0, 10)); } function testAddValidatorsAsAdmin(bytes32 _name, uint256 _firstAddressSalt) public { @@ -1345,6 +1444,20 @@ contract OperatorsRegistryV1TestDistribution is Test { bytes32 salt = bytes32(0); + function _createExitAllocation(uint256[] memory opIndexes, uint32[] memory counts) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](opIndexes.length); + for (uint256 i = 0; i < opIndexes.length; ++i) { + allocations[i] = + IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndexes[i], validatorCount: counts[i]}); + } + return allocations; + } + function genBytes(uint256 len) internal returns (bytes memory) { bytes memory res = ""; while (res.length < len) { @@ -1475,9 +1588,15 @@ contract OperatorsRegistryV1TestDistribution is Test { ), false ); + uint32[] memory allocCounts = new uint32[](5); + allocCounts[0] = 10; + allocCounts[1] = 10; + allocCounts[2] = 10; + allocCounts[3] = 10; + allocCounts[4] = 10; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(50); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); assert(publicKeys.length == 50); assert(signatures.length == 50); @@ -1539,9 +1658,15 @@ contract OperatorsRegistryV1TestDistribution is Test { ), false ); + uint32[] memory allocCounts2 = new uint32[](5); + allocCounts2[0] = 40; + allocCounts2[1] = 40; + allocCounts2[2] = 40; + allocCounts2[3] = 40; + allocCounts2[4] = 40; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(200); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts2)); assert(publicKeys.length == 200); assert(signatures.length == 200); @@ -1554,6 +1679,45 @@ contract OperatorsRegistryV1TestDistribution is Test { } } + function testDepositDistributionWithZeroCountAllocationFails() external { + // Setup: add validators to operators + bytes[] memory rawKeys = new bytes[](3); + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + rawKeys[2] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + operatorsRegistry.addValidators(2, 10, rawKeys[2]); + vm.stopPrank(); + + // Set limits for all operators + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + + uint256[] memory operators = new uint256[](3); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with operator 1 having validatorCount = 0 + // This should revert with AllocationWithZeroValidatorCount + uint32[] memory allocCounts = new uint32[](3); + allocCounts[0] = 5; // operator 0 gets 5 validators + allocCounts[1] = 0; // operator 1 gets 0 validators (should cause revert) + allocCounts[2] = 3; // operator 2 gets 3 validators + + vm.expectRevert(abi.encodeWithSelector(IOperatorsRegistryV1.AllocationWithZeroValidatorCount.selector)); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + } + function testInactiveDepositDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); @@ -1564,22 +1728,32 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.stopPrank(); - uint32[] memory limits = new uint32[](5); + uint32[] memory limits = new uint32[](3); limits[0] = 50; limits[1] = 50; limits[2] = 50; - limits[3] = 50; - limits[4] = 50; - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; + uint256[] memory activeOperators = new uint256[](3); + activeOperators[0] = 0; + activeOperators[1] = 2; + activeOperators[2] = 4; + + uint256[] memory allOperators = new uint256[](5); + allOperators[0] = 0; + allOperators[1] = 1; + allOperators[2] = 2; + allOperators[3] = 3; + allOperators[4] = 4; + + uint32[] memory allLimits = new uint32[](5); + allLimits[0] = 50; + allLimits[1] = 50; + allLimits[2] = 50; + allLimits[3] = 50; + allLimits[4] = 50; vm.startPrank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); + operatorsRegistry.setOperatorLimits(allOperators, allLimits, block.number); operatorsRegistry.setOperatorStatus(1, false); operatorsRegistry.setOperatorStatus(3, false); vm.stopPrank(); @@ -1587,7 +1761,7 @@ contract OperatorsRegistryV1TestDistribution is Test { { (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(250); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(activeOperators, limits)); assert(publicKeys.length == 150); assert(signatures.length == 150); @@ -1624,8 +1798,14 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); vm.stopPrank(); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(75); + { + uint32[] memory allocCounts = new uint32[](3); + allocCounts[0] = 25; + allocCounts[1] = 25; + allocCounts[2] = 25; + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + } assert(operatorsRegistry.getOperator(0).funded == 25); assert(operatorsRegistry.getOperator(1).funded == 0); assert(operatorsRegistry.getOperator(2).funded == 25); @@ -1657,9 +1837,21 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.stopPrank(); { + uint256[] memory allOps = new uint256[](5); + allOps[0] = 0; + allOps[1] = 1; + allOps[2] = 2; + allOps[3] = 3; + allOps[4] = 4; + uint32[] memory allocCounts = new uint32[](5); + allocCounts[0] = 10; + allocCounts[1] = 10; + allocCounts[2] = 10; + allocCounts[3] = 10; + allocCounts[4] = 10; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(50); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(allOps, allocCounts)); assert(publicKeys.length == 50); assert(signatures.length == 50); @@ -1698,8 +1890,16 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(50); + { + uint32[] memory allocCounts = new uint32[](5); + allocCounts[0] = 10; + allocCounts[1] = 10; + allocCounts[2] = 10; + allocCounts[3] = 10; + allocCounts[4] = 10; + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + } assert(operatorsRegistry.getOperator(0).funded == 10); assert(operatorsRegistry.getOperator(1).funded == 10); assert(operatorsRegistry.getOperator(2).funded == 10); @@ -1736,8 +1936,16 @@ contract OperatorsRegistryV1TestDistribution is Test { OperatorsRegistryInitializableV1(address(operatorsRegistry)) .sudoStoppedValidatorCounts(stoppedValidatorCounts, 47); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(50); + { + uint32[] memory allocCounts = new uint32[](2); + uint256[] memory alloOperators = new uint256[](2); + alloOperators[0] = 1; + alloOperators[1] = 3; + allocCounts[0] = 25; + allocCounts[1] = 25; + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(alloOperators, allocCounts)); + } assert(operatorsRegistry.getOperator(0).funded == 10); assert(operatorsRegistry.getOperator(1).funded == 35); assert(operatorsRegistry.getOperator(2).funded == 10); @@ -1773,7 +1981,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -1841,7 +2049,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -1930,7 +2138,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -1982,7 +2190,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2117,7 +2325,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2207,10 +2415,10 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - } - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(50); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + } assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 0); assert(operatorsRegistry.getOperator(2).funded == 0); @@ -2255,10 +2463,10 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - } - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(100); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + } vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(1, 25); @@ -2303,7 +2511,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2359,7 +2567,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 40); assert(operatorsRegistry.getOperator(2).funded == 30); @@ -2415,7 +2623,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(250); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2470,7 +2678,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(160); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 40); assert(operatorsRegistry.getOperator(2).funded == 30); @@ -2572,7 +2780,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(sum); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2646,7 +2854,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(sum); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2725,7 +2933,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(sum); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); { uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2805,7 +3013,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(sum); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); { uint32[] memory stoppedValidatorCount = new uint32[](5); @@ -2890,7 +3098,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(sum); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2940,7 +3148,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(100); + .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); stoppedValidatorCount[1] = 10; @@ -3007,30 +3215,214 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); + // Create valid allocation requesting exactly 10 validators from each operator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](5); + for (uint256 i = 0; i < 5; ++i) { + allocation[i] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: i, validatorCount: 10}); + } + (bytes[] memory publicKeys, bytes[] memory signatures) = - operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(51); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + // Verify keys are returned (50 total = 10 per operator * 5 operators) assert(publicKeys.length == 50); assert(signatures.length == 50); + } - bytes memory receivedKeys; - bytes memory originalKeys; - for (uint256 i; i < 50; i++) { - receivedKeys = bytes.concat(receivedKeys, bytes.concat(publicKeys[i], signatures[i])); - } - for (uint256 i; i < 5; i++) { - originalKeys = bytes.concat(originalKeys, rawKeys[i]); - } + function testGetNextValidatorsToDepositRevertsWhenExceedingLimit() public { + bytes[] memory rawKeys = new bytes[](5); + + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + rawKeys[2] = genBytes((48 + 96) * 10); + rawKeys[3] = genBytes((48 + 96) * 10); + rawKeys[4] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + operatorsRegistry.addValidators(2, 10, rawKeys[2]); + operatorsRegistry.addValidators(3, 10, rawKeys[3]); + operatorsRegistry.addValidators(4, 10, rawKeys[4]); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + limits[3] = 10; + limits[4] = 10; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); - assert(keccak256(receivedKeys) == keccak256(originalKeys)); + // Create allocation requesting 11 validators from first operator (exceeds limit of 10) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 11}); + + vm.expectRevert( + abi.encodeWithSignature("OperatorDoesNotHaveEnoughFundableKeys(uint256,uint256,uint256)", 0, 11, 10) + ); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); } - function testGetNextValidatorsToDepositFromActiveOperatorsForNoOperators() public { + function testGetNextValidatorsToDepositRevertsWhenOperatorInactive() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Deactivate the operator + operatorsRegistry.setOperatorStatus(0, false); + vm.stopPrank(); + + // Create allocation for inactive operator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + (bytes[] memory publicKeys, bytes[] memory signatures) = - operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(5); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); assert(publicKeys.length == 0); assert(signatures.length == 0); } + function testGetNextValidatorsToDepositForNoOperators() public { + // Create an allocation with no operators + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](0); + + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + assert(publicKeys.length == 0); + assert(signatures.length == 0); + } + + function testGetNextValidatorsToDepositRevertsDuplicateOperatorIndex() public { + bytes[] memory rawKeys = new bytes[](2); + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 10; + limits[1] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with duplicate operator index + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + allocation[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); // Duplicate! + + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + } + + function testGetNextValidatorsToDepositRevertsUnorderedOperatorIndex() public { + bytes[] memory rawKeys = new bytes[](2); + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 10; + limits[1] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with unordered operator indices (1 before 0) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 5}); + allocation[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); // Wrong order! + + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + } + + function testPickNextValidatorsToDepositRevertsDuplicateOperatorIndex() public { + bytes[] memory rawKeys = new bytes[](2); + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 10; + limits[1] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with duplicate operator index + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + allocation[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); // Duplicate! + + vm.prank(river); + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); + } + + function testPickNextValidatorsToDepositRevertsUnorderedOperatorIndex() public { + bytes[] memory rawKeys = new bytes[](2); + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 10; + limits[1] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with unordered operator indices (1 before 0) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 5}); + allocation[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); // Wrong order! + + vm.prank(river); + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); + } + function testVersion() external { assertEq(operatorsRegistry.version(), "1.2.1"); } diff --git a/contracts/test/River.1.t.sol b/contracts/test/River.1.t.sol index f3af2ce1..a20e8950 100644 --- a/contracts/test/River.1.t.sol +++ b/contracts/test/River.1.t.sol @@ -47,6 +47,47 @@ abstract contract RiverV1TestBase is Test, BytesGenerator { AllowlistV1 internal allowlist; OperatorsRegistryWithOverridesV1 internal operatorsRegistry; + function _createAllocation(uint256 opIndex, uint256 count) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndex, validatorCount: count}); + return allocations; + } + + function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + require(opIndexes.length == counts.length, "InvalidAllocationLengths"); + + // First pass: count non-zero allocations + uint256 nonZeroCount = 0; + for (uint256 i = 0; i < counts.length; ++i) { + if (counts[i] > 0) { + ++nonZeroCount; + } + } + + // Allocate array with exact size needed + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](nonZeroCount); + + // Second pass: fill only non-zero allocations + uint256 idx = 0; + for (uint256 i = 0; i < opIndexes.length; ++i) { + if (counts[i] > 0) { + allocations[idx] = + IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndexes[i], validatorCount: counts[i]}); + ++idx; + } + } + return allocations; + } + address internal admin; address internal newAdmin; address internal denier; @@ -559,10 +600,17 @@ contract RiverV1Tests is RiverV1TestBase { river.debug_moveDepositToCommitted(); + // Create allocation for 17 validators from each operator = 34 total + uint256[] memory indexes = new uint256[](2); + indexes[0] = operatorOneIndex; + indexes[1] = operatorTwoIndex; + uint32[] memory counts = new uint32[](2); + counts[0] = 17; + counts[1] = 17; + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = _createMultiAllocation(indexes, counts); + vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(17, bytes32(0)); - vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(17, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(allocation, bytes32(0)); OperatorsV2.Operator memory op1 = operatorsRegistry.getOperator(operatorOneIndex); OperatorsV2.Operator memory op2 = operatorsRegistry.getOperator(operatorTwoIndex); @@ -598,10 +646,17 @@ contract RiverV1Tests is RiverV1TestBase { river.debug_moveDepositToCommitted(); + // Create allocation for 17 validators from each operator = 34 total + uint256[] memory indexes = new uint256[](2); + indexes[0] = operatorOneIndex; + indexes[1] = operatorTwoIndex; + uint32[] memory counts = new uint32[](2); + counts[0] = 17; + counts[1] = 17; + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = _createMultiAllocation(indexes, counts); + vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(17, bytes32(0)); - vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(17, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(allocation, bytes32(0)); OperatorsV2.Operator memory op1 = operatorsRegistry.getOperator(operatorOneIndex); OperatorsV2.Operator memory op2 = operatorsRegistry.getOperator(operatorTwoIndex); @@ -694,10 +749,17 @@ contract RiverV1Tests is RiverV1TestBase { river.debug_moveDepositToCommitted(); + // Create allocation for 17 validators from each operator = 34 total + uint256[] memory indexes = new uint256[](2); + indexes[0] = operatorOneIndex; + indexes[1] = operatorTwoIndex; + uint32[] memory counts = new uint32[](2); + counts[0] = 17; + counts[1] = 17; + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = _createMultiAllocation(indexes, counts); + vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(17, bytes32(0)); - vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(17, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(allocation, bytes32(0)); OperatorsV2.Operator memory op1 = operatorsRegistry.getOperator(operatorOneIndex); OperatorsV2.Operator memory op2 = operatorsRegistry.getOperator(operatorTwoIndex); @@ -741,12 +803,17 @@ contract RiverV1Tests is RiverV1TestBase { river.debug_moveDepositToCommitted(); + // Create allocation for 17 validators from each operator = 34 total + uint256[] memory indexes = new uint256[](2); + indexes[0] = operatorOneIndex; + indexes[1] = operatorTwoIndex; + uint32[] memory counts = new uint32[](2); + counts[0] = 17; + counts[1] = 17; + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = _createMultiAllocation(indexes, counts); + vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(1, bytes32(0)); - vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(2, bytes32(0)); - vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(31, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(allocation, bytes32(0)); OperatorsV2.Operator memory op1 = operatorsRegistry.getOperator(operatorOneIndex); OperatorsV2.Operator memory op2 = operatorsRegistry.getOperator(operatorTwoIndex); @@ -782,15 +849,19 @@ contract RiverV1Tests is RiverV1TestBase { river.debug_moveDepositToCommitted(); + // First deposit: 20 validators from operator 1 vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(20, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(_createAllocation(operatorOneIndex, 20), bytes32(0)); + uint32[] memory stoppedCounts = new uint32[](3); stoppedCounts[0] = 10; stoppedCounts[1] = 10; stoppedCounts[2] = 0; operatorsRegistry.sudoStoppedValidatorCounts(stoppedCounts, 20); + + // Second deposit: 10 validators from operator 2 vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(10, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(_createAllocation(operatorTwoIndex, 10), bytes32(0)); OperatorsV2.Operator memory op1 = operatorsRegistry.getOperator(operatorOneIndex); OperatorsV2.Operator memory op2 = operatorsRegistry.getOperator(operatorTwoIndex); @@ -938,6 +1009,10 @@ contract RiverV1TestsReport_HEAVY_FUZZING is RiverV1TestBase { operatorCount = bound(_salt, 1, 100); _salt = _next(_salt); + // Arrays to store operator info for allocation + uint256[] memory operatorIndices = new uint256[](operatorCount); + uint32[] memory operatorKeyCounts = new uint32[](operatorCount); + uint256 rest = depositCount % operatorCount; for (uint256 idx = 0; idx < operatorCount; ++idx) { address operatorAddress = address(uint160(_salt)); @@ -947,11 +1022,13 @@ contract RiverV1TestsReport_HEAVY_FUZZING is RiverV1TestBase { vm.prank(admin); uint256 operatorIndex = operatorsRegistry.addOperator(operatorName, operatorAddress); + operatorIndices[idx] = operatorIndex; uint256 operatorKeyCount = (depositCount / operatorCount) + (rest > 0 ? 1 : 0); if (rest > 0) { --rest; } + operatorKeyCounts[idx] = uint32(operatorKeyCount); if (operatorKeyCount > 0) { bytes memory operatorKeys = genBytes((48 + 96) * operatorKeyCount); @@ -968,8 +1045,12 @@ contract RiverV1TestsReport_HEAVY_FUZZING is RiverV1TestBase { } } + // Create allocation from collected operator data + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = + _createMultiAllocation(operatorIndices, operatorKeyCounts); + vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(depositCount, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(allocation, bytes32(0)); _newSalt = _salt; } @@ -1763,8 +1844,9 @@ contract RiverV1TestsReport_HEAVY_FUZZING is RiverV1TestBase { river.debug_moveDepositToCommitted(); + // Create allocation for this single operator vm.prank(admin); - river.depositToConsensusLayerWithDepositRoot(count, bytes32(0)); + river.depositToConsensusLayerWithDepositRoot(_createAllocation(operatorIndex, uint32(count)), bytes32(0)); return _salt; } diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index d6b94a3f..b698442e 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -35,8 +35,17 @@ contract ConsensusLayerDepositManagerV1ExposeInitializer is ConsensusLayerDeposi bytes public _signatures = hex"6e93b287f9972d6e4bb7b9b7bdf75e2f3190b61dff0699d9708ee2a6e08f0ce1436b3f0213c1d7e0168cd1b221326b917e0dba509208bf586923ccc53e30b9bc697834508a4c54cd4f097f2c8c5d1b7b3c829fdc326f8df92aae75f008099e1e0324e6ea8734ab375bc33000ab02c63423c3dec20823ac27cadc1e393fa1f15774e52c6a5194dd9136f253b1dc8e0cf9f1a9eec02517d923af4f242e2215d4f82d2bfb657e666f24e5c5f8e6c9636250c0e8f2c20ddd91eda71d1ef5896dbc0fd84508f71958ab19b047030cee1911d55194e38051111021e0710e0be25c3f878ba11c7db118b06a6fc04570cba519c1aa4184693f024bc0e02019dfb62dacab8a2b1127d1b03645ed6377717cbd099aab8d6a5bef2be1aa8e0bb7e2565c8eddfa91b72ae014adb0a47a272d1aedd5920a2ec2f788fe76852b45961d959fdb627329326352f8f3e73bb758022265174af7bc6e3b8ef19f173244735f68789d0f6a34de6da1e22142478205388e8b9db291e01227aa5e4e7173aa11624341b31a202ffade6b5418099dd583708c1fb95525bbfa87b1d08455b640ce25cf322b00471f8dc813dbcd8b82c20e9d07c6215e86237d94ed6f81c7a7ffce0180c128be4f036203e9acfa713d41609a654de0a56a1689da6dcd3950dfd1e3f36987cca569ba947c97b205e34f8ed2dd87b4e29a822676457121ff48ee8bb4dd0b7200093883f6cde4edf1026abc5bc5692dbbfb2197fb4cfbac4eecc99b7956a4dab19cc74db50cf83ff35e880ef58457d3a5b444a17c072ea617ff28cf7bba2657f8ef118a8e6f65453548aafea8c8b88a0df7dbeeaecff69d05ff0dfc55fb97eb94b05b7d7aa748f5aaf6fe38aa6183f400d65e0152004780a089449a5bd77e04b7bd0682c67f5c4fd12bf56b6b31ec3eccfe104f8f64c8b9d23375e0078ba8fe6253037a8a2171682301d5463ce24b4e920af83fd009b6214450382309a143332e8dfa05a95dfa686a630b95b80cfd9b42d33cc3de7f5708dd67714192a14ca814a1f3cc4b4932c36831674ee8ba3a58f12643c1b4bf1e00370290ac4d5e994410d69bad8c691efaf5b6e8fe8331882f7dc304d8ccb6bd9d6079c1698dbdef47996c937046157498db082443ddd33f61e1abb204f12d553b25ea1d773812f701a3c9b36c5909c3b9ebd18d2ba1b8a2daeae36a2811a59bbae1d334fde54e07eac5770172c36d50d821fb181c97bb00a9684a904a2fc8c9c520e730fca4751b4f0d266dc33ddbb7e8ea065ccc47a7dbea61a185ab2413917a039e505e85e2f781eeef96658b94a07f9662ff3e6c8728de755c7a305f975ae8772c8b75468ad30a5467"; - function _getNextValidators(uint256 _amount) internal view override returns (bytes[] memory, bytes[] memory) { - uint256 amount = _amount > 10 ? 10 : _amount; + function _getNextValidators(IOperatorsRegistryV1.OperatorAllocation[] memory _allocations) + internal + view + override + returns (bytes[] memory, bytes[] memory) + { + uint256 totalRequested = 0; + for (uint256 i = 0; i < _allocations.length; ++i) { + totalRequested += _allocations[i].validatorCount; + } + uint256 amount = totalRequested > 10 ? 10 : totalRequested; bytes[] memory publicKeys = new bytes[](amount); bytes[] memory signatures = new bytes[](amount); @@ -113,12 +122,18 @@ contract ConsensusLayerDepositManagerV1Tests is Test { assert(depositManager.getWithdrawalCredentials() == withdrawalCredentials); } - function testDepositNotEnoughFunds() public { + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + + function testDepositAllocationWithZeroValidatorCount() public { vm.deal(address(depositManager), 31.9 ether); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); - vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(5, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(0), bytes32(0)); } function testDepositTenValidators() public { @@ -126,17 +141,18 @@ contract ConsensusLayerDepositManagerV1Tests is Test { ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); assert(address(depositManager).balance == 320 ether); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(10, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); assert(address(depositManager).balance == 0); } - function testDepositTwentyValidators() public { + function testRequestToDepositMoreThanMaxDepositableCountFailsWithInvalidPublicKeyCount() public { vm.deal(address(depositManager), 640 ether); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); assert(address(depositManager).balance == 640 ether); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(20, bytes32(0)); - assert(address(depositManager).balance == 320 ether); + vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(20), bytes32(0)); + assert(address(depositManager).balance == 640 ether); } } @@ -170,9 +186,18 @@ contract ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest is Consen scenario = _newScenario; } - function _getNextValidators(uint256 _amount) internal view override returns (bytes[] memory, bytes[] memory) { + function _getNextValidators(IOperatorsRegistryV1.OperatorAllocation[] memory _allocations) + internal + view + override + returns (bytes[] memory, bytes[] memory) + { + uint256 totalRequested = 0; + for (uint256 i = 0; i < _allocations.length; ++i) { + totalRequested += _allocations[i].validatorCount; + } if (scenario == 0) { - uint256 amount = _amount > 10 ? 10 : _amount; + uint256 amount = totalRequested > 10 ? 10 : totalRequested; bytes[] memory publicKeys = new bytes[](amount); bytes[] memory signatures = new bytes[](amount); @@ -255,31 +280,37 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + function testInconsistentPublicKey() public { - vm.deal(address(depositManager), 32 ether); + vm.deal(address(depositManager), 5 * 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); - ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(1); - vm.expectRevert(abi.encodeWithSignature("InconsistentPublicKeys()")); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(1); // only returns 1 public key + vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(5, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); } - function testInconsistentSignature() public { - vm.deal(address(depositManager), 32 ether); + function testPublicKeyAndSignatureCountMismatch() public { + vm.deal(address(depositManager), 5 * 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); - ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(2); - vm.expectRevert(abi.encodeWithSignature("InconsistentSignatures()")); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(2); // returns less key signature pairs than expected + vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(5, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); } function testUnavailableKeys() public { - vm.deal(address(depositManager), 32 ether); + vm.deal(address(depositManager), 5 * 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(3); vm.expectRevert(abi.encodeWithSignature("NoAvailableValidatorKeys()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(5, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); } function testInvalidPublicKeyCount() public { @@ -288,7 +319,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(4); vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(5, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } } @@ -310,14 +341,20 @@ contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is Test { LibImplementationUnbricker.unbrick(vm, address(depositManager)); } + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + function testInvalidWithdrawalCredential() public { - vm.deal(address(depositManager), 32 ether); + vm.deal(address(depositManager), 5 * 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(0); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setKeeper(address(0x1)); vm.expectRevert(abi.encodeWithSignature("InvalidWithdrawalCredentials()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(5, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)) .sudoSetWithdrawalCredentials(withdrawalCredentials); } @@ -349,8 +386,17 @@ contract ConsensusLayerDepositManagerV1ValidKeys is ConsensusLayerDepositManager bytes public _signatures = hex"8A1979CC3E8D2897044AA18F99F78569AFC0EF9CF5CA5F9545070CF2D2A2CCD5C328B2B2280A8BA80CC810A46470BFC80D2EAAC53E533E43BA054A00587027BA0BCBA5FAD22355257CEB96B23E45D5746022312FBB7E7EFA8C3AE17C0713B426"; - function _getNextValidators(uint256 _amount) internal view override returns (bytes[] memory, bytes[] memory) { - uint256 amount = _amount > 1 ? 1 : _amount; + function _getNextValidators(IOperatorsRegistryV1.OperatorAllocation[] memory _allocations) + internal + view + override + returns (bytes[] memory, bytes[] memory) + { + uint256 totalRequested = 0; + for (uint256 i = 0; i < _allocations.length; ++i) { + totalRequested += _allocations[i].validatorCount; + } + uint256 amount = totalRequested > 1 ? 1 : totalRequested; bytes[] memory publicKeys = new bytes[](amount); bytes[] memory signatures = new bytes[](amount); @@ -396,6 +442,12 @@ contract ConsensusLayerDepositManagerV1ValidKeysTest is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + function testDepositValidKey() external { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ValidKeys(address(depositManager)).sudoSyncBalance(); @@ -405,7 +457,7 @@ contract ConsensusLayerDepositManagerV1ValidKeysTest is Test { bytes32(uint256(uint160(address(0x1)))) ); vm.startPrank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(1, depositContract.get_deposit_root()); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), depositContract.get_deposit_root()); assert(DepositContractEnhancedMock(address(depositContract)).debug_getLastDepositDataRoot() == depositDataRoot); } @@ -417,7 +469,7 @@ contract ConsensusLayerDepositManagerV1ValidKeysTest is Test { ); vm.startPrank(address(0x1)); vm.expectRevert(abi.encodeWithSignature("InvalidDepositRoot()")); - depositManager.depositToConsensusLayerWithDepositRoot(1, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } } @@ -436,12 +488,18 @@ contract ConsensusLayerDepositManagerV1InvalidDepositContract is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + function testDepositInvalidDepositContract() external { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ValidKeys(address(depositManager)).sudoSyncBalance(); vm.expectRevert(abi.encodeWithSignature("ErrorOnDeposit()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(1, bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } } @@ -466,6 +524,12 @@ contract ConsensusLayerDepositManagerV1KeeperTest is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } + function testDepositValidKeeper() external { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ValidKeys(address(depositManager)).sudoSyncBalance(); @@ -475,7 +539,7 @@ contract ConsensusLayerDepositManagerV1KeeperTest is Test { bytes32(uint256(uint160(address(0x1)))) ); vm.startPrank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(1, depositContract.get_deposit_root()); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), depositContract.get_deposit_root()); assert(DepositContractEnhancedMock(address(depositContract)).debug_getLastDepositDataRoot() == depositDataRoot); } @@ -490,6 +554,6 @@ contract ConsensusLayerDepositManagerV1KeeperTest is Test { bytes32 depositRoot = depositContract.get_deposit_root(); vm.expectRevert(abi.encodeWithSignature("OnlyKeeper()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(1, depositRoot); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), depositRoot); } } From ca84d0edf562c2e3207e2476e28a9026a26b61f4 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 12:26:33 +0000 Subject: [PATCH 02/60] chore: tidy up OperatorsRegistry tests for allocations --- contracts/test/OperatorsRegistry.1.t.sol | 144 +++++++++-------------- 1 file changed, 58 insertions(+), 86 deletions(-) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 434e265a..4b55290c 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -17,6 +17,17 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { operator.funded = _funded; } + function debugGetNextValidatorsToDepositFromActiveOperators(OperatorAllocation[] memory _allocations) + external + returns (bytes[] memory publicKeys, bytes[] memory signatures) + { + return _pickNextValidatorsToDepositFromActiveOperators(_allocations); + } + + function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { + return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); + } + function sudoSetKeys(uint256 _operatorIndex, uint32 _keyCount) external { OperatorsV2.setKeys(_operatorIndex, _keyCount); } @@ -30,53 +41,6 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { { _setStoppedValidatorCounts(stoppedValidatorCount, depositedValidatorCount); } - - /// @notice Debug function to simulate deposits by directly updating funded count - /// @param _allocations The operator allocations specifying how many validators per operator - function debugGetNextValidatorsToDepositFromActiveOperators(IOperatorsRegistryV1 - .OperatorAllocation[] memory _allocations) - external - returns (bytes[] memory publicKeys, bytes[] memory signatures) - { - return _pickNextValidatorsToDepositFromActiveOperators(_allocations); - } - - /// @notice Debug function to simulate deposits with equal distribution across all active operators - /// @param _requestedAmount The total number of validators to fund - function debugGetNextValidatorsToDepositFromActiveOperators(uint256 _requestedAmount) - external - returns (bytes[] memory publicKeys, bytes[] memory signatures) - { - uint256 operatorCount = OperatorsV2.getCount(); - if (operatorCount == 0) return (publicKeys, signatures); - - uint256 perOperator = _requestedAmount / operatorCount; - uint256 remainder = _requestedAmount % operatorCount; - - for (uint256 i = 0; i < operatorCount; ++i) { - OperatorsV2.Operator storage operator = OperatorsV2.get(i); - if (!operator.active) continue; - - uint256 toFund = perOperator + (i < remainder ? 1 : 0); - uint256 fundableKeys = operator.limit - operator.funded; - toFund = toFund > fundableKeys ? fundableKeys : toFund; - - if (toFund == 0) continue; - - (bytes[] memory _publicKeys, bytes[] memory _signatures) = ValidatorKeys.getKeys(i, operator.funded, toFund); - emit FundedValidatorKeys(i, _publicKeys, false); - publicKeys = _concatenateByteArrays(publicKeys, _publicKeys); - signatures = _concatenateByteArrays(signatures, _signatures); - operator.funded += uint32(toFund); - } - - return (publicKeys, signatures); - } - - /// @notice Debug function to simulate exit requests - function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { - return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); - } } contract RiverMock { @@ -1329,7 +1293,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(totalCount); + .debugGetNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); vm.prank(river); for (uint256 idx = 1; idx < len + 1; ++idx) { @@ -1412,7 +1376,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(totalCount); + .debugGetNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); stoppedValidators[0] -= 1; @@ -1444,7 +1408,20 @@ contract OperatorsRegistryV1TestDistribution is Test { bytes32 salt = bytes32(0); - function _createExitAllocation(uint256[] memory opIndexes, uint32[] memory counts) + function genBytes(uint256 len) internal returns (bytes memory) { + bytes memory res = ""; + while (res.length < len) { + salt = keccak256(abi.encodePacked(salt)); + if (len - res.length >= 32) { + res = bytes.concat(res, abi.encode(salt)); + } else { + res = bytes.concat(res, LibBytes.slice(abi.encode(salt), 0, len - res.length)); + } + } + return res; + } + + function _createAllocation(uint256[] memory opIndexes, uint32[] memory counts) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) @@ -1458,17 +1435,12 @@ contract OperatorsRegistryV1TestDistribution is Test { return allocations; } - function genBytes(uint256 len) internal returns (bytes memory) { - bytes memory res = ""; - while (res.length < len) { - salt = keccak256(abi.encodePacked(salt)); - if (len - res.length >= 32) { - res = bytes.concat(res, abi.encode(salt)); - } else { - res = bytes.concat(res, LibBytes.slice(abi.encode(salt), 0, len - res.length)); - } - } - return res; + function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + return _createAllocation(opIndexes, counts); } function setUp() public { @@ -1596,7 +1568,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[4] = 10; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); assert(publicKeys.length == 50); assert(signatures.length == 50); @@ -1666,7 +1638,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts2[4] = 40; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts2)); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts2)); assert(publicKeys.length == 200); assert(signatures.length == 200); @@ -1715,7 +1687,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.expectRevert(abi.encodeWithSelector(IOperatorsRegistryV1.AllocationWithZeroValidatorCount.selector)); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); } function testInactiveDepositDistribution() external { @@ -1761,7 +1733,7 @@ contract OperatorsRegistryV1TestDistribution is Test { { (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(activeOperators, limits)); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(activeOperators, limits)); assert(publicKeys.length == 150); assert(signatures.length == 150); @@ -1804,7 +1776,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[1] = 25; allocCounts[2] = 25; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); } assert(operatorsRegistry.getOperator(0).funded == 25); assert(operatorsRegistry.getOperator(1).funded == 0); @@ -1851,7 +1823,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[4] = 10; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(allOps, allocCounts)); + ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(allOps, allocCounts)); assert(publicKeys.length == 50); assert(signatures.length == 50); @@ -1898,7 +1870,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[3] = 10; allocCounts[4] = 10; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, allocCounts)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); } assert(operatorsRegistry.getOperator(0).funded == 10); assert(operatorsRegistry.getOperator(1).funded == 10); @@ -1944,7 +1916,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[0] = 25; allocCounts[1] = 25; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(alloOperators, allocCounts)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(alloOperators, allocCounts)); } assert(operatorsRegistry.getOperator(0).funded == 10); assert(operatorsRegistry.getOperator(1).funded == 35); @@ -1981,7 +1953,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2049,7 +2021,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2138,7 +2110,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2190,7 +2162,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2325,7 +2297,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2417,7 +2389,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); } assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 0); @@ -2465,7 +2437,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); } vm.expectEmit(true, true, true, true); @@ -2511,7 +2483,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2567,7 +2539,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 40); assert(operatorsRegistry.getOperator(2).funded == 30); @@ -2623,7 +2595,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2678,7 +2650,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 40); assert(operatorsRegistry.getOperator(2).funded == 30); @@ -2780,7 +2752,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2854,7 +2826,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2933,7 +2905,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); { uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -3013,7 +2985,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); { uint32[] memory stoppedValidatorCount = new uint32[](5); @@ -3098,7 +3070,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -3148,7 +3120,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createExitAllocation(operators, limits)); + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); stoppedValidatorCount[1] = 10; From 140ecb40b7cdd8d2e48f0957ee7f0e5ea93f811d Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 12:35:00 +0000 Subject: [PATCH 03/60] feat(deposits): add allocation ordering validation and optimize deposit loop - Validate allocation operator indices are in ascending order - Use unchecked increment in deposit loop for gas savings - Update natspec for getNextValidatorsToDepositFromActiveOperators --- .../src/components/ConsensusLayerDepositManager.1.sol | 8 +++++++- contracts/src/interfaces/IOperatorRegistry.1.sol | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/src/components/ConsensusLayerDepositManager.1.sol b/contracts/src/components/ConsensusLayerDepositManager.1.sol index e508ac5f..90ec2af7 100644 --- a/contracts/src/components/ConsensusLayerDepositManager.1.sol +++ b/contracts/src/components/ConsensusLayerDepositManager.1.sol @@ -108,6 +108,9 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage // Calculate total requested from allocations uint256 totalRequested = 0; for (uint256 i = 0; i < _allocations.length; ++i) { + if (i > 0 && !(_allocations[i].operatorIndex > _allocations[i - 1].operatorIndex)) { + revert IOperatorsRegistryV1.UnorderedOperatorList(); + } if (_allocations[i].validatorCount == 0) { revert IOperatorsRegistryV1.AllocationWithZeroValidatorCount(); } @@ -139,8 +142,11 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage revert InvalidWithdrawalCredentials(); } - for (uint256 idx = 0; idx < receivedPublicKeyCount; ++idx) { + for (uint256 idx = 0; idx < receivedPublicKeyCount;) { _depositValidator(publicKeys[idx], signatures[idx], withdrawalCredentials); + unchecked { + ++idx; + } } _setCommittedBalance(committedBalance - DEPOSIT_SIZE * receivedPublicKeyCount); uint256 currentDepositedValidatorCount = DepositedValidatorCount.get(); diff --git a/contracts/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index ffe763cf..7a3e5235 100644 --- a/contracts/src/interfaces/IOperatorRegistry.1.sol +++ b/contracts/src/interfaces/IOperatorRegistry.1.sol @@ -256,7 +256,7 @@ interface IOperatorsRegistryV1 { view returns (bytes memory publicKey, bytes memory signature, bool funded); - /// @notice Validate allocations and retrieve validator keys that will be funded + /// @notice Get the next validators that would be funded based on the proposed allocations /// @param _allocations The proposed allocations to validate /// @return publicKeys An array of fundable public keys /// @return signatures An array of signatures linked to the public keys From 6f8430bbfba307c335d61dcd53f4254952703d98 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 12:49:34 +0000 Subject: [PATCH 04/60] fix: revert NotEnoughFunds implementation changes + expectation of tests --- .../src/components/ConsensusLayerDepositManager.1.sol | 4 ++++ .../components/IConsensusLayerDepositManager.1.sol | 3 +++ contracts/test/Firewall.t.sol | 8 ++++---- .../test/components/ConsensusLayerDepositManager.1.t.sol | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/src/components/ConsensusLayerDepositManager.1.sol b/contracts/src/components/ConsensusLayerDepositManager.1.sol index 90ec2af7..92b18feb 100644 --- a/contracts/src/components/ConsensusLayerDepositManager.1.sol +++ b/contracts/src/components/ConsensusLayerDepositManager.1.sol @@ -105,6 +105,10 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage uint256 committedBalance = CommittedBalance.get(); uint256 maxDepositableCount = committedBalance / DEPOSIT_SIZE; + + if (maxDepositableCount == 0) { + revert NotEnoughFunds(); + } // Calculate total requested from allocations uint256 totalRequested = 0; for (uint256 i = 0; i < _allocations.length; ++i) { diff --git a/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol b/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol index 9a04ea80..567df8df 100644 --- a/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol +++ b/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol @@ -20,6 +20,9 @@ interface IConsensusLayerDepositManagerV1 { /// @param newDepositedValidatorCount The new deposited validator count value event SetDepositedValidatorCount(uint256 oldDepositedValidatorCount, uint256 newDepositedValidatorCount); + /// @notice Not enough funds to deposit the requested number of validators + error NotEnoughFunds(); + /// @notice The length of the BLS Public key is invalid during deposit error InconsistentPublicKeys(); diff --git a/contracts/test/Firewall.t.sol b/contracts/test/Firewall.t.sol index ce6e8a5c..5462444a 100644 --- a/contracts/test/Firewall.t.sol +++ b/contracts/test/Firewall.t.sol @@ -289,17 +289,17 @@ contract FirewallTests is BytesGenerator, Test { } function testGovernorCannotdepositToConsensusLayerWithDepositRoot() public { - // Assert this by expecting OperatorAllocationsExceedCommittedBalance, NOT Unauthorized + // Assert this by expecting NotEnoughFunds, NOT Unauthorized vm.startPrank(riverGovernorDAO); - vm.expectRevert(abi.encodeWithSignature("OperatorAllocationsExceedCommittedBalance()")); + vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); firewalledRiver.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); vm.stopPrank(); } function testExecutorCannotdepositToConsensusLayerWithDepositRoot() public { - // Assert this by expecting OperatorAllocationsExceedCommittedBalance, NOT Unauthorized + // Assert this by expecting NotEnoughFunds, NOT Unauthorized vm.startPrank(executor); - vm.expectRevert(abi.encodeWithSignature("OperatorAllocationsExceedCommittedBalance()")); + vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); firewalledRiver.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); vm.stopPrank(); } diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index b698442e..25943b5c 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -128,10 +128,10 @@ contract ConsensusLayerDepositManagerV1Tests is Test { return allocations; } - function testDepositAllocationWithZeroValidatorCount() public { + function testDepositAllocationFailsWithNotEnoughFunds() public { vm.deal(address(depositManager), 31.9 ether); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); - vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); vm.prank(address(0x1)); depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(0), bytes32(0)); } From c6ef4a61bfeece081cd60f4c3e9b07ae6335f635 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 13:37:58 +0000 Subject: [PATCH 05/60] style: consistent looping --- contracts/src/OperatorsRegistry.1.sol | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 94f6bc8b..8912b75c 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -218,18 +218,18 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab if (fundableOperatorCount == 0) { return (new bytes[](0), new bytes[](0)); } - uint256 len = _allocations.length; - // First pass: validate ordering and update picked count for each operator + uint256 len = _allocations.length; for (uint256 i = 0; i < len; ++i) { - uint256 operatorIndex = _allocations[i].operatorIndex; - if (i > 0 && !(operatorIndex > _allocations[i - 1].operatorIndex)) { + if (i > 0 && !(_allocations[i].operatorIndex > _allocations[i - 1].operatorIndex)) { revert UnorderedOperatorList(); } if (_allocations[i].validatorCount == 0) { revert AllocationWithZeroValidatorCount(); } - _updateCountOfPickedValidatorsForEachOperator(operators, operatorIndex, _allocations[i].validatorCount); + _updateCountOfPickedValidatorsForEachOperator( + operators, _allocations[i].operatorIndex, _allocations[i].validatorCount + ); } // we loop on all operators for (uint256 idx = 0; idx < fundableOperatorCount; ++idx) { @@ -702,17 +702,16 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab } uint256 len = _allocations.length; - // Iterate over the allocations, validate ordering and update the picked count for each operator for (uint256 i = 0; i < len; ++i) { - uint256 operatorIndex = _allocations[i].operatorIndex; - if (i > 0 && !(operatorIndex > _allocations[i - 1].operatorIndex)) { + if (i > 0 && !(_allocations[i].operatorIndex > _allocations[i - 1].operatorIndex)) { revert UnorderedOperatorList(); } - if (_allocations[i].validatorCount == 0) { revert AllocationWithZeroValidatorCount(); } - _updateCountOfPickedValidatorsForEachOperator(operators, operatorIndex, _allocations[i].validatorCount); + _updateCountOfPickedValidatorsForEachOperator( + operators, _allocations[i].operatorIndex, _allocations[i].validatorCount + ); } // we loop on all operators for (uint256 idx = 0; idx < fundableOperatorCount; ++idx) { @@ -737,7 +736,7 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab // Find the operator in the compacted array by matching .index for (uint256 i = 0; i < operators.length; ++i) { if (operators[i].index == _operatorIndex) { - // we take the smallest value between limit - (funded + picked), _validatorCount + // we take the smallest value between limit - (funded + picked) uint256 availableKeys = operators[i].limit - (operators[i].funded + operators[i].picked); if (_validatorCount > availableKeys) { From 8c0b55df22806fc5a16b88cc0b6e191ebac9cb81 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 13:43:54 +0000 Subject: [PATCH 06/60] fix: natspec comment --- .../interfaces/components/IConsensusLayerDepositManager.1.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol b/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol index 567df8df..a480eed3 100644 --- a/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol +++ b/contracts/src/interfaces/components/IConsensusLayerDepositManager.1.sol @@ -20,7 +20,7 @@ interface IConsensusLayerDepositManagerV1 { /// @param newDepositedValidatorCount The new deposited validator count value event SetDepositedValidatorCount(uint256 oldDepositedValidatorCount, uint256 newDepositedValidatorCount); - /// @notice Not enough funds to deposit the requested number of validators + /// @notice Not enough funds to deposit one validator error NotEnoughFunds(); /// @notice The length of the BLS Public key is invalid during deposit From 94256547280adcd7d300ab32617bf5269dc2cfce Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:06:55 +0000 Subject: [PATCH 07/60] test(deposits): add test for faulty registry returning fewer keys Add scenario 6 to mock that returns half of requested keys and test that InvalidPublicKeyCount() is thrown when registry returns fewer keys than requested. --- .../ConsensusLayerDepositManager.1.t.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index 25943b5c..9e2ed81f 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -248,6 +248,16 @@ contract ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest is Consen signatures[0] = LibBytes.slice(_signatures, 0, 96); signatures[1] = LibBytes.slice(_signatures, 96, 96); return (publicKeys, signatures); + } else if (scenario == 6) { + // Return fewer keys than requested (simulates faulty registry) + uint256 amount = totalRequested / 2; + bytes[] memory publicKeys = new bytes[](amount); + bytes[] memory signatures = new bytes[](amount); + for (uint256 idx = 0; idx < amount; ++idx) { + publicKeys[idx] = LibBytes.slice(_publicKeys, idx * 48, 48); + signatures[idx] = LibBytes.slice(_signatures, idx * 96, 96); + } + return (publicKeys, signatures); } return (new bytes[](0), new bytes[](0)); } @@ -321,6 +331,15 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { vm.prank(address(0x1)); depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } + + function testFaultyRegistryReturnsFewerKeys() public { + vm.deal(address(depositManager), 4 * 32 ether); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(6); // returns half of requested keys + vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(4), bytes32(0)); + } } contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is Test { From 7c2068740027f918902c34c4955f0effa917478a Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:08:58 +0000 Subject: [PATCH 08/60] test(deposits): add test for allocation exceeding committed balance Test that OperatorAllocationsExceedCommittedBalance() is thrown when total requested validators exceed the maximum that can be funded. --- .../components/ConsensusLayerDepositManager.1.t.sol | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index 9e2ed81f..a491e09e 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -340,6 +340,16 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { vm.prank(address(0x1)); depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(4), bytes32(0)); } + + function testAllocationExceedsCommittedBalance() public { + // Fund with only 2 deposits worth of ETH + vm.deal(address(depositManager), 2 * 32 ether); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + // Try to allocate 5 validators when only 2 can be funded + vm.expectRevert(abi.encodeWithSignature("OperatorAllocationsExceedCommittedBalance()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); + } } contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is Test { From 69191ed2e9e8e16421613ccac7658c18973bdd68 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:10:25 +0000 Subject: [PATCH 09/60] style: natspec changes --- .../components/ConsensusLayerDepositManager.1.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/src/components/ConsensusLayerDepositManager.1.sol b/contracts/src/components/ConsensusLayerDepositManager.1.sol index 92b18feb..e6de70b3 100644 --- a/contracts/src/components/ConsensusLayerDepositManager.1.sol +++ b/contracts/src/components/ConsensusLayerDepositManager.1.sol @@ -38,11 +38,11 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage /// @param newCommittedBalance The new committed balance value function _setCommittedBalance(uint256 newCommittedBalance) internal virtual; - /// @notice Internal helper to retrieve validator keys based on node operator allocations + /// @notice Internal helper to retrieve validator keys ready to be funded /// @dev Must be overridden - /// @param _allocations Node operator allocations - /// @return publicKeys An array of fundable public keys - /// @return signatures An array of signatures linked to the public keys + /// @param _allocations Validator allocations + /// @return publicKeys An array of public keys ready to be funded + /// @return signatures An array of signatures ready to be funded function _getNextValidators(IOperatorsRegistryV1.OperatorAllocation[] memory _allocations) internal virtual @@ -126,7 +126,8 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage revert OperatorAllocationsExceedCommittedBalance(); } - // Get validator keys using provided allocations + // it's up to the internal overriden _getNextValidators method to provide two array of the same + // size for the publicKeys and the signatures (bytes[] memory publicKeys, bytes[] memory signatures) = _getNextValidators(_allocations); uint256 receivedPublicKeyCount = publicKeys.length; @@ -135,7 +136,7 @@ abstract contract ConsensusLayerDepositManagerV1 is IConsensusLayerDepositManage revert NoAvailableValidatorKeys(); } - // Check if the number of received public keys is valid + // Check that the received public keys count equals the total requested and does not exceed the maximum number of validators that can be funded in this run if (receivedPublicKeyCount > maxDepositableCount || receivedPublicKeyCount != totalRequested) { revert InvalidPublicKeyCount(); } From d286f80b841964b6435f4efaf19f062688dc0b57 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Thu, 5 Feb 2026 15:22:20 +0100 Subject: [PATCH 10/60] chore: first draft --- contracts/src/OperatorsRegistry.1.sol | 53 ++++++++++++++++--- .../src/interfaces/IOperatorRegistry.1.sol | 15 +++++- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 94f6bc8b..d8a795a2 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -465,15 +465,56 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab } /// @inheritdoc IOperatorsRegistryV1 - function requestValidatorExits(uint256 _count) external { + function requestValidatorExits(OperatorAllocation[] calldata _allocations) external { + uint256 allocationsLength = _allocations.length; + + if (allocationsLength == 0) { + revert InvalidEmptyArray(); + } + + if (msg.sender != IConsensusLayerDepositManagerV1(RiverAddress.get()).getKeeper()) { + revert IConsensusLayerDepositManagerV1.OnlyKeeper(); + } + uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); - uint256 exitRequestsToPerform = LibUint256.min(currentValidatorExitsDemand, _count); - if (exitRequestsToPerform == 0) { - revert NoExitRequestsToPerform(); + uint256 prevOperatorIndex = 0; + uint256 suppliedExitCount = 0; + + // Check that the exits requested do not exceed the funded validator count of the operator + for (uint256 i = 0; i < allocationsLength; ++i) { + uint256 operatorIndex = _allocations[i].operatorIndex; + uint256 count = _allocations[i].validatorCount; + suppliedExitCount += count; + + if (i > 0 && !(operatorIndex > prevOperatorIndex)) { + revert UnorderedOperatorList(); + } + prevOperatorIndex = operatorIndex; + + OperatorsV2.Operator storage operator = OperatorsV2.get(operatorIndex); + if (!operator.active) { + revert InactiveOperator(operatorIndex); + } + if (count > (operator.funded - operator.requestedExits)) { + // Operator has insufficient funded validators + revert ExitsRequestedExceedsFundedCount(operatorIndex, count, operator.funded); + } else { + // Operator has sufficient funded validators + operator.requestedExits += uint32(count); + emit RequestedValidatorExits(operatorIndex, operator.requestedExits); + } + } + + // Check that the exits requested do not exceed the current validator exits demand + if (suppliedExitCount > currentValidatorExitsDemand) { + revert ExitsRequestedExceedsDemand(suppliedExitCount, currentValidatorExitsDemand); } - uint256 savedCurrentValidatorExitsDemand = currentValidatorExitsDemand; - currentValidatorExitsDemand -= _pickNextValidatorsToExitFromActiveOperators(exitRequestsToPerform); + uint256 savedCurrentValidatorExitsDemand = currentValidatorExitsDemand; + currentValidatorExitsDemand -= suppliedExitCount; + + uint256 totalRequestedExitsValue = TotalValidatorExitsRequested.get(); + _setTotalValidatorExitsRequested(totalRequestedExitsValue, totalRequestedExitsValue + suppliedExitCount); _setCurrentValidatorExitsDemand(savedCurrentValidatorExitsDemand, currentValidatorExitsDemand); } diff --git a/contracts/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index 7a3e5235..346844a0 100644 --- a/contracts/src/interfaces/IOperatorRegistry.1.sol +++ b/contracts/src/interfaces/IOperatorRegistry.1.sol @@ -196,6 +196,17 @@ interface IOperatorsRegistryV1 { /// @notice The provided stopped validator count of an operator is above its funded validator count error StoppedValidatorCountAboveFundedCount(uint256 operatorIndex, uint32 stoppedCount, uint32 fundedCount); + /// @notice The provided exit requests exceed the funded validator count of the operator + /// @param operatorIndex The operator index + /// @param requested The requested count + /// @param funded The funded count + error ExitsRequestedExceedsFundedCount(uint256 operatorIndex, uint256 requested, uint256 funded); + + /// @notice The provided exit requests exceed the current exit request demand + /// @param requested The requested count + /// @param demand The demand count + error ExitsRequestedExceedsDemand(uint256 requested, uint256 demand); + /// @notice Initializes the operators registry /// @param _admin Admin in charge of managing operators /// @param _river Address of River system @@ -347,8 +358,8 @@ interface IOperatorsRegistryV1 { /// @notice Public endpoint to consume the exit request demand and perform the actual exit requests /// @notice The selection algorithm will pick validators based on their active validator counts /// @notice This value is computed by using the count of funded keys and taking into account the stopped validator counts and exit requests - /// @param _count Max amount of exits to request - function requestValidatorExits(uint256 _count) external; + /// @param _allocations The proposed allocations to exit + function requestValidatorExits(OperatorAllocation[] calldata _allocations) external; /// @notice Increases the exit request demand /// @dev This method is only callable by the river contract, and to actually forward the information to the node operators via event emission, the unprotected requestValidatorExits method must be called From a097680dc6b94cfeb4e3cc36f55e010413a4d0bb Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:31:18 +0000 Subject: [PATCH 11/60] fix: ConsensusLayerDepositManager tests --- .../ConsensusLayerDepositManager.1.t.sol | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index a491e09e..ecd3e8f3 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -118,16 +118,24 @@ contract ConsensusLayerDepositManagerV1Tests is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } - function testRetrieveWithdrawalCredentials() public view { - assert(depositManager.getWithdrawalCredentials() == withdrawalCredentials); - } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); return allocations; } + function testRetrieveWithdrawalCredentials() public view { + assert(depositManager.getWithdrawalCredentials() == withdrawalCredentials); + } + + function testDepositNotEnoughFunds() public { + vm.deal(address(depositManager), 31.9 ether); + ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); + vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); + } + function testDepositAllocationFailsWithNotEnoughFunds() public { vm.deal(address(depositManager), 31.9 ether); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); @@ -145,14 +153,13 @@ contract ConsensusLayerDepositManagerV1Tests is Test { assert(address(depositManager).balance == 0); } - function testRequestToDepositMoreThanMaxDepositableCountFailsWithInvalidPublicKeyCount() public { + function testDepositLessThanMaxDepositableCount() public { vm.deal(address(depositManager), 640 ether); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); assert(address(depositManager).balance == 640 ether); vm.prank(address(0x1)); - vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(20), bytes32(0)); - assert(address(depositManager).balance == 640 ether); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(10), bytes32(0)); + assert(address(depositManager).balance == 320 ether); } } @@ -296,31 +303,33 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { return allocations; } + // For InconsistentPublicKeys - scenario 1 returns 1 key with 49-byte pubkey function testInconsistentPublicKey() public { - vm.deal(address(depositManager), 5 * 32 ether); + vm.deal(address(depositManager), 32 ether); // 1 deposit ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); - ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(1); // only returns 1 public key - vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(1); + vm.expectRevert(abi.encodeWithSignature("InconsistentPublicKeys()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } - function testPublicKeyAndSignatureCountMismatch() public { - vm.deal(address(depositManager), 5 * 32 ether); + // For InconsistentSignatures - scenario 2 returns 1 key with 97-byte signature + function testInconsistentSignature() public { + vm.deal(address(depositManager), 32 ether); // 1 deposit ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); - ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(2); // returns less key signature pairs than expected - vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(2); + vm.expectRevert(abi.encodeWithSignature("InconsistentSignatures()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } function testUnavailableKeys() public { - vm.deal(address(depositManager), 5 * 32 ether); + vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(3); vm.expectRevert(abi.encodeWithSignature("NoAvailableValidatorKeys()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } function testInvalidPublicKeyCount() public { @@ -377,13 +386,13 @@ contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is Test { } function testInvalidWithdrawalCredential() public { - vm.deal(address(depositManager), 5 * 32 ether); + vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(0); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setKeeper(address(0x1)); vm.expectRevert(abi.encodeWithSignature("InvalidWithdrawalCredentials()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)) .sudoSetWithdrawalCredentials(withdrawalCredentials); } From 8ac80b1a46d09439c2f29976241c20f571c362cd Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:31:49 +0000 Subject: [PATCH 12/60] style: forge fmt --- .../test/components/ConsensusLayerDepositManager.1.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index ecd3e8f3..d16cdc3b 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -305,7 +305,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { // For InconsistentPublicKeys - scenario 1 returns 1 key with 49-byte pubkey function testInconsistentPublicKey() public { - vm.deal(address(depositManager), 32 ether); // 1 deposit + vm.deal(address(depositManager), 32 ether); // 1 deposit ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(1); vm.expectRevert(abi.encodeWithSignature("InconsistentPublicKeys()")); @@ -315,7 +315,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { // For InconsistentSignatures - scenario 2 returns 1 key with 97-byte signature function testInconsistentSignature() public { - vm.deal(address(depositManager), 32 ether); // 1 deposit + vm.deal(address(depositManager), 32 ether); // 1 deposit ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(2); vm.expectRevert(abi.encodeWithSignature("InconsistentSignatures()")); From 4190d3e3d31a99767ee34aac0880c697e8f8d525 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:38:34 +0000 Subject: [PATCH 13/60] test(deposits): add test for faulty registry returning more keys than requested Add scenario 7 to mock that returns 4 keys regardless of request and test that InvalidPublicKeyCount() is thrown when registry returns more keys than requested but within maxDepositableCount. --- .../ConsensusLayerDepositManager.1.t.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index d16cdc3b..880308e6 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -265,6 +265,16 @@ contract ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest is Consen signatures[idx] = LibBytes.slice(_signatures, idx * 96, 96); } return (publicKeys, signatures); + } else if (scenario == 7) { + // Return more keys than requested (but within max depositable) + uint256 amount = 4; + bytes[] memory publicKeys = new bytes[](amount); + bytes[] memory signatures = new bytes[](amount); + for (uint256 idx = 0; idx < amount; ++idx) { + publicKeys[idx] = LibBytes.slice(_publicKeys, idx * 48, 48); + signatures[idx] = LibBytes.slice(_signatures, idx * 96, 96); + } + return (publicKeys, signatures); } return (new bytes[](0), new bytes[](0)); } @@ -350,6 +360,15 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(4), bytes32(0)); } + function testFaultyRegistryReturnsMoreKeysThanRequested() public { + vm.deal(address(depositManager), 4 * 32 ether); // maxDepositableCount = 4 + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).setScenario(7); // returns 4 keys + vm.expectRevert(abi.encodeWithSignature("InvalidPublicKeyCount()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(2), bytes32(0)); // only request 2 + } + function testAllocationExceedsCommittedBalance() public { // Fund with only 2 deposits worth of ETH vm.deal(address(depositManager), 2 * 32 ether); From 79a945bf8e7ee7008fcc686db615d2eafc4f5aa9 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 14:46:34 +0000 Subject: [PATCH 14/60] style: refactor allocation test helper to avoid duplication --- .../ConsensusLayerDepositManager.1.t.sol | 56 +++++-------------- 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index 880308e6..dc1e0801 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -11,6 +11,14 @@ import "../mocks/DepositContractMock.sol"; import "../mocks/DepositContractEnhancedMock.sol"; import "../mocks/DepositContractInvalidMock.sol"; +abstract contract ConsensusLayerDepositManagerTestBase is Test { + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } +} + contract ConsensusLayerDepositManagerV1ExposeInitializer is ConsensusLayerDepositManagerV1 { function _getRiverAdmin() internal pure override returns (address) { return address(0); @@ -103,7 +111,7 @@ contract ConsensusLayerDepositManagerV1InitTests is Test { } } -contract ConsensusLayerDepositManagerV1Tests is Test { +contract ConsensusLayerDepositManagerV1Tests is ConsensusLayerDepositManagerTestBase { bytes32 internal withdrawalCredentials = bytes32(uint256(1)); ConsensusLayerDepositManagerV1 internal depositManager; @@ -118,12 +126,6 @@ contract ConsensusLayerDepositManagerV1Tests is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - function testRetrieveWithdrawalCredentials() public view { assert(depositManager.getWithdrawalCredentials() == withdrawalCredentials); } @@ -292,7 +294,7 @@ contract ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest is Consen } } -contract ConsensusLayerDepositManagerV1ErrorTests is Test { +contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManagerTestBase { bytes32 internal withdrawalCredentials = bytes32(uint256(1)); ConsensusLayerDepositManagerV1 internal depositManager; @@ -307,12 +309,6 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - // For InconsistentPublicKeys - scenario 1 returns 1 key with 49-byte pubkey function testInconsistentPublicKey() public { vm.deal(address(depositManager), 32 ether); // 1 deposit @@ -380,7 +376,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is Test { } } -contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is Test { +contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is ConsensusLayerDepositManagerTestBase { bytes32 internal withdrawalCredentials = bytes32(uint256(1)); ConsensusLayerDepositManagerV1 internal depositManager; @@ -398,12 +394,6 @@ contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is Test { LibImplementationUnbricker.unbrick(vm, address(depositManager)); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - function testInvalidWithdrawalCredential() public { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); @@ -478,7 +468,7 @@ contract ConsensusLayerDepositManagerV1ValidKeys is ConsensusLayerDepositManager } } -contract ConsensusLayerDepositManagerV1ValidKeysTest is Test { +contract ConsensusLayerDepositManagerV1ValidKeysTest is ConsensusLayerDepositManagerTestBase { ConsensusLayerDepositManagerV1 internal depositManager; IDepositContract internal depositContract; @@ -499,12 +489,6 @@ contract ConsensusLayerDepositManagerV1ValidKeysTest is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - function testDepositValidKey() external { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ValidKeys(address(depositManager)).sudoSyncBalance(); @@ -530,7 +514,7 @@ contract ConsensusLayerDepositManagerV1ValidKeysTest is Test { } } -contract ConsensusLayerDepositManagerV1InvalidDepositContract is Test { +contract ConsensusLayerDepositManagerV1InvalidDepositContract is ConsensusLayerDepositManagerTestBase { ConsensusLayerDepositManagerV1 internal depositManager; IDepositContract internal depositContract; @@ -545,12 +529,6 @@ contract ConsensusLayerDepositManagerV1InvalidDepositContract is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - function testDepositInvalidDepositContract() external { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ValidKeys(address(depositManager)).sudoSyncBalance(); @@ -560,7 +538,7 @@ contract ConsensusLayerDepositManagerV1InvalidDepositContract is Test { } } -contract ConsensusLayerDepositManagerV1KeeperTest is Test { +contract ConsensusLayerDepositManagerV1KeeperTest is ConsensusLayerDepositManagerTestBase { ConsensusLayerDepositManagerV1 internal depositManager; IDepositContract internal depositContract; @@ -581,12 +559,6 @@ contract ConsensusLayerDepositManagerV1KeeperTest is Test { .publicConsensusLayerDepositManagerInitializeV1(address(depositContract), withdrawalCredentials); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - function testDepositValidKeeper() external { vm.deal(address(depositManager), 32 ether); ConsensusLayerDepositManagerV1ValidKeys(address(depositManager)).sudoSyncBalance(); From 618bd55153ed05c65962f8e63f3f1be1f1afdbc5 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 16:01:57 +0000 Subject: [PATCH 15/60] style: rm comment + rename helper for consistency --- contracts/src/OperatorsRegistry.1.sol | 1 - contracts/test/OperatorsRegistry.1.t.sol | 56 ++++++++++++------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 8912b75c..0eac67a2 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -736,7 +736,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab // Find the operator in the compacted array by matching .index for (uint256 i = 0; i < operators.length; ++i) { if (operators[i].index == _operatorIndex) { - // we take the smallest value between limit - (funded + picked) uint256 availableKeys = operators[i].limit - (operators[i].funded + operators[i].picked); if (_validatorCount > availableKeys) { diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 4b55290c..d5a9da4f 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -17,7 +17,7 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { operator.funded = _funded; } - function debugGetNextValidatorsToDepositFromActiveOperators(OperatorAllocation[] memory _allocations) + function debugPickNextValidatorsToDepositFromActiveOperators(OperatorAllocation[] memory _allocations) external returns (bytes[] memory publicKeys, bytes[] memory signatures) { @@ -1293,7 +1293,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); vm.prank(river); for (uint256 idx = 1; idx < len + 1; ++idx) { @@ -1376,7 +1376,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); stoppedValidators[0] -= 1; @@ -1568,7 +1568,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[4] = 10; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); + ).debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); assert(publicKeys.length == 50); assert(signatures.length == 50); @@ -1638,7 +1638,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts2[4] = 40; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts2)); + ).debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts2)); assert(publicKeys.length == 200); assert(signatures.length == 200); @@ -1687,7 +1687,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.expectRevert(abi.encodeWithSelector(IOperatorsRegistryV1.AllocationWithZeroValidatorCount.selector)); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); } function testInactiveDepositDistribution() external { @@ -1733,7 +1733,7 @@ contract OperatorsRegistryV1TestDistribution is Test { { (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(activeOperators, limits)); + ).debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(activeOperators, limits)); assert(publicKeys.length == 150); assert(signatures.length == 150); @@ -1776,7 +1776,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[1] = 25; allocCounts[2] = 25; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); } assert(operatorsRegistry.getOperator(0).funded == 25); assert(operatorsRegistry.getOperator(1).funded == 0); @@ -1823,7 +1823,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[4] = 10; (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( address(operatorsRegistry) - ).debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(allOps, allocCounts)); + ).debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(allOps, allocCounts)); assert(publicKeys.length == 50); assert(signatures.length == 50); @@ -1870,7 +1870,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[3] = 10; allocCounts[4] = 10; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, allocCounts)); } assert(operatorsRegistry.getOperator(0).funded == 10); assert(operatorsRegistry.getOperator(1).funded == 10); @@ -1916,7 +1916,7 @@ contract OperatorsRegistryV1TestDistribution is Test { allocCounts[0] = 25; allocCounts[1] = 25; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(alloOperators, allocCounts)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(alloOperators, allocCounts)); } assert(operatorsRegistry.getOperator(0).funded == 10); assert(operatorsRegistry.getOperator(1).funded == 35); @@ -1953,7 +1953,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2021,7 +2021,7 @@ contract OperatorsRegistryV1TestDistribution is Test { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2110,7 +2110,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2162,7 +2162,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2297,7 +2297,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2389,7 +2389,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); } assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 0); @@ -2437,7 +2437,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); } vm.expectEmit(true, true, true, true); @@ -2483,7 +2483,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2539,7 +2539,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 40); assert(operatorsRegistry.getOperator(2).funded == 30); @@ -2595,7 +2595,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 50); assert(operatorsRegistry.getOperator(2).funded == 50); @@ -2650,7 +2650,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); assert(operatorsRegistry.getOperator(1).funded == 40); assert(operatorsRegistry.getOperator(2).funded == 30); @@ -2752,7 +2752,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2826,7 +2826,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2905,7 +2905,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(operators, limits)); { uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -2985,7 +2985,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); { uint32[] memory stoppedValidatorCount = new uint32[](5); @@ -3070,7 +3070,7 @@ contract OperatorsRegistryV1TestDistribution is Test { + fuzzedStoppedValidatorCount[2] + fuzzedStoppedValidatorCount[3] + fuzzedStoppedValidatorCount[4]; OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); @@ -3120,7 +3120,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); uint32[] memory stoppedValidatorCount = new uint32[](6); stoppedValidatorCount[1] = 10; From 2577c4853d88cfe45608939ba6ae898dc2c91c9b Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 17:13:38 +0000 Subject: [PATCH 16/60] feat: add more unit tests for pickNextValidatorsToDepositFromActiveOperators --- contracts/test/OperatorsRegistry.1.t.sol | 87 +++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index d5a9da4f..a6698194 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3245,7 +3245,54 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); } - function testGetNextValidatorsToDepositRevertsWhenOperatorInactive() public { + function testPickNextValidatorsToDepositFromActiveOperatorsRevertsWhenExceedingLimit() public { + bytes[] memory rawKeys = new bytes[](5); + + rawKeys[0] = genBytes((48 + 96) * 10); + rawKeys[1] = genBytes((48 + 96) * 10); + rawKeys[2] = genBytes((48 + 96) * 10); + rawKeys[3] = genBytes((48 + 96) * 10); + rawKeys[4] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + operatorsRegistry.addValidators(1, 10, rawKeys[1]); + operatorsRegistry.addValidators(2, 10, rawKeys[2]); + operatorsRegistry.addValidators(3, 10, rawKeys[3]); + operatorsRegistry.addValidators(4, 10, rawKeys[4]); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + limits[3] = 10; + limits[4] = 10; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation requesting 11 validators from first operator (exceeds limit of 10) + uint32[] memory allocCounts = new uint32[](1); + allocCounts[0] = 11; + uint256[] memory allocOperators = new uint256[](1); + allocOperators[0] = 0; + + vm.expectRevert( + abi.encodeWithSignature("OperatorDoesNotHaveEnoughFundableKeys(uint256,uint256,uint256)", 0, 11, 10) + ); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(allocOperators, allocCounts)); + } + + function testGetNextValidatorsToDepositReturnsEmptyArraysWhenOperatorInactive() public { bytes memory rawKeys = genBytes((48 + 96) * 10); vm.startPrank(admin); @@ -3271,6 +3318,33 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(signatures.length == 0); } + function testPickNextValidatorsToDepositReturnsEmptyArraysWhenOperatorInactive() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Deactivate the operator + operatorsRegistry.setOperatorStatus(0, false); + vm.stopPrank(); + + // Create allocation for inactive operator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + + (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( + address(operatorsRegistry) + ).debugPickNextValidatorsToDepositFromActiveOperators(allocation); + assert(publicKeys.length == 0); + assert(signatures.length == 0); + } + function testGetNextValidatorsToDepositForNoOperators() public { // Create an allocation with no operators IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](0); @@ -3281,6 +3355,17 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(signatures.length == 0); } + function testPickNextValidatorsToDepositForNoOperators() public { + // Create an allocation with no operators + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](0); + + (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( + address(operatorsRegistry) + ).debugPickNextValidatorsToDepositFromActiveOperators(allocation); + assert(publicKeys.length == 0); + assert(signatures.length == 0); + } + function testGetNextValidatorsToDepositRevertsDuplicateOperatorIndex() public { bytes[] memory rawKeys = new bytes[](2); rawKeys[0] = genBytes((48 + 96) * 10); From 5250225868b31e3b0b4c88404036001fa8009475 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Thu, 5 Feb 2026 18:45:35 +0100 Subject: [PATCH 17/60] chore: test fix --- contracts/test/OperatorsRegistry.1.t.sol | 41 +++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 4b55290c..3f0cbb1c 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -45,6 +45,7 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { contract RiverMock { uint256 public getDepositedValidatorCount; + address public keeper; constructor(uint256 _getDepositedValidatorsCount) { getDepositedValidatorCount = _getDepositedValidatorsCount; @@ -53,6 +54,14 @@ contract RiverMock { function sudoSetDepositedValidatorsCount(uint256 _getDepositedValidatorsCount) external { getDepositedValidatorCount = _getDepositedValidatorsCount; } + + function setKeeper(address _keeper) external { + keeper = _keeper; + } + + function getKeeper() external view returns (address) { + return keeper; + } } abstract contract OperatorsRegistryV1TestBase is Test { @@ -61,6 +70,7 @@ abstract contract OperatorsRegistryV1TestBase is Test { OperatorsRegistryV1 internal operatorsRegistry; address internal admin; address internal river; + address internal keeper; string internal firstName = "Operator One"; string internal secondName = "Operator Two"; @@ -85,7 +95,9 @@ abstract contract OperatorsRegistryV1TestBase is Test { contract OperatorsRegistryV1InitializationTests is OperatorsRegistryV1TestBase { function setUp() public { admin = makeAddr("admin"); + keeper = makeAddr("keeper"); river = address(new RiverMock(0)); + RiverMock(river).setKeeper(keeper); operatorsRegistry = new OperatorsRegistryInitializableV1(); LibImplementationUnbricker.unbrick(vm, address(operatorsRegistry)); } @@ -103,7 +115,9 @@ contract OperatorsRegistryV1InitializationTests is OperatorsRegistryV1TestBase { contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { function setUp() public { admin = makeAddr("admin"); + keeper = makeAddr("keeper"); river = address(new RiverMock(0)); + RiverMock(river).setKeeper(keeper); operatorsRegistry = new OperatorsRegistryInitializableV1(); LibImplementationUnbricker.unbrick(vm, address(operatorsRegistry)); operatorsRegistry.initOperatorsRegistryV1(admin, river); @@ -1400,6 +1414,7 @@ contract OperatorsRegistryV1TestDistribution is Test { address internal operatorThree; address internal operatorFour; address internal operatorFive; + address internal keeper; event AddedValidatorKeys(uint256 indexed index, bytes publicKeys); event RemovedValidatorKey(uint256 indexed index, bytes publicKey); @@ -1446,6 +1461,8 @@ contract OperatorsRegistryV1TestDistribution is Test { function setUp() public { admin = makeAddr("admin"); river = address(new RiverMock(0)); + keeper = makeAddr("keeper"); + RiverMock(river).setKeeper(keeper); operatorOne = makeAddr("operatorOne"); operatorTwo = makeAddr("operatorTwo"); @@ -1983,7 +2000,14 @@ contract OperatorsRegistryV1TestDistribution is Test { emit RequestedValidatorExits(4, 50); vm.expectEmit(true, true, true, true); emit SetTotalValidatorExitsRequested(0, 250); - operatorsRegistry.requestValidatorExits(250); + uint32[] memory exitCounts = new uint32[](5); + exitCounts[0] = 50; + exitCounts[1] = 50; + exitCounts[2] = 50; + exitCounts[3] = 50; + exitCounts[4] = 50; + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createMultiAllocation(operators, exitCounts)); assert(operatorsRegistry.getOperator(0).requestedExits == 50); assert(operatorsRegistry.getOperator(1).requestedExits == 50); @@ -2066,7 +2090,14 @@ contract OperatorsRegistryV1TestDistribution is Test { emit SetTotalValidatorExitsRequested(100, 250); vm.expectEmit(true, true, true, true); emit SetCurrentValidatorExitsDemand(150, 0); - operatorsRegistry.requestValidatorExits(150); + uint32[] memory exitCounts = new uint32[](5); + exitCounts[0] = 30; + exitCounts[1] = 30; + exitCounts[2] = 30; + exitCounts[3] = 30; + exitCounts[4] = 30; + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createMultiAllocation(operators, exitCounts)); assert(operatorsRegistry.getOperator(0).requestedExits == 50); assert(operatorsRegistry.getOperator(1).requestedExits == 50); @@ -2079,8 +2110,10 @@ contract OperatorsRegistryV1TestDistribution is Test { } function testRequestValidatorNoExits() external { - vm.expectRevert(abi.encodeWithSignature("NoExitRequestsToPerform()")); - operatorsRegistry.requestValidatorExits(0); + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](0); + vm.expectRevert(abi.encodeWithSignature("InvalidEmptyArray()")); + operatorsRegistry.requestValidatorExits(allocations); } function testOneExitDistribution() external { From 486b172afced85e9796dc28d2d82a4990ecd8d0b Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 19:09:12 +0000 Subject: [PATCH 18/60] test(consensus-layer-deposit): add allocation validation tests Add tests for UnorderedOperatorList and AllocationWithZeroValidatorCount error cases in depositToConsensusLayerWithDepositRoot to ensure invalid operator allocations are properly rejected. --- .../ConsensusLayerDepositManager.1.t.sol | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index dc1e0801..18e8288b 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -374,6 +374,70 @@ contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManage vm.prank(address(0x1)); depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(5), bytes32(0)); } + + // Tests that duplicate operator indices in allocations revert with UnorderedOperatorList + function testUnorderedOperatorListDuplicate() public { + vm.deal(address(depositManager), 4 * 32 ether); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + + // Create allocations with duplicate operator index: [{0, 2}, {0, 2}] + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](2); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + allocations[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(allocations, bytes32(0)); + } + + // Tests that non-ascending operator indices revert with UnorderedOperatorList + function testUnorderedOperatorListDescendingOperatorIndices() public { + vm.deal(address(depositManager), 4 * 32 ether); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + + // Create allocations with descending order: [{1, 2}, {0, 2}] + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](2); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 2}); + allocations[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(allocations, bytes32(0)); + } + + // Tests that an allocation with zero validator count reverts with AllocationWithZeroValidatorCount + function testAllocationWithZeroValidatorCount() public { + vm.deal(address(depositManager), 2 * 32 ether); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + + // Create allocation with zero validator count: [{0, 0}] + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); + + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(allocations, bytes32(0)); + } + + // Tests that a multi-allocation array with a zero count in the middle reverts + function testAllocationWithZeroValidatorCountInMiddle() public { + vm.deal(address(depositManager), 4 * 32 ether); + ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); + + // Create allocations: [{0, 2}, {1, 0}, {2, 2}] - middle has zero count + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](3); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + allocations[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 0}); + allocations[2] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 2, validatorCount: 2}); + + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + vm.prank(address(0x1)); + depositManager.depositToConsensusLayerWithDepositRoot(allocations, bytes32(0)); + } } contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is ConsensusLayerDepositManagerTestBase { From 23024db17979ab825dfeb13a46eeab14060de6ec Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 19:14:12 +0000 Subject: [PATCH 19/60] test(operators-registry): add allocation validation error tests Add tests for AllocationWithZeroValidatorCount and InactiveOperator error cases in getNextValidatorsToDepositFromActiveOperators and pickNextValidatorsToDeposit functions. --- contracts/test/OperatorsRegistry.1.t.sol | 94 ++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index a6698194..07a98651 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3480,6 +3480,100 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.pickNextValidatorsToDeposit(allocation); } + function testGetNextValidatorsToDepositRevertsZeroValidatorCount() public { + bytes[] memory rawKeys = new bytes[](1); + rawKeys[0] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with zero validator count + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); + + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + } + + function testPickNextValidatorsToDepositRevertsZeroValidatorCount() public { + bytes[] memory rawKeys = new bytes[](1); + rawKeys[0] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with zero validator count + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); + + vm.prank(river); + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); + } + + function testGetNextValidatorsToDepositRevertsInactiveOperator() public { + bytes[] memory rawKeys = new bytes[](1); + rawKeys[0] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with operator index that doesn't exist (only operator 0 exists) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 99, validatorCount: 5}); + + vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + } + + function testPickNextValidatorsToDepositRevertsInactiveOperator() public { + bytes[] memory rawKeys = new bytes[](1); + rawKeys[0] = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Create allocation with operator index that doesn't exist (only operator 0 exists) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 99, validatorCount: 5}); + + vm.prank(river); + vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); + } + function testVersion() external { assertEq(operatorsRegistry.version(), "1.2.1"); } From cbb529351f7ce428cbeb821776020b11f603b9cd Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 19:14:28 +0000 Subject: [PATCH 20/60] style: forge fmt --- .../components/ConsensusLayerDepositManager.1.t.sol | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index 18e8288b..50a0e013 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -381,8 +381,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManage ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); // Create allocations with duplicate operator index: [{0, 2}, {0, 2}] - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](2); + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](2); allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); allocations[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); @@ -397,8 +396,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManage ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); // Create allocations with descending order: [{1, 2}, {0, 2}] - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](2); + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](2); allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 2}); allocations[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); @@ -413,8 +411,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManage ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); // Create allocation with zero validator count: [{0, 0}] - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](1); + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); @@ -428,8 +425,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManage ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest(address(depositManager)).sudoSyncBalance(); // Create allocations: [{0, 2}, {1, 0}, {2, 2}] - middle has zero count - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](3); + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](3); allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); allocations[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 0}); allocations[2] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 2, validatorCount: 2}); From 2145d631cbfd9523e092277a4cb0b4eee7beb154 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 20:11:03 +0000 Subject: [PATCH 21/60] test(operators-registry): add test for zero fundable operators case Add test that triggers the early return path in getNextValidatorsToDepositFromActiveOperators when fundableOperatorCount is zero, ensuring empty arrays are returned correctly. --- contracts/test/OperatorsRegistry.1.t.sol | 178 +++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 07a98651..97cbbbfe 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3577,4 +3577,182 @@ contract OperatorsRegistryV1TestDistribution is Test { function testVersion() external { assertEq(operatorsRegistry.version(), "1.2.1"); } + + function testGetNextValidatorsToDepositFromActiveOperatorsReturnsEmptyWhenNoFundableOperators() public { + // No operators have been added, so fundableOperatorCount will be 0 + // This triggers the early return at line 218: if (fundableOperatorCount == 0) + + // Create an empty allocation array (the allocation content doesn't matter since + // the function returns early when there are no fundable operators) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](0); + + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + // Should return empty arrays since there are no fundable operators + assertEq(publicKeys.length, 0, "Expected empty publicKeys array"); + assertEq(signatures.length, 0, "Expected empty signatures array"); + } + + // Tests to improve branch coverage of _updateCountOfPickedValidatorsForEachOperator + // These ensure the loop iterates past non-matching operators before finding the target + + function testPickValidatorsFromSecondOperatorOnly( + uint256 _operatorOneSalt, + uint256 _operatorTwoSalt, + uint256 _operatorThreeSalt + ) public { + // Setup: Add 3 operators with keys and limits + address _operatorOne = uf._new(_operatorOneSalt); + address _operatorTwo = uf._new(_operatorTwoSalt); + address _operatorThree = uf._new(_operatorThreeSalt); + vm.startPrank(admin); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(_operatorOne); + operatorsRegistry.addValidators(0, 10, tenKeys); + + vm.prank(_operatorTwo); + operatorsRegistry.addValidators(1, 10, tenKeys); + + vm.prank(_operatorThree); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Allocate ONLY to operator 1 (not the first fundable operator) + // This forces the loop to iterate past operator 0 before finding operator 1 + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(1, 5)); + + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + + // Verify only operator 1 was funded + assertEq(operatorsRegistry.getOperator(0).funded, 0); + assertEq(operatorsRegistry.getOperator(1).funded, 5); + assertEq(operatorsRegistry.getOperator(2).funded, 0); + } + + function testPickValidatorsFromLastOperatorOnly( + uint256 _operatorOneSalt, + uint256 _operatorTwoSalt, + uint256 _operatorThreeSalt + ) public { + // Setup: Add 3 operators with keys and limits + address _operatorOne = uf._new(_operatorOneSalt); + address _operatorTwo = uf._new(_operatorTwoSalt); + address _operatorThree = uf._new(_operatorThreeSalt); + vm.startPrank(admin); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(_operatorOne); + operatorsRegistry.addValidators(0, 10, tenKeys); + + vm.prank(_operatorTwo); + operatorsRegistry.addValidators(1, 10, tenKeys); + + vm.prank(_operatorThree); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Allocate ONLY to operator 2 (the last fundable operator) + // This forces the loop to iterate past operators 0 and 1 + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(2, 5)); + + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + + // Verify only operator 2 was funded + assertEq(operatorsRegistry.getOperator(0).funded, 0); + assertEq(operatorsRegistry.getOperator(1).funded, 0); + assertEq(operatorsRegistry.getOperator(2).funded, 5); + } + + function testGetNextValidatorsFromNonFirstOperator( + uint256 _operatorOneSalt, + uint256 _operatorTwoSalt, + uint256 _operatorThreeSalt + ) public { + // Setup: Add 3 operators with keys and limits + address _operatorOne = uf._new(_operatorOneSalt); + address _operatorTwo = uf._new(_operatorTwoSalt); + address _operatorThree = uf._new(_operatorThreeSalt); + vm.startPrank(admin); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(_operatorOne); + operatorsRegistry.addValidators(0, 10, tenKeys); + + vm.prank(_operatorTwo); + operatorsRegistry.addValidators(1, 10, tenKeys); + + vm.prank(_operatorThree); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Test the view function with allocation to operator 2 only + // This also exercises _updateCountOfPickedValidatorsForEachOperator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 2, validatorCount: 5}); + + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + } } From 8e719c7b32ee1360bf56eaf6b1a46a12334e404e Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 20:17:36 +0000 Subject: [PATCH 22/60] test(operators-registry): add branch coverage for operator lookup loop Add tests that allocate validators to non-first operators in the fundable array, ensuring the _updateCountOfPickedValidatorsForEachOperator loop iterates past non-matching operators before finding the target. This improves branch coverage for line 738's condition check. --- contracts/test/OperatorsRegistry.1.t.sol | 324 +++++++++++------------ 1 file changed, 162 insertions(+), 162 deletions(-) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 97cbbbfe..e30f59ce 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -1384,6 +1384,168 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator vm.expectRevert(abi.encodeWithSignature("InvalidStoppedValidatorCountsSum()")); operatorsRegistry.reportStoppedValidatorCounts(stoppedValidators, 0); } + + // Tests to improve branch coverage of _updateCountOfPickedValidatorsForEachOperator + // These ensure the loop iterates past non-matching operators before finding the target + + function testPickValidatorsFromSecondOperatorOnly( + uint256 _operatorOneSalt, + uint256 _operatorTwoSalt, + uint256 _operatorThreeSalt + ) public { + // Setup: Add 3 operators with keys and limits + address _operatorOne = uf._new(_operatorOneSalt); + address _operatorTwo = uf._new(_operatorTwoSalt); + address _operatorThree = uf._new(_operatorThreeSalt); + vm.startPrank(admin); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(_operatorOne); + operatorsRegistry.addValidators(0, 10, tenKeys); + + vm.prank(_operatorTwo); + operatorsRegistry.addValidators(1, 10, tenKeys); + + vm.prank(_operatorThree); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Allocate ONLY to operator 1 (not the first fundable operator) + // This forces the loop to iterate past operator 0 before finding operator 1 + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(1, 5)); + + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + + // Verify only operator 1 was funded + assertEq(operatorsRegistry.getOperator(0).funded, 0); + assertEq(operatorsRegistry.getOperator(1).funded, 5); + assertEq(operatorsRegistry.getOperator(2).funded, 0); + } + + function testPickValidatorsFromLastOperatorOnly( + uint256 _operatorOneSalt, + uint256 _operatorTwoSalt, + uint256 _operatorThreeSalt + ) public { + // Setup: Add 3 operators with keys and limits + address _operatorOne = uf._new(_operatorOneSalt); + address _operatorTwo = uf._new(_operatorTwoSalt); + address _operatorThree = uf._new(_operatorThreeSalt); + vm.startPrank(admin); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(_operatorOne); + operatorsRegistry.addValidators(0, 10, tenKeys); + + vm.prank(_operatorTwo); + operatorsRegistry.addValidators(1, 10, tenKeys); + + vm.prank(_operatorThree); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Allocate ONLY to operator 2 (the last fundable operator) + // This forces the loop to iterate past operators 0 and 1 + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(2, 5)); + + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + + // Verify only operator 2 was funded + assertEq(operatorsRegistry.getOperator(0).funded, 0); + assertEq(operatorsRegistry.getOperator(1).funded, 0); + assertEq(operatorsRegistry.getOperator(2).funded, 5); + } + + function testGetNextValidatorsFromNonFirstOperator( + uint256 _operatorOneSalt, + uint256 _operatorTwoSalt, + uint256 _operatorThreeSalt + ) public { + // Setup: Add 3 operators with keys and limits + address _operatorOne = uf._new(_operatorOneSalt); + address _operatorTwo = uf._new(_operatorTwoSalt); + address _operatorThree = uf._new(_operatorThreeSalt); + vm.startPrank(admin); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); + operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(_operatorOne); + operatorsRegistry.addValidators(0, 10, tenKeys); + + vm.prank(_operatorTwo); + operatorsRegistry.addValidators(1, 10, tenKeys); + + vm.prank(_operatorThree); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Test the view function with allocation to operator 2 only + // This also exercises _updateCountOfPickedValidatorsForEachOperator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 2, validatorCount: 5}); + + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + } } contract OperatorsRegistryV1TestDistribution is Test { @@ -3593,166 +3755,4 @@ contract OperatorsRegistryV1TestDistribution is Test { assertEq(publicKeys.length, 0, "Expected empty publicKeys array"); assertEq(signatures.length, 0, "Expected empty signatures array"); } - - // Tests to improve branch coverage of _updateCountOfPickedValidatorsForEachOperator - // These ensure the loop iterates past non-matching operators before finding the target - - function testPickValidatorsFromSecondOperatorOnly( - uint256 _operatorOneSalt, - uint256 _operatorTwoSalt, - uint256 _operatorThreeSalt - ) public { - // Setup: Add 3 operators with keys and limits - address _operatorOne = uf._new(_operatorOneSalt); - address _operatorTwo = uf._new(_operatorTwoSalt); - address _operatorThree = uf._new(_operatorThreeSalt); - vm.startPrank(admin); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); - vm.stopPrank(); - - assertEq(operatorsRegistry.getOperatorCount(), 3); - - bytes memory tenKeys = genBytes(10 * (48 + 96)); - - vm.prank(_operatorOne); - operatorsRegistry.addValidators(0, 10, tenKeys); - - vm.prank(_operatorTwo); - operatorsRegistry.addValidators(1, 10, tenKeys); - - vm.prank(_operatorThree); - operatorsRegistry.addValidators(2, 10, tenKeys); - - uint256[] memory indexes = new uint256[](3); - indexes[0] = 0; - indexes[1] = 1; - indexes[2] = 2; - uint32[] memory limits = new uint32[](3); - limits[0] = 10; - limits[1] = 10; - limits[2] = 10; - vm.prank(admin); - operatorsRegistry.setOperatorLimits(indexes, limits, block.number); - - // Allocate ONLY to operator 1 (not the first fundable operator) - // This forces the loop to iterate past operator 0 before finding operator 1 - vm.prank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = - operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(1, 5)); - - assertEq(publicKeys.length, 5); - assertEq(signatures.length, 5); - - // Verify only operator 1 was funded - assertEq(operatorsRegistry.getOperator(0).funded, 0); - assertEq(operatorsRegistry.getOperator(1).funded, 5); - assertEq(operatorsRegistry.getOperator(2).funded, 0); - } - - function testPickValidatorsFromLastOperatorOnly( - uint256 _operatorOneSalt, - uint256 _operatorTwoSalt, - uint256 _operatorThreeSalt - ) public { - // Setup: Add 3 operators with keys and limits - address _operatorOne = uf._new(_operatorOneSalt); - address _operatorTwo = uf._new(_operatorTwoSalt); - address _operatorThree = uf._new(_operatorThreeSalt); - vm.startPrank(admin); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); - vm.stopPrank(); - - assertEq(operatorsRegistry.getOperatorCount(), 3); - - bytes memory tenKeys = genBytes(10 * (48 + 96)); - - vm.prank(_operatorOne); - operatorsRegistry.addValidators(0, 10, tenKeys); - - vm.prank(_operatorTwo); - operatorsRegistry.addValidators(1, 10, tenKeys); - - vm.prank(_operatorThree); - operatorsRegistry.addValidators(2, 10, tenKeys); - - uint256[] memory indexes = new uint256[](3); - indexes[0] = 0; - indexes[1] = 1; - indexes[2] = 2; - uint32[] memory limits = new uint32[](3); - limits[0] = 10; - limits[1] = 10; - limits[2] = 10; - vm.prank(admin); - operatorsRegistry.setOperatorLimits(indexes, limits, block.number); - - // Allocate ONLY to operator 2 (the last fundable operator) - // This forces the loop to iterate past operators 0 and 1 - vm.prank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = - operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(2, 5)); - - assertEq(publicKeys.length, 5); - assertEq(signatures.length, 5); - - // Verify only operator 2 was funded - assertEq(operatorsRegistry.getOperator(0).funded, 0); - assertEq(operatorsRegistry.getOperator(1).funded, 0); - assertEq(operatorsRegistry.getOperator(2).funded, 5); - } - - function testGetNextValidatorsFromNonFirstOperator( - uint256 _operatorOneSalt, - uint256 _operatorTwoSalt, - uint256 _operatorThreeSalt - ) public { - // Setup: Add 3 operators with keys and limits - address _operatorOne = uf._new(_operatorOneSalt); - address _operatorTwo = uf._new(_operatorTwoSalt); - address _operatorThree = uf._new(_operatorThreeSalt); - vm.startPrank(admin); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorOne)), _operatorOne); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorTwo)), _operatorTwo); - operatorsRegistry.addOperator(string(abi.encodePacked(_operatorThree)), _operatorThree); - vm.stopPrank(); - - assertEq(operatorsRegistry.getOperatorCount(), 3); - - bytes memory tenKeys = genBytes(10 * (48 + 96)); - - vm.prank(_operatorOne); - operatorsRegistry.addValidators(0, 10, tenKeys); - - vm.prank(_operatorTwo); - operatorsRegistry.addValidators(1, 10, tenKeys); - - vm.prank(_operatorThree); - operatorsRegistry.addValidators(2, 10, tenKeys); - - uint256[] memory indexes = new uint256[](3); - indexes[0] = 0; - indexes[1] = 1; - indexes[2] = 2; - uint32[] memory limits = new uint32[](3); - limits[0] = 10; - limits[1] = 10; - limits[2] = 10; - vm.prank(admin); - operatorsRegistry.setOperatorLimits(indexes, limits, block.number); - - // Test the view function with allocation to operator 2 only - // This also exercises _updateCountOfPickedValidatorsForEachOperator - IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 2, validatorCount: 5}); - - (bytes[] memory publicKeys, bytes[] memory signatures) = - operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); - - assertEq(publicKeys.length, 5); - assertEq(signatures.length, 5); - } } From dd689b58f41fd202f499087d6709cbdf2f98c01b Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 22:42:12 +0000 Subject: [PATCH 23/60] feat: add another OR test to improve % coverage change --- contracts/test/OperatorsRegistry.1.t.sol | 119 +++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index e30f59ce..264aa170 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -1546,6 +1546,125 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator assertEq(publicKeys.length, 5); assertEq(signatures.length, 5); } + + // Deterministic test to ensure full branch coverage of the operator lookup loop + // This test uses fixed addresses (not fuzzed) to ensure consistent coverage + function testPickValidatorsIteratesLoopCorrectly() public { + // Setup 3 operators with specific addresses (not fuzzed) + address op0 = makeAddr("operator0"); + address op1 = makeAddr("operator1"); + address op2 = makeAddr("operator2"); + + vm.startPrank(admin); + operatorsRegistry.addOperator("Operator 0", op0); + operatorsRegistry.addOperator("Operator 1", op1); + operatorsRegistry.addOperator("Operator 2", op2); + vm.stopPrank(); + + assertEq(operatorsRegistry.getOperatorCount(), 3); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(op0); + operatorsRegistry.addValidators(0, 10, tenKeys); + vm.prank(op1); + operatorsRegistry.addValidators(1, 10, tenKeys); + vm.prank(op2); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Test 1: Allocate to operator 2 only (forces loop to iterate twice with false before true) + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(2, 3)); + assertEq(publicKeys.length, 3); + assertEq(signatures.length, 3); + assertEq(operatorsRegistry.getOperator(0).funded, 0); + assertEq(operatorsRegistry.getOperator(1).funded, 0); + assertEq(operatorsRegistry.getOperator(2).funded, 3); + + // Test 2: Now allocate to operator 1 (forces loop to iterate once with false before true) + vm.prank(river); + (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(1, 2)); + assertEq(publicKeys.length, 2); + assertEq(signatures.length, 2); + assertEq(operatorsRegistry.getOperator(0).funded, 0); + assertEq(operatorsRegistry.getOperator(1).funded, 2); + assertEq(operatorsRegistry.getOperator(2).funded, 3); + + // Test 3: Allocate to operator 0 (first match, no false iterations needed) + vm.prank(river); + (publicKeys, signatures) = operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(0, 1)); + assertEq(publicKeys.length, 1); + assertEq(signatures.length, 1); + assertEq(operatorsRegistry.getOperator(0).funded, 1); + assertEq(operatorsRegistry.getOperator(1).funded, 2); + assertEq(operatorsRegistry.getOperator(2).funded, 3); + } + + // Additional deterministic test for the view function + function testGetNextValidatorsIteratesLoopCorrectly() public { + address op0 = makeAddr("operator0"); + address op1 = makeAddr("operator1"); + address op2 = makeAddr("operator2"); + + vm.startPrank(admin); + operatorsRegistry.addOperator("Operator 0", op0); + operatorsRegistry.addOperator("Operator 1", op1); + operatorsRegistry.addOperator("Operator 2", op2); + vm.stopPrank(); + + bytes memory tenKeys = genBytes(10 * (48 + 96)); + + vm.prank(op0); + operatorsRegistry.addValidators(0, 10, tenKeys); + vm.prank(op1); + operatorsRegistry.addValidators(1, 10, tenKeys); + vm.prank(op2); + operatorsRegistry.addValidators(2, 10, tenKeys); + + uint256[] memory indexes = new uint256[](3); + indexes[0] = 0; + indexes[1] = 1; + indexes[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(indexes, limits, block.number); + + // Test with allocation to operator 2 using the view function + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 2, validatorCount: 5}); + + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + assertEq(publicKeys.length, 5); + assertEq(signatures.length, 5); + + // Test with allocation to operator 1 using the view function + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 3}); + (publicKeys, signatures) = operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + assertEq(publicKeys.length, 3); + assertEq(signatures.length, 3); + + // Test with allocation to operator 0 using the view function + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + (publicKeys, signatures) = operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + assertEq(publicKeys.length, 2); + assertEq(signatures.length, 2); + } } contract OperatorsRegistryV1TestDistribution is Test { From 569e8d6b76abd9b66c2ca3b084eb60fd85a61ed8 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 23:41:37 +0000 Subject: [PATCH 24/60] test(operators-registry): add coverage for inactive operator with multiple fundable operators Improves branch coverage for _updateCountOfPickedValidatorsForEachOperator loop by testing the scenario where multiple operators exist in the fundable array but the requested operator doesn't exist, forcing the loop to iterate through all operators with false condition before reverting. --- contracts/test/OperatorsRegistry.1.t.sol | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 264aa170..4d9275f5 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3855,6 +3855,40 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.pickNextValidatorsToDeposit(allocation); } + function testPickNextValidatorsToDepositRevertsInactiveOperatorWithMultipleFundableOperators() public { + // Setup: 3 operators all with keys and limits (all fundable) + // This test ensures the loop in _updateCountOfPickedValidatorsForEachOperator + // iterates through ALL operators (condition false each time) before reverting + bytes memory tenKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, tenKeys); + operatorsRegistry.addValidators(1, 10, tenKeys); + operatorsRegistry.addValidators(2, 10, tenKeys); + vm.stopPrank(); + + uint256[] memory operators = new uint256[](3); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + uint32[] memory limits = new uint32[](3); + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Request operator 99 which doesn't exist + // This forces the loop to iterate through all 3 fundable operators (all false) + // before reverting with InactiveOperator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 99, validatorCount: 5}); + + vm.prank(river); + vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); + } + function testVersion() external { assertEq(operatorsRegistry.version(), "1.2.1"); } From 179a9d778e7d095e02ed961f23713448e8f057c7 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 23:43:20 +0000 Subject: [PATCH 25/60] review: test fix make more robust --- certora/specs/OperatorRegistryV1ValidatorStates.spec | 2 +- contracts/test/components/ConsensusLayerDepositManager.1.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/certora/specs/OperatorRegistryV1ValidatorStates.spec b/certora/specs/OperatorRegistryV1ValidatorStates.spec index 6b21ded9..ed45c69e 100644 --- a/certora/specs/OperatorRegistryV1ValidatorStates.spec +++ b/certora/specs/OperatorRegistryV1ValidatorStates.spec @@ -457,7 +457,7 @@ rule validatorStateTransition_4_3_M13(method f, env e, calldataarg args) filtere require getKeysCount(opIndex) <= 4; //should not be higher than loop_iter uint stateBefore = getValidatorState(opIndex, validatorData); f(e, args); - uint stateAfter = getValidatorState(opIndex, validatorData); + uint stateAfter = getValidatorStateByIndex(opIndex, validatorData); assert (stateAfter == 4) => (stateBefore == 3 || stateBefore == 4); } diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index 50a0e013..9781ba65 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -143,7 +143,7 @@ contract ConsensusLayerDepositManagerV1Tests is ConsensusLayerDepositManagerTest ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(0), bytes32(0)); + depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } function testDepositTenValidators() public { From 8164b6d73d12f91414e2ec13b25def07aac67172 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 5 Feb 2026 23:44:49 +0000 Subject: [PATCH 26/60] style: rm duplicate test --- .../test/components/ConsensusLayerDepositManager.1.t.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index 9781ba65..813bbdea 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -138,14 +138,6 @@ contract ConsensusLayerDepositManagerV1Tests is ConsensusLayerDepositManagerTest depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); } - function testDepositAllocationFailsWithNotEnoughFunds() public { - vm.deal(address(depositManager), 31.9 ether); - ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); - vm.expectRevert(abi.encodeWithSignature("NotEnoughFunds()")); - vm.prank(address(0x1)); - depositManager.depositToConsensusLayerWithDepositRoot(_createAllocation(1), bytes32(0)); - } - function testDepositTenValidators() public { vm.deal(address(depositManager), 320 ether); ConsensusLayerDepositManagerV1ExposeInitializer(address(depositManager)).sudoSyncBalance(); From fbc3cea9f262572f4baae14ec242e80261f7040b Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Fri, 6 Feb 2026 11:30:20 +0000 Subject: [PATCH 27/60] test(operators-registry): increase test coverage for OperatorsRegistry.1.sol Add 10 new tests covering BYOV allocation logic: - OperatorDoesNotHaveEnoughFundableKeys with partial funding - InactiveOperator when operator exists but deactivated - FundedValidatorKeys event emission - Multi-operator allocation key ordering - Sequential allocations to same operator - Allocation when operator fully funded - View vs state-modifying behavior - Multi-operator with partial funding - Multi-operator second operator exceeds limit - Pick returns empty when no fundable operators --- contracts/test/OperatorsRegistry.1.t.sol | 328 +++++++++++++++++++++++ 1 file changed, 328 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 4d9275f5..1148f5bf 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3908,4 +3908,332 @@ contract OperatorsRegistryV1TestDistribution is Test { assertEq(publicKeys.length, 0, "Expected empty publicKeys array"); assertEq(signatures.length, 0, "Expected empty signatures array"); } + + // ============ NEW TESTS FOR BYOV COVERAGE ============ + + /// @notice Tests OperatorDoesNotHaveEnoughFundableKeys when some keys are already funded + /// This covers the case where availableKeys = limit - (funded + picked) is less than requested + function testOperatorDoesNotHaveEnoughFundableKeysWithPartialFunding() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + // First, fund 7 validators so only 3 remain available + IOperatorsRegistryV1.OperatorAllocation[] memory firstAllocation = + new IOperatorsRegistryV1.OperatorAllocation[](1); + firstAllocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 7}); + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(firstAllocation); + + // Verify operator now has 7 funded + OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 7, "Expected 7 funded validators"); + + // Now try to allocate 5 more (only 3 available: limit=10 - funded=7 = 3) + IOperatorsRegistryV1.OperatorAllocation[] memory secondAllocation = + new IOperatorsRegistryV1.OperatorAllocation[](1); + secondAllocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + + vm.expectRevert( + abi.encodeWithSignature("OperatorDoesNotHaveEnoughFundableKeys(uint256,uint256,uint256)", 0, 5, 3) + ); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(secondAllocation); + } + + /// @notice Tests InactiveOperator error when operator exists but has been deactivated + /// This is different from non-existent operator - the operator exists but is inactive + function testInactiveOperatorWhenOperatorExistsButDeactivated() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + // Deactivate the operator + operatorsRegistry.setOperatorStatus(0, false); + vm.stopPrank(); + + // Try to allocate to the deactivated operator + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + + // Since operator is inactive, getAllFundable returns 0 operators, + // so we get empty arrays (not InactiveOperator error in this case) + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + assertEq(publicKeys.length, 0, "Expected empty publicKeys for deactivated operator"); + assertEq(signatures.length, 0, "Expected empty signatures for deactivated operator"); + } + + /// @notice Tests that pickNextValidatorsToDeposit correctly updates funded count and emits FundedValidatorKeys + function testPickNextValidatorsEmitsFundedValidatorKeysEvent() public { + bytes memory rawKeys = genBytes((48 + 96) * 5); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 5, rawKeys); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 5; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 3}); + + // Expect the FundedValidatorKeys event to be emitted with false (not migration) + vm.expectEmit(true, false, false, false); + emit FundedValidatorKeys(0, new bytes[](3), false); + + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation); + + assertEq(publicKeys.length, 3, "Expected 3 public keys"); + assertEq(signatures.length, 3, "Expected 3 signatures"); + + // Verify funded count was updated + OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 3, "Expected funded to be updated to 3"); + } + + /// @notice Tests multi-operator allocation with correct key ordering + function testMultiOperatorAllocationKeyOrdering() public { + bytes memory rawKeys0 = genBytes((48 + 96) * 5); + bytes memory rawKeys1 = genBytes((48 + 96) * 5); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 5, rawKeys0); + operatorsRegistry.addValidators(1, 5, rawKeys1); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 5; + limits[1] = 5; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + // Allocate 2 from operator 0 and 3 from operator 1 + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + allocation[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 3}); + + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + // Should return 5 total keys (2 + 3) + assertEq(publicKeys.length, 5, "Expected 5 public keys total"); + assertEq(signatures.length, 5, "Expected 5 signatures total"); + } + + /// @notice Tests that sequential allocations to the same operator work correctly + function testSequentialAllocationsToSameOperator() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + // First allocation: fund 3 validators + IOperatorsRegistryV1.OperatorAllocation[] memory allocation1 = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation1[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 3}); + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(allocation1); + + OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 3, "Expected 3 funded after first allocation"); + + // Second allocation: fund 4 more validators + IOperatorsRegistryV1.OperatorAllocation[] memory allocation2 = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation2[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 4}); + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(allocation2); + + op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 7, "Expected 7 funded after second allocation"); + + // Third allocation: try to fund exactly the remaining 3 + IOperatorsRegistryV1.OperatorAllocation[] memory allocation3 = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation3[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 3}); + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(allocation3); + + op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 10, "Expected 10 funded after third allocation (fully funded)"); + } + + /// @notice Tests allocation when operator has no available keys (limit == funded) + function testAllocationWhenOperatorFullyFunded() public { + bytes memory rawKeys = genBytes((48 + 96) * 5); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 5, rawKeys); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 5; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + // Fund all 5 validators + IOperatorsRegistryV1.OperatorAllocation[] memory allocation1 = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation1[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(allocation1); + + // Now try to allocate more - should get empty arrays since operator has limit==funded + IOperatorsRegistryV1.OperatorAllocation[] memory allocation2 = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation2[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 1}); + + // Operator is no longer fundable (limit == funded), so getAllFundable returns 0 + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation2); + + assertEq(publicKeys.length, 0, "Expected empty publicKeys when operator fully funded"); + assertEq(signatures.length, 0, "Expected empty signatures when operator fully funded"); + } + + /// @notice Tests that getNextValidators (view) doesn't modify state while pickNextValidators does + function testViewVsStateModifyingBehavior() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory limits = new uint32[](1); + limits[0] = 10; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 5}); + + // Call view function multiple times - should always return same result + (bytes[] memory keys1,) = operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + (bytes[] memory keys2,) = operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + + assertEq(keys1.length, keys2.length, "View function should return same result on repeated calls"); + + // Verify funded count hasn't changed + OperatorsV2.Operator memory op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 0, "View function should not modify funded count"); + + // Now call the state-modifying version + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); + + op = operatorsRegistry.getOperator(0); + assertEq(op.funded, 5, "pickNextValidatorsToDeposit should modify funded count"); + } + + /// @notice Tests allocation with multiple operators where one has partially funded keys + function testMultiOperatorWithPartialFunding() public { + bytes memory rawKeys = genBytes((48 + 96) * 10); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 10, rawKeys); + operatorsRegistry.addValidators(1, 10, rawKeys); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 10; + limits[1] = 10; + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + // Fund 6 validators from operator 0 + IOperatorsRegistryV1.OperatorAllocation[] memory allocation1 = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation1[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 6}); + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(allocation1); + + // Now allocate from both operators: 4 from op0 (has 4 remaining) and 5 from op1 (has 10) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation2 = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation2[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 4}); + allocation2[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 5}); + + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation2); + + assertEq(publicKeys.length, 9, "Expected 9 public keys (4 + 5)"); + assertEq(signatures.length, 9, "Expected 9 signatures"); + + OperatorsV2.Operator memory op0 = operatorsRegistry.getOperator(0); + OperatorsV2.Operator memory op1 = operatorsRegistry.getOperator(1); + assertEq(op0.funded, 10, "Operator 0 should be fully funded"); + assertEq(op1.funded, 5, "Operator 1 should have 5 funded"); + } + + /// @notice Tests that pick reverts with OperatorDoesNotHaveEnoughFundableKeys for second operator in multi-allocation + function testMultiOperatorSecondOperatorExceedsLimit() public { + bytes memory rawKeys = genBytes((48 + 96) * 5); + + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 5, rawKeys); + operatorsRegistry.addValidators(1, 5, rawKeys); + + uint256[] memory operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + uint32[] memory limits = new uint32[](2); + limits[0] = 5; + limits[1] = 3; // Only 3 available for operator 1 + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.stopPrank(); + + // Try to allocate: 2 from op0 (ok) and 5 from op1 (exceeds limit of 3) + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](2); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 2}); + allocation[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 5}); + + vm.expectRevert( + abi.encodeWithSignature("OperatorDoesNotHaveEnoughFundableKeys(uint256,uint256,uint256)", 1, 5, 3) + ); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + } + + /// @notice Tests the _pickNextValidatorsToDepositFromActiveOperators returns empty when no fundable operators + function testPickReturnsEmptyWhenNoFundableOperators() public { + // No operators have keys or limits set, so none are fundable + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](0); + + vm.prank(river); + (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation); + + assertEq(publicKeys.length, 0, "Expected empty publicKeys"); + assertEq(signatures.length, 0, "Expected empty signatures"); + } } From 850e4a3c4635b733741ec20bb90da0f868855b8b Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Fri, 6 Feb 2026 11:31:48 +0000 Subject: [PATCH 28/60] fix: forge fmt --- contracts/test/OperatorsRegistry.1.t.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 1148f5bf..a52dce3b 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -4002,7 +4002,8 @@ contract OperatorsRegistryV1TestDistribution is Test { emit FundedValidatorKeys(0, new bytes[](3), false); vm.prank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(allocation); assertEq(publicKeys.length, 3, "Expected 3 public keys"); assertEq(signatures.length, 3, "Expected 3 signatures"); @@ -4186,7 +4187,8 @@ contract OperatorsRegistryV1TestDistribution is Test { allocation2[1] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 1, validatorCount: 5}); vm.prank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation2); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(allocation2); assertEq(publicKeys.length, 9, "Expected 9 public keys (4 + 5)"); assertEq(signatures.length, 9, "Expected 9 signatures"); @@ -4231,7 +4233,8 @@ contract OperatorsRegistryV1TestDistribution is Test { IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](0); vm.prank(river); - (bytes[] memory publicKeys, bytes[] memory signatures) = operatorsRegistry.pickNextValidatorsToDeposit(allocation); + (bytes[] memory publicKeys, bytes[] memory signatures) = + operatorsRegistry.pickNextValidatorsToDeposit(allocation); assertEq(publicKeys.length, 0, "Expected empty publicKeys"); assertEq(signatures.length, 0, "Expected empty signatures"); From cff3955140cf8f873416588a694c4412a465c35b Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Fri, 6 Feb 2026 13:32:53 +0000 Subject: [PATCH 29/60] feat: add Operators.1.t.sol to increase cov --- .../state/operatorsRegistry/Operators.1.t.sol | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 contracts/test/state/operatorsRegistry/Operators.1.t.sol diff --git a/contracts/test/state/operatorsRegistry/Operators.1.t.sol b/contracts/test/state/operatorsRegistry/Operators.1.t.sol new file mode 100644 index 00000000..e7702b45 --- /dev/null +++ b/contracts/test/state/operatorsRegistry/Operators.1.t.sol @@ -0,0 +1,368 @@ +//SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.20; + +import "forge-std/Test.sol"; + +import "../../../src/state/operatorsRegistry/Operators.1.sol"; +import "../../utils/LibImplementationUnbricker.sol"; + +/// @title Harness contract to expose internal OperatorsV1 library functions +contract OperatorsV1Harness { + /// @notice Push a new operator + function push(OperatorsV1.Operator memory _operator) external returns (uint256) { + return OperatorsV1.push(_operator); + } + + /// @notice Get an operator by index + function get(uint256 _index) external view returns (OperatorsV1.Operator memory) { + return OperatorsV1.get(_index); + } + + /// @notice Get the count of operators + function getCount() external view returns (uint256) { + return OperatorsV1.getCount(); + } + + /// @notice Get all active operators + function getAllActive() external view returns (OperatorsV1.Operator[] memory) { + return OperatorsV1.getAllActive(); + } + + /// @notice Get all fundable operators + function getAllFundable() external view returns (OperatorsV1.CachedOperator[] memory) { + return OperatorsV1.getAllFundable(); + } + + /// @notice Set keys for an operator + function setKeys(uint256 _index, uint256 _newKeys) external { + OperatorsV1.setKeys(_index, _newKeys); + } + + /// @notice Check if operator has fundable keys + function hasFundableKeys(OperatorsV1.Operator memory _operator) external pure returns (bool) { + return OperatorsV1._hasFundableKeys(_operator); + } + + /// @notice Helper to set operator fields for testing + function setOperatorActive(uint256 _index, bool _active) external { + OperatorsV1.Operator storage op = OperatorsV1.get(_index); + op.active = _active; + } + + /// @notice Helper to set operator limit for testing + function setOperatorLimit(uint256 _index, uint256 _limit) external { + OperatorsV1.Operator storage op = OperatorsV1.get(_index); + op.limit = _limit; + } + + /// @notice Helper to set operator funded for testing + function setOperatorFunded(uint256 _index, uint256 _funded) external { + OperatorsV1.Operator storage op = OperatorsV1.get(_index); + op.funded = _funded; + } +} + +contract OperatorsV1Test is Test { + OperatorsV1Harness internal harness; + + function setUp() public { + harness = new OperatorsV1Harness(); + } + + /// @notice Helper to create a valid operator + function _createOperator(string memory _name, address _addr, bool _active) + internal + pure + returns (OperatorsV1.Operator memory) + { + return OperatorsV1.Operator({ + active: _active, + name: _name, + operator: _addr, + limit: 0, + funded: 0, + keys: 0, + stopped: 0, + latestKeysEditBlockNumber: 0 + }); + } + + // ============ push() tests ============ + + function testPushOperator() public { + OperatorsV1.Operator memory op = _createOperator("Operator One", address(0x1), true); + + uint256 count = harness.push(op); + + assertEq(count, 1, "Expected count to be 1 after push"); + assertEq(harness.getCount(), 1, "Expected getCount to return 1"); + } + + function testPushMultipleOperators() public { + OperatorsV1.Operator memory op1 = _createOperator("Operator One", address(0x1), true); + OperatorsV1.Operator memory op2 = _createOperator("Operator Two", address(0x2), true); + OperatorsV1.Operator memory op3 = _createOperator("Operator Three", address(0x3), false); + + harness.push(op1); + harness.push(op2); + uint256 count = harness.push(op3); + + assertEq(count, 3, "Expected count to be 3 after 3 pushes"); + } + + function testPushOperatorZeroAddressReverts() public { + OperatorsV1.Operator memory op = _createOperator("Operator One", address(0), true); + + vm.expectRevert(abi.encodeWithSignature("InvalidZeroAddress()")); + harness.push(op); + } + + function testPushOperatorEmptyNameReverts() public { + OperatorsV1.Operator memory op = _createOperator("", address(0x1), true); + + vm.expectRevert(abi.encodeWithSignature("InvalidEmptyString()")); + harness.push(op); + } + + // ============ get() tests ============ + + function testGetOperator() public { + OperatorsV1.Operator memory op = _createOperator("Operator One", address(0x1), true); + harness.push(op); + + OperatorsV1.Operator memory retrieved = harness.get(0); + + assertEq(retrieved.name, "Operator One", "Name mismatch"); + assertEq(retrieved.operator, address(0x1), "Operator address mismatch"); + assertTrue(retrieved.active, "Should be active"); + } + + function testGetOperatorNotFoundReverts() public { + vm.expectRevert(abi.encodeWithSignature("OperatorNotFound(uint256)", 0)); + harness.get(0); + } + + function testGetOperatorOutOfBoundsReverts() public { + OperatorsV1.Operator memory op = _createOperator("Operator One", address(0x1), true); + harness.push(op); + + vm.expectRevert(abi.encodeWithSignature("OperatorNotFound(uint256)", 5)); + harness.get(5); + } + + // ============ getCount() tests ============ + + function testGetCountEmpty() public { + assertEq(harness.getCount(), 0, "Expected count to be 0 initially"); + } + + function testGetCountAfterPush() public { + harness.push(_createOperator("Op1", address(0x1), true)); + harness.push(_createOperator("Op2", address(0x2), true)); + + assertEq(harness.getCount(), 2, "Expected count to be 2"); + } + + // ============ getAllActive() tests ============ + + function testGetAllActiveEmpty() public { + OperatorsV1.Operator[] memory active = harness.getAllActive(); + assertEq(active.length, 0, "Expected empty array"); + } + + function testGetAllActiveAllActive() public { + harness.push(_createOperator("Op1", address(0x1), true)); + harness.push(_createOperator("Op2", address(0x2), true)); + harness.push(_createOperator("Op3", address(0x3), true)); + + OperatorsV1.Operator[] memory active = harness.getAllActive(); + + assertEq(active.length, 3, "Expected 3 active operators"); + } + + function testGetAllActiveWithInactive() public { + harness.push(_createOperator("Op1", address(0x1), true)); + harness.push(_createOperator("Op2", address(0x2), false)); + harness.push(_createOperator("Op3", address(0x3), true)); + + OperatorsV1.Operator[] memory active = harness.getAllActive(); + + assertEq(active.length, 2, "Expected 2 active operators"); + assertEq(active[0].name, "Op1", "First active should be Op1"); + assertEq(active[1].name, "Op3", "Second active should be Op3"); + } + + function testGetAllActiveNoneActive() public { + harness.push(_createOperator("Op1", address(0x1), false)); + harness.push(_createOperator("Op2", address(0x2), false)); + + OperatorsV1.Operator[] memory active = harness.getAllActive(); + + assertEq(active.length, 0, "Expected no active operators"); + } + + // ============ getAllFundable() tests ============ + + function testGetAllFundableEmpty() public { + OperatorsV1.CachedOperator[] memory fundable = harness.getAllFundable(); + assertEq(fundable.length, 0, "Expected empty array"); + } + + function testGetAllFundableWithFundableOperator() public { + harness.push(_createOperator("Op1", address(0x1), true)); + harness.setOperatorLimit(0, 10); + + OperatorsV1.CachedOperator[] memory fundable = harness.getAllFundable(); + + assertEq(fundable.length, 1, "Expected 1 fundable operator"); + assertEq(fundable[0].limit, 10, "Limit should be 10"); + assertEq(fundable[0].funded, 0, "Funded should be 0"); + assertEq(fundable[0].index, 0, "Index should be 0"); + } + + function testGetAllFundableNotFundableWhenLimitEqualsFunded() public { + harness.push(_createOperator("Op1", address(0x1), true)); + harness.setOperatorLimit(0, 10); + harness.setOperatorFunded(0, 10); + + OperatorsV1.CachedOperator[] memory fundable = harness.getAllFundable(); + + assertEq(fundable.length, 0, "Expected no fundable operators when limit == funded"); + } + + function testGetAllFundableNotFundableWhenInactive() public { + harness.push(_createOperator("Op1", address(0x1), false)); + harness.setOperatorLimit(0, 10); + + OperatorsV1.CachedOperator[] memory fundable = harness.getAllFundable(); + + assertEq(fundable.length, 0, "Expected no fundable operators when inactive"); + } + + function testGetAllFundableMixed() public { + // Op0: active, fundable (limit > funded) + harness.push(_createOperator("Op0", address(0x1), true)); + harness.setOperatorLimit(0, 10); + harness.setOperatorFunded(0, 5); + + // Op1: active, not fundable (limit == funded) + harness.push(_createOperator("Op1", address(0x2), true)); + harness.setOperatorLimit(1, 10); + harness.setOperatorFunded(1, 10); + + // Op2: inactive, would be fundable but inactive + harness.push(_createOperator("Op2", address(0x3), false)); + harness.setOperatorLimit(2, 10); + + // Op3: active, fundable + harness.push(_createOperator("Op3", address(0x4), true)); + harness.setOperatorLimit(3, 20); + harness.setOperatorFunded(3, 15); + + OperatorsV1.CachedOperator[] memory fundable = harness.getAllFundable(); + + assertEq(fundable.length, 2, "Expected 2 fundable operators"); + assertEq(fundable[0].index, 0, "First fundable should be index 0"); + assertEq(fundable[1].index, 3, "Second fundable should be index 3"); + } + + // ============ setKeys() tests ============ + + function testSetKeys() public { + harness.push(_createOperator("Op1", address(0x1), true)); + + uint256 blockBefore = block.number; + vm.roll(block.number + 10); + + harness.setKeys(0, 100); + + OperatorsV1.Operator memory op = harness.get(0); + assertEq(op.keys, 100, "Keys should be 100"); + assertEq(op.latestKeysEditBlockNumber, blockBefore + 10, "Block number should be updated"); + } + + function testSetKeysUpdatesBlockNumber() public { + harness.push(_createOperator("Op1", address(0x1), true)); + + vm.roll(50); + harness.setKeys(0, 10); + + OperatorsV1.Operator memory op = harness.get(0); + assertEq(op.latestKeysEditBlockNumber, 50, "Block number should be 50"); + + vm.roll(100); + harness.setKeys(0, 20); + + op = harness.get(0); + assertEq(op.latestKeysEditBlockNumber, 100, "Block number should be updated to 100"); + assertEq(op.keys, 20, "Keys should be 20"); + } + + function testSetKeysOperatorNotFoundReverts() public { + vm.expectRevert(abi.encodeWithSignature("OperatorNotFound(uint256)", 0)); + harness.setKeys(0, 100); + } + + // ============ _hasFundableKeys() tests ============ + + function testHasFundableKeysActiveWithAvailableKeys() public { + OperatorsV1.Operator memory op = OperatorsV1.Operator({ + active: true, + name: "Op1", + operator: address(0x1), + limit: 10, + funded: 5, + keys: 10, + stopped: 0, + latestKeysEditBlockNumber: 0 + }); + + assertTrue(harness.hasFundableKeys(op), "Should be fundable"); + } + + function testHasFundableKeysInactive() public { + OperatorsV1.Operator memory op = OperatorsV1.Operator({ + active: false, + name: "Op1", + operator: address(0x1), + limit: 10, + funded: 5, + keys: 10, + stopped: 0, + latestKeysEditBlockNumber: 0 + }); + + assertFalse(harness.hasFundableKeys(op), "Should not be fundable when inactive"); + } + + function testHasFundableKeysLimitEqualsFunded() public { + OperatorsV1.Operator memory op = OperatorsV1.Operator({ + active: true, + name: "Op1", + operator: address(0x1), + limit: 10, + funded: 10, + keys: 10, + stopped: 0, + latestKeysEditBlockNumber: 0 + }); + + assertFalse(harness.hasFundableKeys(op), "Should not be fundable when limit == funded"); + } + + function testHasFundableKeysZeroLimit() public { + OperatorsV1.Operator memory op = OperatorsV1.Operator({ + active: true, + name: "Op1", + operator: address(0x1), + limit: 0, + funded: 0, + keys: 10, + stopped: 0, + latestKeysEditBlockNumber: 0 + }); + + assertFalse(harness.hasFundableKeys(op), "Should not be fundable when limit is 0"); + } +} From bb3d3f86035cf2872113788821f5bda19057c66c Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Fri, 6 Feb 2026 13:58:46 +0000 Subject: [PATCH 30/60] fix: unintended change --- certora/specs/OperatorRegistryV1ValidatorStates.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/specs/OperatorRegistryV1ValidatorStates.spec b/certora/specs/OperatorRegistryV1ValidatorStates.spec index ed45c69e..6b21ded9 100644 --- a/certora/specs/OperatorRegistryV1ValidatorStates.spec +++ b/certora/specs/OperatorRegistryV1ValidatorStates.spec @@ -457,7 +457,7 @@ rule validatorStateTransition_4_3_M13(method f, env e, calldataarg args) filtere require getKeysCount(opIndex) <= 4; //should not be higher than loop_iter uint stateBefore = getValidatorState(opIndex, validatorData); f(e, args); - uint stateAfter = getValidatorStateByIndex(opIndex, validatorData); + uint stateAfter = getValidatorState(opIndex, validatorData); assert (stateAfter == 4) => (stateBefore == 3 || stateBefore == 4); } From e3b06a3dddb8bc841d133c0c4b40f00c778bddec Mon Sep 17 00:00:00 2001 From: iamsahu Date: Fri, 6 Feb 2026 16:23:39 +0100 Subject: [PATCH 31/60] chore: tests added for coverage --- contracts/src/OperatorsRegistry.1.sol | 4 +- contracts/test/OperatorsRegistry.1.t.sol | 166 ++++++++++++++++++++++- 2 files changed, 166 insertions(+), 4 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index d8a795a2..ed81b1de 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -479,7 +479,7 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); uint256 prevOperatorIndex = 0; uint256 suppliedExitCount = 0; - + // Check that the exits requested do not exceed the funded validator count of the operator for (uint256 i = 0; i < allocationsLength; ++i) { uint256 operatorIndex = _allocations[i].operatorIndex; @@ -512,7 +512,7 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab uint256 savedCurrentValidatorExitsDemand = currentValidatorExitsDemand; currentValidatorExitsDemand -= suppliedExitCount; - + uint256 totalRequestedExitsValue = TotalValidatorExitsRequested.get(); _setTotalValidatorExitsRequested(totalRequestedExitsValue, totalRequestedExitsValue + suppliedExitCount); _setCurrentValidatorExitsDemand(savedCurrentValidatorExitsDemand, currentValidatorExitsDemand); diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 3f0cbb1c..f3615a39 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -1944,6 +1944,169 @@ contract OperatorsRegistryV1TestDistribution is Test { event SetTotalValidatorExitsRequested(uint256 previousTotalRequestedExits, uint256 newTotalRequestedExits); + function testNonKeeperCantRequestExits() external { + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + vm.expectRevert(abi.encodeWithSignature("OnlyKeeper()")); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } + + function testRequestExitsWithInactiveOperator() external { + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.prank(admin); + operatorsRegistry.setOperatorStatus(0, false); + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 0)); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } + + function testRequestExitsWithMoreRequestsThanDemand() external { + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + assert(operatorsRegistry.getOperator(0).funded == 50); + assert(operatorsRegistry.getOperator(1).funded == 50); + assert(operatorsRegistry.getOperator(2).funded == 50); + assert(operatorsRegistry.getOperator(3).funded == 50); + assert(operatorsRegistry.getOperator(4).funded == 50); + + RiverMock(address(river)).sudoSetDepositedValidatorsCount(250); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); + + limits[0] = 60; + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("ExitsRequestedExceedsFundedCount(uint256,uint256,uint256)", 0, 60, 50)); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } + + function testRequestExitsWithUnorderedOperators() external { + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + assert(operatorsRegistry.getOperator(0).funded == 50); + assert(operatorsRegistry.getOperator(1).funded == 50); + assert(operatorsRegistry.getOperator(2).funded == 50); + assert(operatorsRegistry.getOperator(3).funded == 50); + assert(operatorsRegistry.getOperator(4).funded == 50); + + RiverMock(address(river)).sudoSetDepositedValidatorsCount(250); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); + + operators[0] = 1; + operators[1] = 0; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } + function testRegularExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); @@ -2110,8 +2273,7 @@ contract OperatorsRegistryV1TestDistribution is Test { } function testRequestValidatorNoExits() external { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](0); + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](0); vm.expectRevert(abi.encodeWithSignature("InvalidEmptyArray()")); operatorsRegistry.requestValidatorExits(allocations); } From a5836a05ca7501ff556cbd74a506a59fd2e585e6 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Fri, 6 Feb 2026 17:31:38 +0100 Subject: [PATCH 32/60] chore: tests fixed --- contracts/src/OperatorsRegistry.1.sol | 93 ---- contracts/test/OperatorsRegistry.1.t.sol | 551 ++--------------------- 2 files changed, 34 insertions(+), 610 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index ed81b1de..6c1ae48c 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -804,99 +804,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab return _operator.funded - (_operator.requestedExits + _operator.picked); } - /// @notice Internal utility to pick the next validator counts to exit for every operator - /// @param _count The count of validators to request exits for - function _pickNextValidatorsToExitFromActiveOperators(uint256 _count) internal returns (uint256) { - (OperatorsV2.CachedExitableOperator[] memory operators, uint256 exitableOperatorCount) = - OperatorsV2.getAllExitable(); - - if (exitableOperatorCount == 0) { - return 0; - } - - uint256 initialExitRequestDemand = _count; - uint256 totalRequestedExitsValue = TotalValidatorExitsRequested.get(); - uint256 totalRequestedExitsCopy = totalRequestedExitsValue; - - // we loop to find the highest count of active validators, the number of operators that have this amount and the second highest amount - while (_count > 0) { - uint32 highestActiveCount = 0; - uint32 secondHighestActiveCount = 0; - uint32 siblings = 0; - - for (uint256 idx = 0; idx < exitableOperatorCount;) { - uint32 activeCount = _getActiveValidatorCountForExitRequests(operators[idx]); - - if (activeCount == highestActiveCount) { - unchecked { - ++siblings; - } - } else if (activeCount > highestActiveCount) { - secondHighestActiveCount = highestActiveCount; - highestActiveCount = activeCount; - siblings = 1; - } else if (activeCount > secondHighestActiveCount) { - secondHighestActiveCount = activeCount; - } - - unchecked { - ++idx; - } - } - - // we exited all exitable validators - if (highestActiveCount == 0) { - break; - } - // The optimal amount is how much we should dispatch to all the operators with the highest count for them to get the same amount - // of active validators as the second highest count. We then take the minimum between this value and the total we need to exit - uint32 optimalTotalDispatchCount = - uint32(LibUint256.min((highestActiveCount - secondHighestActiveCount) * siblings, _count)); - - // We lookup the operators again to assign the exit requests - uint256 rest = optimalTotalDispatchCount % siblings; - uint32 baseExitRequestAmount = optimalTotalDispatchCount / siblings; - for (uint256 idx = 0; idx < exitableOperatorCount;) { - if (_getActiveValidatorCountForExitRequests(operators[idx]) == highestActiveCount) { - uint32 additionalRequestedExits = baseExitRequestAmount + (rest > 0 ? 1 : 0); - operators[idx].picked += additionalRequestedExits; - if (rest > 0) { - unchecked { - --rest; - } - } - } - unchecked { - ++idx; - } - } - - totalRequestedExitsValue += optimalTotalDispatchCount; - _count -= optimalTotalDispatchCount; - } - - // We loop over the operators and apply the change, also emit the exit request event - for (uint256 idx = 0; idx < exitableOperatorCount;) { - if (operators[idx].picked > 0) { - uint256 opIndex = operators[idx].index; - uint32 newRequestedExits = operators[idx].requestedExits + operators[idx].picked; - - OperatorsV2.get(opIndex).requestedExits = newRequestedExits; - emit RequestedValidatorExits(opIndex, newRequestedExits); - } - - unchecked { - ++idx; - } - } - - if (totalRequestedExitsValue != totalRequestedExitsCopy) { - _setTotalValidatorExitsRequested(totalRequestedExitsCopy, totalRequestedExitsValue); - } - - return initialExitRequestDemand - _count; - } - /// @notice Internal utility to set the total validator exits requested by the system /// @param _currentValue The current value of the total validator exits requested /// @param _newValue The new value of the total validator exits requested diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index f3615a39..f5c31607 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -25,7 +25,8 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { } function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { - return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); + return 0; + // return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); } function sudoSetKeys(uint256 _operatorIndex, uint32 _keyCount) external { @@ -1895,6 +1896,15 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(operatorsRegistry.getOperator(3).funded == 10); assert(operatorsRegistry.getOperator(4).funded == 10); + vm.prank(river); + operatorsRegistry.demandValidatorExits(50, 250); + + limits[0] = 10; + limits[1] = 10; + limits[2] = 10; + limits[3] = 10; + limits[4] = 10; + vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(0, 10); vm.expectEmit(true, true, true, true); @@ -1905,7 +1915,8 @@ contract OperatorsRegistryV1TestDistribution is Test { emit RequestedValidatorExits(3, 10); vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).requestedExits == 10); assert(operatorsRegistry.getOperator(1).requestedExits == 10); @@ -2312,9 +2323,19 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(operatorsRegistry.getOperator(3).funded == 50); assert(operatorsRegistry.getOperator(4).funded == 50); + vm.prank(river); + operatorsRegistry.demandValidatorExits(1, 250); + + limits[0] = 1; + limits[1] = 0; + limits[2] = 0; + limits[3] = 0; + limits[4] = 0; + vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(0, 1); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(1); + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).requestedExits == 1); assert(operatorsRegistry.getOperator(1).requestedExits == 0); @@ -2330,328 +2351,7 @@ contract OperatorsRegistryV1TestDistribution is Test { event SetCurrentValidatorExitsDemand(uint256 previousValidatorExitsDemand, uint256 nextValidatorExitsDemand); - function testExitDistributionWithCatchupToStoppedAlreadyExistingArray() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); - vm.stopPrank(); - - uint32[] memory limits = new uint32[](5); - limits[0] = 50; - limits[1] = 50; - limits[2] = 50; - limits[3] = 50; - limits[4] = 50; - - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 50); - assert(operatorsRegistry.getOperator(2).funded == 50); - assert(operatorsRegistry.getOperator(3).funded == 50); - assert(operatorsRegistry.getOperator(4).funded == 50); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - - assert(operatorsRegistry.getOperator(0).requestedExits == 10); - assert(operatorsRegistry.getOperator(1).requestedExits == 10); - assert(operatorsRegistry.getOperator(2).requestedExits == 10); - assert(operatorsRegistry.getOperator(3).requestedExits == 10); - assert(operatorsRegistry.getOperator(4).requestedExits == 10); - - uint32[] memory stoppedValidatorCounts = new uint32[](6); - stoppedValidatorCounts[0] = 50; - stoppedValidatorCounts[1] = 10; - stoppedValidatorCounts[2] = 10; - stoppedValidatorCounts[3] = 10; - stoppedValidatorCounts[4] = 10; - stoppedValidatorCounts[5] = 10; - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .sudoStoppedValidatorCounts(stoppedValidatorCounts, 50); - - OperatorsV2.Operator memory o = operatorsRegistry.getOperator(0); - assertEq(o.requestedExits, 10); - o = operatorsRegistry.getOperator(1); - assertEq(o.requestedExits, 10); - o = operatorsRegistry.getOperator(2); - assertEq(o.requestedExits, 10); - o = operatorsRegistry.getOperator(3); - assertEq(o.requestedExits, 10); - o = operatorsRegistry.getOperator(4); - assertEq(o.requestedExits, 10); - - stoppedValidatorCounts[0] = 65; - stoppedValidatorCounts[1] = 11; - stoppedValidatorCounts[2] = 12; - stoppedValidatorCounts[3] = 13; - stoppedValidatorCounts[4] = 14; - stoppedValidatorCounts[5] = 15; - - RiverMock(address(river)).sudoSetDepositedValidatorsCount(65); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(0, 10, 11); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(1, 10, 12); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(2, 10, 13); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(3, 10, 14); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(4, 10, 15); - vm.expectEmit(true, true, true, true); - emit SetTotalValidatorExitsRequested(50, 65); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .sudoStoppedValidatorCounts(stoppedValidatorCounts, 65); - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 65); - - o = operatorsRegistry.getOperator(0); - assertEq(o.requestedExits, 11); - o = operatorsRegistry.getOperator(1); - assertEq(o.requestedExits, 12); - o = operatorsRegistry.getOperator(2); - assertEq(o.requestedExits, 13); - o = operatorsRegistry.getOperator(3); - assertEq(o.requestedExits, 14); - o = operatorsRegistry.getOperator(4); - assertEq(o.requestedExits, 15); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 23); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 115); - - assert(operatorsRegistry.getOperator(0).requestedExits == 23); - assert(operatorsRegistry.getOperator(1).requestedExits == 23); - assert(operatorsRegistry.getOperator(2).requestedExits == 23); - assert(operatorsRegistry.getOperator(3).requestedExits == 23); - assert(operatorsRegistry.getOperator(4).requestedExits == 23); - - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 115); - } - - function testExitDistributionWithCatchupToStopped() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); - vm.stopPrank(); - - uint32[] memory limits = new uint32[](5); - limits[0] = 50; - limits[1] = 50; - limits[2] = 50; - limits[3] = 50; - limits[4] = 50; - - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 50); - assert(operatorsRegistry.getOperator(2).funded == 50); - assert(operatorsRegistry.getOperator(3).funded == 50); - assert(operatorsRegistry.getOperator(4).funded == 50); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - - assert(operatorsRegistry.getOperator(0).requestedExits == 10); - assert(operatorsRegistry.getOperator(1).requestedExits == 10); - assert(operatorsRegistry.getOperator(2).requestedExits == 10); - assert(operatorsRegistry.getOperator(3).requestedExits == 10); - assert(operatorsRegistry.getOperator(4).requestedExits == 10); - - uint32[] memory stoppedValidatorCounts = new uint32[](6); - stoppedValidatorCounts[0] = 65; - stoppedValidatorCounts[1] = 11; - stoppedValidatorCounts[2] = 12; - stoppedValidatorCounts[3] = 13; - stoppedValidatorCounts[4] = 14; - stoppedValidatorCounts[5] = 15; - - RiverMock(address(river)).sudoSetDepositedValidatorsCount(65); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(0, 10, 11); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(1, 10, 12); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(2, 10, 13); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(3, 10, 14); - vm.expectEmit(true, true, true, true); - emit UpdatedRequestedValidatorExitsUponStopped(4, 10, 15); - vm.expectEmit(true, true, true, true); - emit SetTotalValidatorExitsRequested(50, 65); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .sudoStoppedValidatorCounts(stoppedValidatorCounts, 65); - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 65); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 23); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 23); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 115); - - assert(operatorsRegistry.getOperator(0).requestedExits == 23); - assert(operatorsRegistry.getOperator(1).requestedExits == 23); - assert(operatorsRegistry.getOperator(2).requestedExits == 23); - assert(operatorsRegistry.getOperator(3).requestedExits == 23); - assert(operatorsRegistry.getOperator(4).requestedExits == 23); - - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 115); - } - - function testExitDistributionWithCatchupToStoppedAndUnexitableOperators() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - vm.stopPrank(); - - { - uint32[] memory limits = new uint32[](1); - limits[0] = 50; - - uint256[] memory operators = new uint256[](1); - operators[0] = 0; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - } - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 0); - assert(operatorsRegistry.getOperator(2).funded == 0); - assert(operatorsRegistry.getOperator(3).funded == 0); - assert(operatorsRegistry.getOperator(4).funded == 0); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 50); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - - assert(operatorsRegistry.getOperator(0).requestedExits == 50); - assert(operatorsRegistry.getOperator(1).requestedExits == 0); - assert(operatorsRegistry.getOperator(2).requestedExits == 0); - assert(operatorsRegistry.getOperator(3).requestedExits == 0); - assert(operatorsRegistry.getOperator(4).requestedExits == 0); - - uint32[] memory stoppedValidatorCounts = new uint32[](6); - stoppedValidatorCounts[0] = 50; - stoppedValidatorCounts[1] = 50; - stoppedValidatorCounts[2] = 0; - stoppedValidatorCounts[3] = 0; - stoppedValidatorCounts[4] = 0; - stoppedValidatorCounts[5] = 0; - - RiverMock(address(river)).sudoSetDepositedValidatorsCount(65); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .sudoStoppedValidatorCounts(stoppedValidatorCounts, 50); - - vm.startPrank(admin); - operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); - vm.stopPrank(); - - { - uint32[] memory limits = new uint32[](2); - limits[0] = 50; - limits[1] = 50; - - uint256[] memory operators = new uint256[](2); - operators[0] = 1; - operators[1] = 2; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - } - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 25); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 25); - assertEq( - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToExitFromActiveOperators(50), - 50 - ); - assert(operatorsRegistry.getOperator(0).requestedExits == 50); - assert(operatorsRegistry.getOperator(1).requestedExits == 25); - assert(operatorsRegistry.getOperator(2).requestedExits == 25); - assert(operatorsRegistry.getOperator(3).requestedExits == 0); - assert(operatorsRegistry.getOperator(4).requestedExits == 0); - } - - function testMoreThanMaxExitDistribution() external { + function testUnevenExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2685,117 +2385,14 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(operatorsRegistry.getOperator(3).funded == 50); assert(operatorsRegistry.getOperator(4).funded == 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 50); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToExitFromActiveOperators(500); - - assert(operatorsRegistry.getOperator(0).requestedExits == 50); - assert(operatorsRegistry.getOperator(1).requestedExits == 50); - assert(operatorsRegistry.getOperator(2).requestedExits == 50); - assert(operatorsRegistry.getOperator(3).requestedExits == 50); - assert(operatorsRegistry.getOperator(4).requestedExits == 50); - - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 250); - } - - function testMoreThanMaxExitDistributionOnUnevenSetup() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(1, 40, genBytes((48 + 96) * 40)); - operatorsRegistry.addValidators(2, 30, genBytes((48 + 96) * 30)); - operatorsRegistry.addValidators(3, 20, genBytes((48 + 96) * 20)); - operatorsRegistry.addValidators(4, 10, genBytes((48 + 96) * 10)); - vm.stopPrank(); - - uint32[] memory limits = new uint32[](5); - limits[0] = 50; - limits[1] = 40; - limits[2] = 30; - limits[3] = 20; - limits[4] = 10; - - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 40); - assert(operatorsRegistry.getOperator(2).funded == 30); - assert(operatorsRegistry.getOperator(3).funded == 20); - assert(operatorsRegistry.getOperator(4).funded == 10); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 40); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToExitFromActiveOperators(500); - - assert(operatorsRegistry.getOperator(0).requestedExits == 50); - assert(operatorsRegistry.getOperator(1).requestedExits == 40); - assert(operatorsRegistry.getOperator(2).requestedExits == 30); - assert(operatorsRegistry.getOperator(3).requestedExits == 20); - assert(operatorsRegistry.getOperator(4).requestedExits == 10); - - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 150); - } - - function testUnevenExitDistribution() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); - vm.stopPrank(); - - uint32[] memory limits = new uint32[](5); - limits[0] = 50; - limits[1] = 50; - limits[2] = 50; - limits[3] = 50; - limits[4] = 50; - - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); + vm.prank(river); + operatorsRegistry.demandValidatorExits(14, 250); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 50); - assert(operatorsRegistry.getOperator(2).funded == 50); - assert(operatorsRegistry.getOperator(3).funded == 50); - assert(operatorsRegistry.getOperator(4).funded == 50); + limits[0] = 3; + limits[1] = 3; + limits[2] = 3; + limits[3] = 3; + limits[4] = 2; vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(0, 3); @@ -2807,7 +2404,8 @@ contract OperatorsRegistryV1TestDistribution is Test { emit RequestedValidatorExits(3, 3); vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(4, 2); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(14); + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).requestedExits == 3); assert(operatorsRegistry.getOperator(1).requestedExits == 3); @@ -2818,87 +2416,6 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(operatorsRegistry.getTotalValidatorExitsRequested() == 14); } - function testExitDistributionUnevenFunded() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(1, 40, genBytes((48 + 96) * 40)); - operatorsRegistry.addValidators(2, 30, genBytes((48 + 96) * 30)); - operatorsRegistry.addValidators(3, 30, genBytes((48 + 96) * 30)); - operatorsRegistry.addValidators(4, 10, genBytes((48 + 96) * 10)); - vm.stopPrank(); - - uint32[] memory limits = new uint32[](5); - limits[0] = 50; - limits[1] = 40; - limits[2] = 30; - limits[3] = 30; - limits[4] = 10; - - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 40); - assert(operatorsRegistry.getOperator(2).funded == 30); - assert(operatorsRegistry.getOperator(3).funded == 30); - assert(operatorsRegistry.getOperator(4).funded == 10); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(30); - - assert(operatorsRegistry.getOperator(0).requestedExits == 20); - assert(operatorsRegistry.getOperator(1).requestedExits == 10); - assert(operatorsRegistry.getOperator(2).requestedExits == 0); - assert(operatorsRegistry.getOperator(3).requestedExits == 0); - assert(operatorsRegistry.getOperator(4).requestedExits == 0); - - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 30); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(40); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 40); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 20); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(40); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 40); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - } - function testDecreasingStoppedValidatorCounts(uint8 decreasingIndex, uint8[5] memory fuzzedStoppedValidatorCount) external { From 74901b4296ad0fc27627391ee3c9aa7c03f76780 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Sat, 7 Feb 2026 13:10:54 +0100 Subject: [PATCH 33/60] chore: coverage complete --- contracts/src/OperatorsRegistry.1.sol | 11 ------ contracts/test/OperatorsRegistry.1.t.sol | 48 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 6c1ae48c..56c91516 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -793,17 +793,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab revert InactiveOperator(_operatorIndex); } - /// @notice Internal utility to get the count of active validators during the exit selection process - /// @param _operator The Operator structure in memory - /// @return The count of active validators for the operator - function _getActiveValidatorCountForExitRequests(OperatorsV2.CachedExitableOperator memory _operator) - internal - pure - returns (uint32) - { - return _operator.funded - (_operator.requestedExits + _operator.picked); - } - /// @notice Internal utility to set the total validator exits requested by the system /// @param _currentValue The current value of the total validator exits requested /// @param _newValue The new value of the total validator exits requested diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index f5c31607..f4631570 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2066,6 +2066,54 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } + function testRequestExitsRequestedExceedsDemand() external { + vm.startPrank(admin); + operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(2, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(3, 50, genBytes((48 + 96) * 50)); + operatorsRegistry.addValidators(4, 50, genBytes((48 + 96) * 50)); + vm.stopPrank(); + + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + assert(operatorsRegistry.getOperator(0).funded == 50); + assert(operatorsRegistry.getOperator(1).funded == 50); + assert(operatorsRegistry.getOperator(2).funded == 50); + assert(operatorsRegistry.getOperator(3).funded == 50); + assert(operatorsRegistry.getOperator(4).funded == 50); + + RiverMock(address(river)).sudoSetDepositedValidatorsCount(250); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(10, 250); + + limits[0] = 50; + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("ExitsRequestedExceedsDemand(uint256,uint256)", 250, 10)); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } + function testRequestExitsWithUnorderedOperators() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); From 476c9800df906e1bcaf4635a22fe11ca8dcdd323 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Mon, 9 Feb 2026 12:16:25 +0100 Subject: [PATCH 34/60] chore: test additions --- contracts/src/OperatorsRegistry.1.sol | 6 ++++- contracts/test/OperatorsRegistry.1.t.sol | 30 +++++++++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 56c91516..3e2e192e 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -467,6 +467,11 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab /// @inheritdoc IOperatorsRegistryV1 function requestValidatorExits(OperatorAllocation[] calldata _allocations) external { uint256 allocationsLength = _allocations.length; + uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); + + if (currentValidatorExitsDemand == 0) { + revert NoExitRequestsToPerform(); + } if (allocationsLength == 0) { revert InvalidEmptyArray(); @@ -476,7 +481,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab revert IConsensusLayerDepositManagerV1.OnlyKeeper(); } - uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); uint256 prevOperatorIndex = 0; uint256 suppliedExitCount = 0; diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index f4631570..64eaf573 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -24,11 +24,6 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { return _pickNextValidatorsToDepositFromActiveOperators(_allocations); } - function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { - return 0; - // return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); - } - function sudoSetKeys(uint256 _operatorIndex, uint32 _keyCount) external { OperatorsV2.setKeys(_operatorIndex, _keyCount); } @@ -1985,6 +1980,25 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } + function testRequestValidatorNoExits() external { + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; + + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; + vm.expectRevert(abi.encodeWithSignature("NoExitRequestsToPerform()")); + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } + function testRequestExitsWithInactiveOperator() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); @@ -2331,12 +2345,6 @@ contract OperatorsRegistryV1TestDistribution is Test { assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 250); } - function testRequestValidatorNoExits() external { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](0); - vm.expectRevert(abi.encodeWithSignature("InvalidEmptyArray()")); - operatorsRegistry.requestValidatorExits(allocations); - } - function testOneExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); From 262a4230919fd3475c2e37ccb4617e0b877a03f7 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Mon, 9 Feb 2026 12:34:10 +0100 Subject: [PATCH 35/60] chore: fixed tests --- contracts/src/OperatorsRegistry.1.sol | 8 +-- contracts/test/OperatorsRegistry.1.t.sol | 84 +----------------------- 2 files changed, 7 insertions(+), 85 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index c30c9cb8..974d1a9e 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -469,6 +469,10 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab uint256 allocationsLength = _allocations.length; uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); + if (msg.sender != IConsensusLayerDepositManagerV1(RiverAddress.get()).getKeeper()) { + revert IConsensusLayerDepositManagerV1.OnlyKeeper(); + } + if (currentValidatorExitsDemand == 0) { revert NoExitRequestsToPerform(); } @@ -477,10 +481,6 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab revert InvalidEmptyArray(); } - if (msg.sender != IConsensusLayerDepositManagerV1(RiverAddress.get()).getKeeper()) { - revert IConsensusLayerDepositManagerV1.OnlyKeeper(); - } - uint256 prevOperatorIndex = 0; uint256 suppliedExitCount = 0; diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index d85cd1ad..265a8f4e 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2303,6 +2303,9 @@ contract OperatorsRegistryV1TestDistribution is Test { operators[3] = 3; operators[4] = 4; + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); + vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); vm.prank(admin); @@ -2753,87 +2756,6 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(operatorsRegistry.getTotalValidatorExitsRequested() == 14); } - function testExitDistributionUnevenFunded() external { - vm.startPrank(admin); - operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); - operatorsRegistry.addValidators(1, 40, genBytes((48 + 96) * 40)); - operatorsRegistry.addValidators(2, 30, genBytes((48 + 96) * 30)); - operatorsRegistry.addValidators(3, 30, genBytes((48 + 96) * 30)); - operatorsRegistry.addValidators(4, 10, genBytes((48 + 96) * 10)); - vm.stopPrank(); - - uint32[] memory limits = new uint32[](5); - limits[0] = 50; - limits[1] = 40; - limits[2] = 30; - limits[3] = 30; - limits[4] = 10; - - uint256[] memory operators = new uint256[](5); - operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - operators[3] = 3; - operators[4] = 4; - - vm.prank(admin); - operatorsRegistry.setOperatorLimits(operators, limits, block.number); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - assert(operatorsRegistry.getOperator(0).funded == 50); - assert(operatorsRegistry.getOperator(1).funded == 40); - assert(operatorsRegistry.getOperator(2).funded == 30); - assert(operatorsRegistry.getOperator(3).funded == 30); - assert(operatorsRegistry.getOperator(4).funded == 10); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(30); - - assert(operatorsRegistry.getOperator(0).requestedExits == 20); - assert(operatorsRegistry.getOperator(1).requestedExits == 10); - assert(operatorsRegistry.getOperator(2).requestedExits == 0); - assert(operatorsRegistry.getOperator(3).requestedExits == 0); - assert(operatorsRegistry.getOperator(4).requestedExits == 0); - - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 30); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 10); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(40); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 40); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 20); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 20); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(40); - - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 40); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 30); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); - } - function testDecreasingStoppedValidatorCounts(uint8 decreasingIndex, uint8[5] memory fuzzedStoppedValidatorCount) external { From 1474ee7ca7388963ca22e9ca4137f005e5c6e08a Mon Sep 17 00:00:00 2001 From: juliaaschmidt <121238805+juliaaschmidt@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:00:58 +0000 Subject: [PATCH 36/60] fix: Certora Prover Verifier Rules To Match New Function Signatures (#340) * fix(certora): update pickNextValidatorsToDeposit signature for BYOV Update Certora specs to reflect the new function signature that takes OperatorAllocation[] instead of uint256 count. Also removes the unused getMaxValidatorAttributionPerRound harness function. * fix(certora): update depositToConsensusLayerWithDepositRoot signature Update function signature from (uint256, bytes32) to (OperatorAllocation[], bytes32) to match the BYOV deposit changes. * fix(certora): add via-ir compilation and update config for BYOV Add solc_via_ir and optimizer settings to resolve stack-too-deep errors. Update configs to use OperatorsRegistryV1Harness for proper type resolution with the new OperatorAllocation struct parameter types. * bug-fix: add solc_via_ir to CI config files where needed * fix(certora): update pickNextValidatorsToDeposit call sites to use OperatorAllocation[] Update rule call sites in startingValidatorsDecreasesDiscrepancy and witness4_3StartingValidatorsDecreasesDiscrepancy to pass IOperatorsRegistryV1.OperatorAllocation[] instead of uint count, matching the new function signature. * fix(certora): add missing loop increment in getValidatorState harness The loop in getValidatorState was missing the valIndex increment, causing it to only ever check index 0 and potentially infinite loop. * bug-fix: see if removing via ir fixes rule bug --------- Co-authored-by: juliaaschmidt Co-authored-by: juuliaaschmidt --- certora/conf/OperatorRegistryV1.conf | 2 ++ certora/conf/RedeemManagerV1.conf | 2 ++ certora/conf/RiverV1.conf | 4 +++- certora/conf/SharesManagerV1.conf | 4 +++- certora/confs_for_CI/RedeemManagerV1.conf | 2 ++ .../harness/OperatorsRegistryV1Harness.sol | 6 +---- certora/specs/Base.spec | 4 ++-- certora/specs/OperatorRegistryV1.spec | 6 ++--- certora/specs/OperatorRegistryV1_base.spec | 6 ++--- .../OperatorRegistryV1_finishedRules.spec | 8 +++---- certora/specs/OperatorRegistryV1_orig.spec | 16 ++++++------- certora/specs/SharesManagerV1.spec | 2 +- .../OperatorRegistryV1_for_CI_3.spec | 24 +++++++++---------- certora/specs_for_CI/River_base.spec | 2 +- 14 files changed, 47 insertions(+), 41 deletions(-) diff --git a/certora/conf/OperatorRegistryV1.conf b/certora/conf/OperatorRegistryV1.conf index 2233a6ed..1c4054a9 100644 --- a/certora/conf/OperatorRegistryV1.conf +++ b/certora/conf/OperatorRegistryV1.conf @@ -14,6 +14,8 @@ "optimistic_hashing": true, "optimistic_fallback": true, "solc": "solc8.20", + "solc_via_ir": true, + "solc_optimize": "200", //"multi_assert_check": true, "smt_timeout": "5000", "prover_args": [ diff --git a/certora/conf/RedeemManagerV1.conf b/certora/conf/RedeemManagerV1.conf index 100b60fe..04c2fc08 100644 --- a/certora/conf/RedeemManagerV1.conf +++ b/certora/conf/RedeemManagerV1.conf @@ -17,6 +17,8 @@ "optimistic_loop": true, "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "solc": "solc8.20", + "solc_via_ir": true, + "solc_optimize": "200", "prover_args": [ " -contractRecursionLimit 1", // River.resolveRedeemRequests(uint32[]) calls RedeemManager.resolveRedeemRequests(uint32[]) " -recursionErrorAsAssert false", //RedeemManager._claimRedeemRequest() is recursive diff --git a/certora/conf/RiverV1.conf b/certora/conf/RiverV1.conf index 346861dd..e1f75921 100644 --- a/certora/conf/RiverV1.conf +++ b/certora/conf/RiverV1.conf @@ -4,7 +4,7 @@ "contracts/src/Allowlist.1.sol:AllowlistV1", "contracts/src/CoverageFund.1.sol:CoverageFundV1", "contracts/src/ELFeeRecipient.1.sol:ELFeeRecipientV1", - "contracts/src/OperatorsRegistry.1.sol:OperatorsRegistryV1", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", "certora/harness/RedeemManagerV1Harness.sol", "contracts/src/Withdraw.1.sol:WithdrawV1", "contracts/src/mock/DepositContractMock.sol", // This is needed only when working with the Ethereum network outside. @@ -19,6 +19,8 @@ "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "optimistic_hashing": true, "solc": "solc8.20", + "solc_via_ir": true, + "solc_optimize": "200", "global_timeout": "7000", // default contractRecursionLimit is 0, 1 needed because we have 2 functions of the same name diff --git a/certora/conf/SharesManagerV1.conf b/certora/conf/SharesManagerV1.conf index 556dbc08..0c27597f 100644 --- a/certora/conf/SharesManagerV1.conf +++ b/certora/conf/SharesManagerV1.conf @@ -4,7 +4,7 @@ "contracts/src/Allowlist.1.sol:AllowlistV1", "contracts/src/CoverageFund.1.sol:CoverageFundV1", "contracts/src/ELFeeRecipient.1.sol:ELFeeRecipientV1", - "contracts/src/OperatorsRegistry.1.sol:OperatorsRegistryV1", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", "certora/harness/RedeemManagerV1Harness.sol", "contracts/src/Withdraw.1.sol:WithdrawV1", "contracts/src/mock/DepositContractMock.sol", // This is needed only when working with the Ethereum network outside. @@ -17,6 +17,8 @@ "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "optimistic_hashing": true, "solc": "solc8.20", + "solc_via_ir": true, + "solc_optimize": "200", "global_timeout": "7198", // default contractRecursionLimit is 0, 1 needed because we have 2 functions of the same name "contract_recursion_limit": "1", // River.resolveRedeemRequests(uint32[]) calls RedeemManager.resolveRedeemRequests(uint32[]) diff --git a/certora/confs_for_CI/RedeemManagerV1.conf b/certora/confs_for_CI/RedeemManagerV1.conf index 79ec3e8e..eb96e798 100644 --- a/certora/confs_for_CI/RedeemManagerV1.conf +++ b/certora/confs_for_CI/RedeemManagerV1.conf @@ -15,6 +15,8 @@ "optimistic_loop": true, "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "optimistic_fallback": true, + "solc_via_ir": true, + "solc_optimize": "200", "contract_recursion_limit": "1", "prover_args": [ " -recursionErrorAsAssert false", diff --git a/certora/harness/OperatorsRegistryV1Harness.sol b/certora/harness/OperatorsRegistryV1Harness.sol index b7281546..1b118d02 100644 --- a/certora/harness/OperatorsRegistryV1Harness.sol +++ b/certora/harness/OperatorsRegistryV1Harness.sol @@ -14,10 +14,6 @@ contract OperatorsRegistryV1Harness is OperatorsRegistryV1 { return OperatorsV2.getCount(); } - function getMaxValidatorAttributionPerRound() external view returns (uint256) { - return MAX_VALIDATOR_ATTRIBUTION_PER_ROUND; - } - function getStoppedValidatorsLength() external view returns (uint256) { return OperatorsV2.getStoppedValidators().length; } @@ -42,7 +38,7 @@ contract OperatorsRegistryV1Harness is OperatorsRegistryV1 { OperatorsV2.Operator memory op = OperatorsV2.get(opIndex); uint256 valIndex = 0; bytes32 validatorDataHash = keccak256(abi.encodePacked(publicKeyAndSignature)); - for (; valIndex < op.keys;) + for (; valIndex < op.keys; ++valIndex) { (bytes memory valData) = ValidatorKeys.getRaw(opIndex, valIndex); if (validatorDataHash == keccak256(abi.encodePacked(valData))) //element found diff --git a/certora/specs/Base.spec b/certora/specs/Base.spec index 4de78f1f..616bcc68 100644 --- a/certora/specs/Base.spec +++ b/certora/specs/Base.spec @@ -66,7 +66,7 @@ methods { function RiverV1Harness.getCLValidatorCount() external returns(uint256) envfree; // RiverV1 : ConsensusLayerDepositManagerV1 - function _.depositToConsensusLayerWithDepositRoot(uint256, bytes32) external => DISPATCHER(true); + function _.depositToConsensusLayerWithDepositRoot(IOperatorsRegistryV1.OperatorAllocation[], bytes32) external => DISPATCHER(true); function RiverV1Harness.getDepositedValidatorCount() external returns(uint256) envfree; // WithdrawV1 @@ -83,7 +83,7 @@ methods { function OR.getStoppedAndRequestedExitCounts() external returns (uint32, uint256) envfree; function _.getStoppedAndRequestedExitCounts() external => DISPATCHER(true); function _.demandValidatorExits(uint256, uint256) external => DISPATCHER(true); - function _.pickNextValidatorsToDeposit(uint256) external => DISPATCHER(true); // has no effect - CERT-4615 + function _.pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]) external => DISPATCHER(true); // has no effect - CERT-4615 //function _.deposit(bytes,bytes,bytes,bytes32) external => DISPATCHER(true); // has no effect - CERT-4615 diff --git a/certora/specs/OperatorRegistryV1.spec b/certora/specs/OperatorRegistryV1.spec index 20ccc667..4132c920 100644 --- a/certora/specs/OperatorRegistryV1.spec +++ b/certora/specs/OperatorRegistryV1.spec @@ -129,7 +129,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) } { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 1; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -216,7 +216,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && f.selector != sig:requestValidatorExits(uint256).selector && - f.selector != sig:pickNextValidatorsToDeposit(uint256).selector && + f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -225,7 +225,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) invariant operatorsStatesRemainValid_LI2_pickNextValidatorsToDeposit(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && - !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(uint256).selector + !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector } // proves the invariant for reportStoppedValidatorCounts diff --git a/certora/specs/OperatorRegistryV1_base.spec b/certora/specs/OperatorRegistryV1_base.spec index 11789d62..5e8c102e 100644 --- a/certora/specs/OperatorRegistryV1_base.spec +++ b/certora/specs/OperatorRegistryV1_base.spec @@ -9,7 +9,7 @@ methods { function OR.getStoppedAndRequestedExitCounts() external returns (uint32, uint256) envfree; function _.getStoppedAndRequestedExitCounts() external => DISPATCHER(true); function _.demandValidatorExits(uint256, uint256) external => DISPATCHER(true); - //function _.pickNextValidatorsToDeposit(uint256) external => DISPATCHER(true); // has no effect - CERT-4615 + //function _.pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]) external => DISPATCHER(true); // has no effect - CERT-4615 //function _.deposit(bytes,bytes,bytes,bytes32) external => DISPATCHER(true); // has no effect - CERT-4615 @@ -30,7 +30,7 @@ methods { function OR.getActiveOperatorsCount() external returns (uint256) envfree; function OR.getOperatorsSaturationDiscrepancy() external returns (uint256) envfree; function OR.getKeysCount(uint256) external returns (uint256) envfree; - function OR.pickNextValidatorsToDeposit(uint256) external returns (bytes[] memory, bytes[] memory); + function OR.pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]) external returns (bytes[] memory, bytes[] memory); function OR.requestValidatorExits(uint256) external; function OR.setOperatorAddress(uint256, address) external; function OR.getOperatorsSaturationDiscrepancy(uint256, uint256) external returns (uint256) envfree; @@ -88,7 +88,7 @@ definition isMethodID(method f, uint ID) returns bool = (f.selector == sig:addOperator(string,address).selector && ID == 4) || (f.selector == sig:demandValidatorExits(uint256,uint256).selector && ID == 5) || (f.selector == sig:initOperatorsRegistryV1(address,address).selector && ID == 6) || - (f.selector == sig:pickNextValidatorsToDeposit(uint256).selector && ID == 7) || + (f.selector == sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && ID == 7) || (f.selector == sig:proposeAdmin(address).selector && ID == 8) || (f.selector == sig:removeValidators(uint256,uint256[]).selector && ID == 9) || (f.selector == sig:requestValidatorExits(uint256).selector && ID == 10) || diff --git a/certora/specs/OperatorRegistryV1_finishedRules.spec b/certora/specs/OperatorRegistryV1_finishedRules.spec index 3d2fbf56..9c72c5fc 100644 --- a/certora/specs/OperatorRegistryV1_finishedRules.spec +++ b/certora/specs/OperatorRegistryV1_finishedRules.spec @@ -49,7 +49,7 @@ invariant inactiveOperatorsRemainNotFunded(uint opIndex) (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 1; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -68,7 +68,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) } { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 1; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -181,7 +181,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && f.selector != sig:requestValidatorExits(uint256).selector && - f.selector != sig:pickNextValidatorsToDeposit(uint256).selector && + f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -190,7 +190,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) invariant operatorsStatesRemainValid_LI2_pickNextValidatorsToDeposit(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && - !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(uint256).selector + !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector } // proves the invariant for reportStoppedValidatorCounts diff --git a/certora/specs/OperatorRegistryV1_orig.spec b/certora/specs/OperatorRegistryV1_orig.spec index a231344f..99558a87 100644 --- a/certora/specs/OperatorRegistryV1_orig.spec +++ b/certora/specs/OperatorRegistryV1_orig.spec @@ -38,7 +38,7 @@ invariant validatorKeysRemainUnique_LI2( filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) } { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 2; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 2; } } @@ -73,9 +73,9 @@ rule startingValidatorsDecreasesDiscrepancy(env e) require getKeysCount(index1) < 5; require getKeysCount(index2) < 5; //counterexamples when the keys count overflows - uint count; - require count > 0 && count <= 3; - pickNextValidatorsToDeposit(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length > 0 && allocations.length <= 3; + pickNextValidatorsToDeposit(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); //uint256 keysAfter1; uint256 limitAfter1; uint256 fundedAfter1; uint256 requestedExitsAfter1; uint256 stoppedCountAfter1; bool activeAfter1; address operatorAfter1; @@ -84,7 +84,7 @@ rule startingValidatorsDecreasesDiscrepancy(env e) //keysAfter2, limitAfter2, fundedAfter2, requestedExitsAfter2, stoppedCountAfter2, activeAfter2, operatorAfter2 = getOperatorState(e, index2); assert discrepancyBefore > 0 => to_mathint(discrepancyBefore) >= - discrepancyAfter - count + 1; //getMaxValidatorAttributionPerRound(e); + discrepancyAfter - allocations.length + 1; } rule startingValidatorsNeverUsesSameValidatorTwice(env e) @@ -129,9 +129,9 @@ rule witness4_3StartingValidatorsDecreasesDiscrepancy(env e) require operatorStateIsValid(index2); uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - pickNextValidatorsToDeposit(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + pickNextValidatorsToDeposit(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); satisfy discrepancyBefore == 4 && discrepancyAfter == 3; } diff --git a/certora/specs/SharesManagerV1.spec b/certora/specs/SharesManagerV1.spec index e7d70920..3abe94b5 100644 --- a/certora/specs/SharesManagerV1.spec +++ b/certora/specs/SharesManagerV1.spec @@ -195,7 +195,7 @@ rule sharesBalanceChangesRestrictively(method f) filtered { rule pricePerShareChangesRespectively(method f) filtered { f -> !f.isView && f.selector != sig:initRiverV1_1(address,uint64,uint64,uint64,uint64,uint64,uint256,uint256,uint128,uint128).selector - && f.selector != sig:depositToConsensusLayerWithDepositRoot(uint256, bytes32).selector + && f.selector != sig:depositToConsensusLayerWithDepositRoot(IOperatorsRegistryV1.OperatorAllocation[], bytes32).selector && f.selector != sig:claimRedeemRequests(uint32[],uint32[]).selector && f.selector != sig:deposit().selector && f.selector != sig:depositAndTransfer(address).selector diff --git a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec index d63b5e33..0877763d 100644 --- a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec +++ b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec @@ -21,13 +21,13 @@ rule startingValidatorsDecreasesDiscrepancy(env e) require getKeysCount(index1) < 5; require getKeysCount(index2) < 5; - uint count; - require count > 0 && count <= 3; - pickNextValidatorsToDeposit(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length > 0 && allocations.length <= 3; + pickNextValidatorsToDeposit(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => to_mathint(discrepancyBefore) >= - discrepancyAfter - count + 1; // this conditions is fine as long as count <= MAX_VALIDATOR_ATTRIBUTION_PER_ROUND + discrepancyAfter - allocations.length + 1; } rule witness4_3StartingValidatorsDecreasesDiscrepancy(env e) @@ -38,9 +38,9 @@ rule witness4_3StartingValidatorsDecreasesDiscrepancy(env e) require operatorStateIsValid(index2); uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - pickNextValidatorsToDeposit(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + pickNextValidatorsToDeposit(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); satisfy discrepancyBefore == 4 && discrepancyAfter == 3; } @@ -119,7 +119,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) } { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 1; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -178,7 +178,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && f.selector != sig:requestValidatorExits(uint256).selector && - f.selector != sig:pickNextValidatorsToDeposit(uint256).selector && + f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -187,7 +187,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) invariant operatorsStatesRemainValid_LI2_pickNextValidatorsToDeposit(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && - !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(uint256).selector + !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector } // proves the invariant for reportStoppedValidatorCounts @@ -392,7 +392,7 @@ invariant validatorKeysRemainUnique_LI2( filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) } { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 2; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 2; } } @@ -784,7 +784,7 @@ invariant inactiveOperatorsRemainNotFunded(uint opIndex) (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(uint256 x) with(env e) { require x <= 1; } + preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } diff --git a/certora/specs_for_CI/River_base.spec b/certora/specs_for_CI/River_base.spec index 9f846d0a..6b678f0c 100644 --- a/certora/specs_for_CI/River_base.spec +++ b/certora/specs_for_CI/River_base.spec @@ -14,4 +14,4 @@ definition ignoredMethod(method f) returns bool = f.selector == sig:helper11_commitBalanceToDeposit(OracleManagerV1.ConsensusLayerDataReportingVariables).selector; definition excludedInCI(method f) returns bool = - f.selector == sig:depositToConsensusLayerWithDepositRoot(uint256, bytes32).selector; \ No newline at end of file + f.selector == sig:depositToConsensusLayerWithDepositRoot(IOperatorsRegistryV1.OperatorAllocation[], bytes32).selector; \ No newline at end of file From dbdc2b20dfe15cd9abf039152a38fef13fde42fc Mon Sep 17 00:00:00 2001 From: iamsahu Date: Mon, 9 Feb 2026 14:33:28 +0100 Subject: [PATCH 37/60] chore: test coverage --- contracts/test/OperatorsRegistry.1.t.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 265a8f4e..689643b8 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2464,6 +2464,15 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } + function testRequestExitsWithInvalidEmptyArray() external { + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("InvalidEmptyArray()")); + operatorsRegistry.requestValidatorExits(_createAllocation(new uint256[](0), new uint32[](0))); + } + function testRegularExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); From 9f566951b12356a12894a8b68ebde63269442517 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Mon, 9 Feb 2026 15:22:10 +0100 Subject: [PATCH 38/60] chore: removed unused code from OperatorsV2 --- .../state/operatorsRegistry/Operators.2.sol | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/contracts/src/state/operatorsRegistry/Operators.2.sol b/contracts/src/state/operatorsRegistry/Operators.2.sol index 3e8b4e9e..105fda99 100644 --- a/contracts/src/state/operatorsRegistry/Operators.2.sol +++ b/contracts/src/state/operatorsRegistry/Operators.2.sol @@ -213,48 +213,6 @@ library OperatorsV2 { return (fundableOperators, fundableCount); } - /// @notice Retrieve all the active and exitable operators - /// @dev This method will return a memory array of length equal to the number of operator, but only - /// @dev populated up to the exitable operator count, also returned by the method - /// @return The list of active and exitable operators - /// @return The count of active and exitable operators - function getAllExitable() internal view returns (CachedExitableOperator[] memory, uint256) { - bytes32 slot = OPERATORS_SLOT; - - SlotOperator storage r; - - // solhint-disable-next-line no-inline-assembly - assembly { - r.slot := slot - } - - uint256 exitableCount = 0; - uint256 operatorCount = r.value.length; - - CachedExitableOperator[] memory exitableOperators = new CachedExitableOperator[](operatorCount); - - for (uint256 idx = 0; idx < operatorCount;) { - if (_hasExitableKeys(r.value[idx])) { - Operator storage op = r.value[idx]; - exitableOperators[exitableCount] = CachedExitableOperator({ - funded: op.funded, requestedExits: op.requestedExits, index: uint32(idx), picked: 0 - }); - unchecked { - ++exitableCount; - } - } - unchecked { - ++idx; - } - } - - assembly ("memory-safe") { - mstore(exitableOperators, exitableCount) - } - - return (exitableOperators, exitableCount); - } - /// @notice Add a new operator in storage /// @param _newOperator Value of the new operator /// @return The size of the operator array after the operation @@ -292,13 +250,6 @@ library OperatorsV2 { return (_operator.active && _operator.limit > _operator.funded); } - /// @notice Checks if an operator is active and has exitable keys - /// @param _operator The operator details - /// @return True if active and exitable - function _hasExitableKeys(OperatorsV2.Operator memory _operator) internal pure returns (bool) { - return (_operator.active && _operator.funded > _operator.requestedExits); - } - /// @notice Storage slot of the Stopped Validators bytes32 internal constant STOPPED_VALIDATORS_SLOT = bytes32(uint256(keccak256("river.state.stoppedValidators")) - 1); From b20b8def14534b1ee52195607a168f475c5a53c4 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Mon, 9 Feb 2026 15:39:39 +0100 Subject: [PATCH 39/60] chore: check for 0 allocation --- contracts/src/OperatorsRegistry.1.sol | 3 +++ contracts/src/interfaces/IOperatorRegistry.1.sol | 4 +--- contracts/test/OperatorsRegistry.1.t.sol | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 974d1a9e..93d94cf2 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -488,6 +488,9 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab for (uint256 i = 0; i < allocationsLength; ++i) { uint256 operatorIndex = _allocations[i].operatorIndex; uint256 count = _allocations[i].validatorCount; + if (count == 0) { + revert AllocationWithZeroValidatorCount(); + } suppliedExitCount += count; if (i > 0 && !(operatorIndex > prevOperatorIndex)) { diff --git a/contracts/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index 346844a0..466024f8 100644 --- a/contracts/src/interfaces/IOperatorRegistry.1.sol +++ b/contracts/src/interfaces/IOperatorRegistry.1.sol @@ -355,9 +355,7 @@ interface IOperatorsRegistryV1 { external returns (bytes[] memory publicKeys, bytes[] memory signatures); - /// @notice Public endpoint to consume the exit request demand and perform the actual exit requests - /// @notice The selection algorithm will pick validators based on their active validator counts - /// @notice This value is computed by using the count of funded keys and taking into account the stopped validator counts and exit requests + /// @notice The keeper supplies explicit per-operator exit allocations to be performed /// @param _allocations The proposed allocations to exit function requestValidatorExits(OperatorAllocation[] calldata _allocations) external; diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 689643b8..42d5b89e 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2473,6 +2473,20 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.requestValidatorExits(_createAllocation(new uint256[](0), new uint32[](0))); } + function testRequestExitsWithAllocationWithZeroValidatorCount() external { + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); + + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory exitCounts = new uint32[](1); + exitCounts[0] = 0; + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, exitCounts)); + } + function testRegularExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); From 1590ef2e1eaeee9671170174242e8003e1418817 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Mon, 9 Feb 2026 16:35:37 +0100 Subject: [PATCH 40/60] chore: fix test --- contracts/test/OperatorsRegistry.1.t.sol | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 42d5b89e..f4d012a7 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2687,13 +2687,13 @@ contract OperatorsRegistryV1TestDistribution is Test { assert(operatorsRegistry.getOperator(4).funded == 50); vm.prank(river); - operatorsRegistry.demandValidatorExits(1, 250); + operatorsRegistry.demandValidatorExits(5, 250); limits[0] = 1; - limits[1] = 0; - limits[2] = 0; - limits[3] = 0; - limits[4] = 0; + limits[1] = 1; + limits[2] = 1; + limits[3] = 1; + limits[4] = 1; vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(0, 1); @@ -2701,11 +2701,11 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).requestedExits == 1); - assert(operatorsRegistry.getOperator(1).requestedExits == 0); - assert(operatorsRegistry.getOperator(2).requestedExits == 0); - assert(operatorsRegistry.getOperator(3).requestedExits == 0); - assert(operatorsRegistry.getOperator(4).requestedExits == 0); - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 1); + assert(operatorsRegistry.getOperator(1).requestedExits == 1); + assert(operatorsRegistry.getOperator(2).requestedExits == 1); + assert(operatorsRegistry.getOperator(3).requestedExits == 1); + assert(operatorsRegistry.getOperator(4).requestedExits == 1); + assert(operatorsRegistry.getTotalValidatorExitsRequested() == 5); } event UpdatedRequestedValidatorExitsUponStopped( From 4128357257ce6e86be05590427dd80382681540c Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Wed, 11 Feb 2026 09:00:50 +0000 Subject: [PATCH 41/60] fix: update solc version Operators.1.t.sol --- contracts/test/state/operatorsRegistry/Operators.1.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/state/operatorsRegistry/Operators.1.t.sol b/contracts/test/state/operatorsRegistry/Operators.1.t.sol index e7702b45..6bbcd9f1 100644 --- a/contracts/test/state/operatorsRegistry/Operators.1.t.sol +++ b/contracts/test/state/operatorsRegistry/Operators.1.t.sol @@ -1,6 +1,6 @@ //SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.20; +pragma solidity 0.8.33; import "forge-std/Test.sol"; From e21a489141c2a84c47035d437db034d3b3bb89df Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Wed, 11 Feb 2026 09:15:38 +0000 Subject: [PATCH 42/60] bugfix: bump prover RedeemManagerV1.conf solc to 0.8.33 --- certora/conf/RedeemManagerV1.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certora/conf/RedeemManagerV1.conf b/certora/conf/RedeemManagerV1.conf index 04c2fc08..f948b615 100644 --- a/certora/conf/RedeemManagerV1.conf +++ b/certora/conf/RedeemManagerV1.conf @@ -16,7 +16,7 @@ "loop_iter": "2", "optimistic_loop": true, "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], - "solc": "solc8.20", + "solc": "solc8.33", "solc_via_ir": true, "solc_optimize": "200", "prover_args": [ From e374dec851ac537cb9e769180a1a9a9c74ecfe00 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Wed, 11 Feb 2026 09:41:18 +0000 Subject: [PATCH 43/60] bug-fix: add JDK version 21 to Certora.yaml --- .github/workflows/Certora.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/Certora.yaml b/.github/workflows/Certora.yaml index ef5c9165..e32bbffb 100644 --- a/.github/workflows/Certora.yaml +++ b/.github/workflows/Certora.yaml @@ -42,6 +42,12 @@ jobs: - name: Install certora run: pip3 install certora-cli + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + - name: Install solc run: | pip install solc-select From edfe6bbc0efed1208df9ed8ee9f9232285c18a9e Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Wed, 11 Feb 2026 10:03:50 +0000 Subject: [PATCH 44/60] bugfix: remove solc optimizer step --- certora/conf/RedeemManagerV1.conf | 2 -- certora/confs_for_CI/RedeemManagerV1.conf | 2 -- 2 files changed, 4 deletions(-) diff --git a/certora/conf/RedeemManagerV1.conf b/certora/conf/RedeemManagerV1.conf index f948b615..60f02698 100644 --- a/certora/conf/RedeemManagerV1.conf +++ b/certora/conf/RedeemManagerV1.conf @@ -17,8 +17,6 @@ "optimistic_loop": true, "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "solc": "solc8.33", - "solc_via_ir": true, - "solc_optimize": "200", "prover_args": [ " -contractRecursionLimit 1", // River.resolveRedeemRequests(uint32[]) calls RedeemManager.resolveRedeemRequests(uint32[]) " -recursionErrorAsAssert false", //RedeemManager._claimRedeemRequest() is recursive diff --git a/certora/confs_for_CI/RedeemManagerV1.conf b/certora/confs_for_CI/RedeemManagerV1.conf index eb96e798..79ec3e8e 100644 --- a/certora/confs_for_CI/RedeemManagerV1.conf +++ b/certora/confs_for_CI/RedeemManagerV1.conf @@ -15,8 +15,6 @@ "optimistic_loop": true, "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "optimistic_fallback": true, - "solc_via_ir": true, - "solc_optimize": "200", "contract_recursion_limit": "1", "prover_args": [ " -recursionErrorAsAssert false", From 4b845200b9b97f81481a9d310f9e361af9ecbe86 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Wed, 11 Feb 2026 11:53:20 +0100 Subject: [PATCH 45/60] chore: suggestions --- contracts/src/OperatorsRegistry.1.sol | 19 +++++++++---------- .../state/operatorsRegistry/Operators.2.sol | 12 ------------ 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 93d94cf2..c938db54 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -481,22 +481,21 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab revert InvalidEmptyArray(); } - uint256 prevOperatorIndex = 0; - uint256 suppliedExitCount = 0; + uint256 requestedExitCount = 0; // Check that the exits requested do not exceed the funded validator count of the operator for (uint256 i = 0; i < allocationsLength; ++i) { uint256 operatorIndex = _allocations[i].operatorIndex; uint256 count = _allocations[i].validatorCount; + if (count == 0) { revert AllocationWithZeroValidatorCount(); } - suppliedExitCount += count; - - if (i > 0 && !(operatorIndex > prevOperatorIndex)) { + if (i > 0 && !(operatorIndex > _allocations[i - 1].operatorIndex)) { revert UnorderedOperatorList(); } - prevOperatorIndex = operatorIndex; + + requestedExitCount += count; OperatorsV2.Operator storage operator = OperatorsV2.get(operatorIndex); if (!operator.active) { @@ -513,15 +512,15 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab } // Check that the exits requested do not exceed the current validator exits demand - if (suppliedExitCount > currentValidatorExitsDemand) { - revert ExitsRequestedExceedsDemand(suppliedExitCount, currentValidatorExitsDemand); + if (requestedExitCount > currentValidatorExitsDemand) { + revert ExitsRequestedExceedsDemand(requestedExitCount, currentValidatorExitsDemand); } uint256 savedCurrentValidatorExitsDemand = currentValidatorExitsDemand; - currentValidatorExitsDemand -= suppliedExitCount; + currentValidatorExitsDemand -= requestedExitCount; uint256 totalRequestedExitsValue = TotalValidatorExitsRequested.get(); - _setTotalValidatorExitsRequested(totalRequestedExitsValue, totalRequestedExitsValue + suppliedExitCount); + _setTotalValidatorExitsRequested(totalRequestedExitsValue, totalRequestedExitsValue + requestedExitCount); _setCurrentValidatorExitsDemand(savedCurrentValidatorExitsDemand, currentValidatorExitsDemand); } diff --git a/contracts/src/state/operatorsRegistry/Operators.2.sol b/contracts/src/state/operatorsRegistry/Operators.2.sol index 105fda99..4d6a9278 100644 --- a/contracts/src/state/operatorsRegistry/Operators.2.sol +++ b/contracts/src/state/operatorsRegistry/Operators.2.sol @@ -46,18 +46,6 @@ library OperatorsV2 { uint32 picked; } - /// @notice The Operator structure when loaded in memory for the exit selection - struct CachedExitableOperator { - /// @custom:attribute The count of funded validators - uint32 funded; - /// @custom:attribute The count of exit requests made to this operator - uint32 requestedExits; - /// @custom:attribute The original index of the operator - uint32 index; - /// @custom:attribute The amount of picked keys, buffer used before changing funded in storage - uint32 picked; - } - /// @notice The structure at the storage slot struct SlotOperator { /// @custom:attribute Array containing all the operators From 2d9ae649e354db1aacf115431589c28b3bb14ec2 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Wed, 11 Feb 2026 14:18:14 +0100 Subject: [PATCH 46/60] chore: more suggestions --- contracts/src/OperatorsRegistry.1.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 085f6f96..2fb99748 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -485,11 +485,10 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab if (count > (operator.funded - operator.requestedExits)) { // Operator has insufficient funded validators revert ExitsRequestedExceedsFundedCount(operatorIndex, count, operator.funded); - } else { - // Operator has sufficient funded validators - operator.requestedExits += uint32(count); - emit RequestedValidatorExits(operatorIndex, operator.requestedExits); } + // Operator has sufficient funded validators + operator.requestedExits += uint32(count); + emit RequestedValidatorExits(operatorIndex, operator.requestedExits); } // Check that the exits requested do not exceed the current validator exits demand From a1278bb523523967e32e5d5919fd32f2a2ad9ef9 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Wed, 11 Feb 2026 14:40:17 +0100 Subject: [PATCH 47/60] chore: correction in error naming --- contracts/src/OperatorsRegistry.1.sol | 2 +- contracts/src/interfaces/IOperatorRegistry.1.sol | 2 +- contracts/test/OperatorsRegistry.1.t.sol | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 2fb99748..2d3c53c8 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -493,7 +493,7 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab // Check that the exits requested do not exceed the current validator exits demand if (requestedExitCount > currentValidatorExitsDemand) { - revert ExitsRequestedExceedsDemand(requestedExitCount, currentValidatorExitsDemand); + revert ExitsRequestedExceedDemand(requestedExitCount, currentValidatorExitsDemand); } uint256 savedCurrentValidatorExitsDemand = currentValidatorExitsDemand; diff --git a/contracts/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index ead9a189..73e5ab88 100644 --- a/contracts/src/interfaces/IOperatorRegistry.1.sol +++ b/contracts/src/interfaces/IOperatorRegistry.1.sol @@ -205,7 +205,7 @@ interface IOperatorsRegistryV1 { /// @notice The provided exit requests exceed the current exit request demand /// @param requested The requested count /// @param demand The demand count - error ExitsRequestedExceedsDemand(uint256 requested, uint256 demand); + error ExitsRequestedExceedDemand(uint256 requested, uint256 demand); /// @notice Initializes the operators registry /// @param _admin Admin in charge of managing operators diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 2e01210f..b9ff3018 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2370,7 +2370,7 @@ contract OperatorsRegistryV1TestDistribution is Test { operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } - function testRequestExitsRequestedExceedsDemand() external { + function testRequestExitsRequestedExceedDemand() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2414,7 +2414,7 @@ contract OperatorsRegistryV1TestDistribution is Test { limits[0] = 50; vm.prank(keeper); - vm.expectRevert(abi.encodeWithSignature("ExitsRequestedExceedsDemand(uint256,uint256)", 250, 10)); + vm.expectRevert(abi.encodeWithSignature("ExitsRequestedExceedDemand(uint256,uint256)", 250, 10)); operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } From 5c7e4b2719d22acf6855360ee3e1ad73eb35f342 Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Wed, 11 Feb 2026 14:27:44 +0000 Subject: [PATCH 48/60] feat: external test helper (review suggestion) --- contracts/test/Firewall.t.sol | 9 ++------ contracts/test/OperatorAllocationTestBase.sol | 14 +++++++++++++ .../ConsensusLayerDepositManager.1.t.sol | 21 +++++++------------ 3 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 contracts/test/OperatorAllocationTestBase.sol diff --git a/contracts/test/Firewall.t.sol b/contracts/test/Firewall.t.sol index 903879b8..aa2b361f 100644 --- a/contracts/test/Firewall.t.sol +++ b/contracts/test/Firewall.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.33; import "forge-std/Test.sol"; +import "./OperatorAllocationTestBase.sol"; import "./utils/BytesGenerator.sol"; import "./utils/LibImplementationUnbricker.sol"; import "./mocks/DepositContractMock.sol"; @@ -18,7 +19,7 @@ import "../src/Oracle.1.sol"; import "../src/OperatorsRegistry.1.sol"; import "../src/ELFeeRecipient.1.sol"; -contract FirewallTests is BytesGenerator, Test { +contract FirewallTests is BytesGenerator, OperatorAllocationTestBase { AllowlistV1 internal allowlist; ELFeeRecipientV1 internal elFeeRecipient; @@ -282,12 +283,6 @@ contract FirewallTests is BytesGenerator, Test { vm.stopPrank(); } - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } - function testGovernorCannotdepositToConsensusLayerWithDepositRoot() public { // Assert this by expecting NotEnoughFunds, NOT Unauthorized vm.startPrank(riverGovernorDAO); diff --git a/contracts/test/OperatorAllocationTestBase.sol b/contracts/test/OperatorAllocationTestBase.sol new file mode 100644 index 00000000..3114eb3b --- /dev/null +++ b/contracts/test/OperatorAllocationTestBase.sol @@ -0,0 +1,14 @@ +//SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.33; + +import "forge-std/Test.sol"; +import "../src/interfaces/IOperatorRegistry.1.sol"; + +abstract contract OperatorAllocationTestBase is Test { + function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return allocations; + } +} diff --git a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol index c73b5d8f..be0475bc 100644 --- a/contracts/test/components/ConsensusLayerDepositManager.1.t.sol +++ b/contracts/test/components/ConsensusLayerDepositManager.1.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.33; import "forge-std/Test.sol"; +import "../OperatorAllocationTestBase.sol"; import "../../src/components/ConsensusLayerDepositManager.1.sol"; import "../utils/LibImplementationUnbricker.sol"; @@ -11,14 +12,6 @@ import "../mocks/DepositContractMock.sol"; import "../mocks/DepositContractEnhancedMock.sol"; import "../mocks/DepositContractInvalidMock.sol"; -abstract contract ConsensusLayerDepositManagerTestBase is Test { - function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); - return allocations; - } -} - contract ConsensusLayerDepositManagerV1ExposeInitializer is ConsensusLayerDepositManagerV1 { function _getRiverAdmin() internal pure override returns (address) { return address(0); @@ -111,7 +104,7 @@ contract ConsensusLayerDepositManagerV1InitTests is Test { } } -contract ConsensusLayerDepositManagerV1Tests is ConsensusLayerDepositManagerTestBase { +contract ConsensusLayerDepositManagerV1Tests is OperatorAllocationTestBase { bytes32 internal withdrawalCredentials = bytes32(uint256(1)); ConsensusLayerDepositManagerV1 internal depositManager; @@ -286,7 +279,7 @@ contract ConsensusLayerDepositManagerV1ControllableValidatorKeyRequest is Consen } } -contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManagerTestBase { +contract ConsensusLayerDepositManagerV1ErrorTests is OperatorAllocationTestBase { bytes32 internal withdrawalCredentials = bytes32(uint256(1)); ConsensusLayerDepositManagerV1 internal depositManager; @@ -428,7 +421,7 @@ contract ConsensusLayerDepositManagerV1ErrorTests is ConsensusLayerDepositManage } } -contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is ConsensusLayerDepositManagerTestBase { +contract ConsensusLayerDepositManagerV1WithdrawalCredentialError is OperatorAllocationTestBase { bytes32 internal withdrawalCredentials = bytes32(uint256(1)); ConsensusLayerDepositManagerV1 internal depositManager; @@ -520,7 +513,7 @@ contract ConsensusLayerDepositManagerV1ValidKeys is ConsensusLayerDepositManager } } -contract ConsensusLayerDepositManagerV1ValidKeysTest is ConsensusLayerDepositManagerTestBase { +contract ConsensusLayerDepositManagerV1ValidKeysTest is OperatorAllocationTestBase { ConsensusLayerDepositManagerV1 internal depositManager; IDepositContract internal depositContract; @@ -566,7 +559,7 @@ contract ConsensusLayerDepositManagerV1ValidKeysTest is ConsensusLayerDepositMan } } -contract ConsensusLayerDepositManagerV1InvalidDepositContract is ConsensusLayerDepositManagerTestBase { +contract ConsensusLayerDepositManagerV1InvalidDepositContract is OperatorAllocationTestBase { ConsensusLayerDepositManagerV1 internal depositManager; IDepositContract internal depositContract; @@ -590,7 +583,7 @@ contract ConsensusLayerDepositManagerV1InvalidDepositContract is ConsensusLayerD } } -contract ConsensusLayerDepositManagerV1KeeperTest is ConsensusLayerDepositManagerTestBase { +contract ConsensusLayerDepositManagerV1KeeperTest is OperatorAllocationTestBase { ConsensusLayerDepositManagerV1 internal depositManager; IDepositContract internal depositContract; From 292f26f6f048437070d3af4b878ca310cf3a282d Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Wed, 11 Feb 2026 14:36:06 +0000 Subject: [PATCH 49/60] style: refactor test helpers to external Test Base (review suggestion) --- contracts/test/OperatorAllocationTestBase.sol | 33 +++++++++++- contracts/test/OperatorsRegistry.1.t.sol | 51 ++----------------- contracts/test/River.1.t.sol | 14 ++--- 3 files changed, 38 insertions(+), 60 deletions(-) diff --git a/contracts/test/OperatorAllocationTestBase.sol b/contracts/test/OperatorAllocationTestBase.sol index 3114eb3b..0cfc6030 100644 --- a/contracts/test/OperatorAllocationTestBase.sol +++ b/contracts/test/OperatorAllocationTestBase.sol @@ -7,8 +7,39 @@ import "../src/interfaces/IOperatorRegistry.1.sol"; abstract contract OperatorAllocationTestBase is Test { function _createAllocation(uint256 count) internal pure returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { + return _createAllocation(0, count); + } + + function _createAllocation(uint256 operatorIndex, uint256 count) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: operatorIndex, validatorCount: count}); return allocations; } + + function _createAllocation(uint256[] memory opIndexes, uint32[] memory counts) + internal + pure + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = + new IOperatorsRegistryV1.OperatorAllocation[](opIndexes.length); + for (uint256 i = 0; i < opIndexes.length; ++i) { + allocations[i] = + IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndexes[i], validatorCount: counts[i]}); + } + return allocations; + } + + function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) + internal + pure + virtual + returns (IOperatorsRegistryV1.OperatorAllocation[] memory) + { + return _createAllocation(opIndexes, counts); + } } diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 583acd07..03204861 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.33; import "forge-std/Test.sol"; +import "./OperatorAllocationTestBase.sol"; import "../src/libraries/LibBytes.sol"; import "./utils/UserFactory.sol"; import "./utils/BytesGenerator.sol"; @@ -100,7 +101,7 @@ contract OperatorsRegistryV1InitializationTests is OperatorsRegistryV1TestBase { } } -contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator { +contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator, OperatorAllocationTestBase { function setUp() public { admin = makeAddr("admin"); river = address(new RiverMock(0)); @@ -109,30 +110,6 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.initOperatorsRegistryV1(admin, river); } - function _createAllocation(uint256 opIndex, uint256 count) - internal - pure - returns (IOperatorsRegistryV1.OperatorAllocation[] memory) - { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndex, validatorCount: count}); - return allocations; - } - - function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) - internal - pure - returns (IOperatorsRegistryV1.OperatorAllocation[] memory) - { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](opIndexes.length); - for (uint256 i = 0; i < opIndexes.length; ++i) { - allocations[i] = - IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndexes[i], validatorCount: counts[i]}); - } - return allocations; - } - function testInitializeTwice() public { vm.expectRevert(abi.encodeWithSignature("InvalidInitialization(uint256,uint256)", 0, 1)); operatorsRegistry.initOperatorsRegistryV1(admin, river); @@ -1673,7 +1650,7 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator } } -contract OperatorsRegistryV1TestDistribution is Test { +contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { UserFactory internal uf = new UserFactory(); OperatorsRegistryV1 internal operatorsRegistry; @@ -1708,28 +1685,6 @@ contract OperatorsRegistryV1TestDistribution is Test { return res; } - function _createAllocation(uint256[] memory opIndexes, uint32[] memory counts) - internal - pure - returns (IOperatorsRegistryV1.OperatorAllocation[] memory) - { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = - new IOperatorsRegistryV1.OperatorAllocation[](opIndexes.length); - for (uint256 i = 0; i < opIndexes.length; ++i) { - allocations[i] = - IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndexes[i], validatorCount: counts[i]}); - } - return allocations; - } - - function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) - internal - pure - returns (IOperatorsRegistryV1.OperatorAllocation[] memory) - { - return _createAllocation(opIndexes, counts); - } - function setUp() public { admin = makeAddr("admin"); river = address(new RiverMock(0)); diff --git a/contracts/test/River.1.t.sol b/contracts/test/River.1.t.sol index 2edf2a1f..7ce12981 100644 --- a/contracts/test/River.1.t.sol +++ b/contracts/test/River.1.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.33; import "forge-std/Test.sol"; +import "./OperatorAllocationTestBase.sol"; import "./utils/UserFactory.sol"; import "./utils/BytesGenerator.sol"; import "./utils/LibImplementationUnbricker.sol"; @@ -35,7 +36,7 @@ contract RiverV1ForceCommittable is RiverV1 { } } -abstract contract RiverV1TestBase is Test, BytesGenerator { +abstract contract RiverV1TestBase is OperatorAllocationTestBase, BytesGenerator { UserFactory internal uf = new UserFactory(); RiverV1ForceCommittable internal river; @@ -47,19 +48,10 @@ abstract contract RiverV1TestBase is Test, BytesGenerator { AllowlistV1 internal allowlist; OperatorsRegistryWithOverridesV1 internal operatorsRegistry; - function _createAllocation(uint256 opIndex, uint256 count) - internal - pure - returns (IOperatorsRegistryV1.OperatorAllocation[] memory) - { - IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: opIndex, validatorCount: count}); - return allocations; - } - function _createMultiAllocation(uint256[] memory opIndexes, uint32[] memory counts) internal pure + override returns (IOperatorsRegistryV1.OperatorAllocation[] memory) { require(opIndexes.length == counts.length, "InvalidAllocationLengths"); From 880019720a41a3f67bb05ca07af4cbaaa3612bbe Mon Sep 17 00:00:00 2001 From: iamsahu Date: Wed, 11 Feb 2026 17:05:17 +0100 Subject: [PATCH 50/60] chore: review suggestions --- contracts/src/OperatorsRegistry.1.sol | 6 ++++-- contracts/src/interfaces/IOperatorRegistry.1.sol | 6 +++--- contracts/test/OperatorsRegistry.1.t.sol | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 2d3c53c8..556a96dc 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -483,8 +483,10 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab revert InactiveOperator(operatorIndex); } if (count > (operator.funded - operator.requestedExits)) { - // Operator has insufficient funded validators - revert ExitsRequestedExceedsFundedCount(operatorIndex, count, operator.funded); + // Operator has insufficient available funded validators + revert ExitsRequestedExceedAvailableFundedCount( + operatorIndex, count, operator.funded - operator.requestedExits + ); } // Operator has sufficient funded validators operator.requestedExits += uint32(count); diff --git a/contracts/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index 73e5ab88..01dbad61 100644 --- a/contracts/src/interfaces/IOperatorRegistry.1.sol +++ b/contracts/src/interfaces/IOperatorRegistry.1.sol @@ -196,11 +196,11 @@ interface IOperatorsRegistryV1 { /// @notice The provided stopped validator count of an operator is above its funded validator count error StoppedValidatorCountAboveFundedCount(uint256 operatorIndex, uint32 stoppedCount, uint32 fundedCount); - /// @notice The provided exit requests exceed the funded validator count of the operator + /// @notice The provided exit requests exceed the available funded validator count of the operator /// @param operatorIndex The operator index /// @param requested The requested count - /// @param funded The funded count - error ExitsRequestedExceedsFundedCount(uint256 operatorIndex, uint256 requested, uint256 funded); + /// @param available The available count + error ExitsRequestedExceedAvailableFundedCount(uint256 operatorIndex, uint256 requested, uint256 available); /// @notice The provided exit requests exceed the current exit request demand /// @param requested The requested count diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index b9ff3018..3eef9b62 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -2366,7 +2366,9 @@ contract OperatorsRegistryV1TestDistribution is Test { limits[0] = 60; vm.prank(keeper); - vm.expectRevert(abi.encodeWithSignature("ExitsRequestedExceedsFundedCount(uint256,uint256,uint256)", 0, 60, 50)); + vm.expectRevert( + abi.encodeWithSignature("ExitsRequestedExceedAvailableFundedCount(uint256,uint256,uint256)", 0, 60, 50) + ); operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } From 75d6c4b473441958936c110e596194fd12395e03 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Wed, 11 Feb 2026 17:18:04 +0100 Subject: [PATCH 51/60] chore: review suggestions --- contracts/src/OperatorsRegistry.1.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index 556a96dc..3dc24d97 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -447,17 +447,16 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab /// @inheritdoc IOperatorsRegistryV1 function requestValidatorExits(OperatorAllocation[] calldata _allocations) external { - uint256 allocationsLength = _allocations.length; - uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); - if (msg.sender != IConsensusLayerDepositManagerV1(RiverAddress.get()).getKeeper()) { revert IConsensusLayerDepositManagerV1.OnlyKeeper(); } + uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); if (currentValidatorExitsDemand == 0) { revert NoExitRequestsToPerform(); } + uint256 allocationsLength = _allocations.length; if (allocationsLength == 0) { revert InvalidEmptyArray(); } From 19c9f43144ed2a82cabc8a44ef0f9a9c0a9d96ea Mon Sep 17 00:00:00 2001 From: juliaaschmidt Date: Thu, 12 Feb 2026 08:20:27 +0000 Subject: [PATCH 52/60] fix(OperatorsRegistry) prover fix --- certora/conf/OperatorRegistryV1.conf | 7 +++---- certora/confs_for_CI/OperatorRegistryV1_1.conf | 2 +- .../confs_for_CI/OperatorRegistryV1_2_2loops.conf | 2 +- .../confs_for_CI/OperatorRegistryV1_2_4loops_v1.conf | 2 +- .../confs_for_CI/OperatorRegistryV1_2_4loops_v2.conf | 2 +- certora/confs_for_CI/OperatorRegistryV1_3.conf | 2 +- certora/harness/OperatorsRegistryV1Harness.sol | 7 +++++++ certora/specs/OperatorRegistryV1.spec | 12 +++++++----- certora/specs/OperatorRegistryV1_base.spec | 8 ++++---- certora/specs/OperatorRegistryV1_obsoleteRules.spec | 3 ++- 10 files changed, 28 insertions(+), 19 deletions(-) diff --git a/certora/conf/OperatorRegistryV1.conf b/certora/conf/OperatorRegistryV1.conf index 0d2501c4..9db6a312 100644 --- a/certora/conf/OperatorRegistryV1.conf +++ b/certora/conf/OperatorRegistryV1.conf @@ -1,19 +1,18 @@ { "files": [ - "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", - + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness" ], "link" : [], "server": "production", "prover_version": "master", "rule_sanity": "basic", - + "optimistic_loop": true, "packages": ["openzeppelin-contracts=lib/openzeppelin-contracts"], "optimistic_hashing": true, "optimistic_fallback": true, - "solc": "solc8.33", + "solc": "solc", //"multi_assert_check": true, "smt_timeout": "5000", "prover_args": [ diff --git a/certora/confs_for_CI/OperatorRegistryV1_1.conf b/certora/confs_for_CI/OperatorRegistryV1_1.conf index f07e6b16..d1a704c9 100644 --- a/certora/confs_for_CI/OperatorRegistryV1_1.conf +++ b/certora/confs_for_CI/OperatorRegistryV1_1.conf @@ -1,6 +1,6 @@ { "files": [ - "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness" ], "rule_sanity": "basic", "optimistic_loop": true, diff --git a/certora/confs_for_CI/OperatorRegistryV1_2_2loops.conf b/certora/confs_for_CI/OperatorRegistryV1_2_2loops.conf index 3dfb9017..4e278a92 100644 --- a/certora/confs_for_CI/OperatorRegistryV1_2_2loops.conf +++ b/certora/confs_for_CI/OperatorRegistryV1_2_2loops.conf @@ -1,6 +1,6 @@ { "files": [ - "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness" ], "rule_sanity": "basic", "optimistic_loop": true, diff --git a/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v1.conf b/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v1.conf index 7a9d18e1..3cb22f6b 100644 --- a/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v1.conf +++ b/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v1.conf @@ -1,6 +1,6 @@ { "files": [ - "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness" ], "rule_sanity": "basic", "optimistic_loop": true, diff --git a/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v2.conf b/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v2.conf index 99f23e2c..22b059ae 100644 --- a/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v2.conf +++ b/certora/confs_for_CI/OperatorRegistryV1_2_4loops_v2.conf @@ -1,6 +1,6 @@ { "files": [ - "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness" ], "rule_sanity": "basic", "optimistic_loop": true, diff --git a/certora/confs_for_CI/OperatorRegistryV1_3.conf b/certora/confs_for_CI/OperatorRegistryV1_3.conf index 96396a42..05a9f4ab 100644 --- a/certora/confs_for_CI/OperatorRegistryV1_3.conf +++ b/certora/confs_for_CI/OperatorRegistryV1_3.conf @@ -1,6 +1,6 @@ { "files": [ - "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness", + "certora/harness/OperatorsRegistryV1Harness.sol:OperatorsRegistryV1Harness" ], "rule_sanity": "basic", "optimistic_loop": true, diff --git a/certora/harness/OperatorsRegistryV1Harness.sol b/certora/harness/OperatorsRegistryV1Harness.sol index 657feb87..7e0d6727 100644 --- a/certora/harness/OperatorsRegistryV1Harness.sol +++ b/certora/harness/OperatorsRegistryV1Harness.sol @@ -156,6 +156,13 @@ contract OperatorsRegistryV1Harness is OperatorsRegistryV1 { return maxSaturation - minSaturation; } + /// @dev Certora-only: single-arg wrapper so specs need not reference IOperatorsRegistryV1 (listing the interface in conf causes "no bytecode" fatal error). + function pickNextValidatorsToDepositWithCount(uint256 count) external returns (bytes[] memory, bytes[] memory) { + IOperatorsRegistryV1.OperatorAllocation[] memory allocations = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocations[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: count}); + return this.pickNextValidatorsToDeposit(allocations); + } + function getOperatorsSaturationDiscrepancy(uint256 index1, uint256 index2) external view returns (uint256) { OperatorsV2.Operator[] storage ops = OperatorsV2.getAll(); diff --git a/certora/specs/OperatorRegistryV1.spec b/certora/specs/OperatorRegistryV1.spec index 4132c920..2c566147 100644 --- a/certora/specs/OperatorRegistryV1.spec +++ b/certora/specs/OperatorRegistryV1.spec @@ -25,7 +25,8 @@ rule startingValidatorsDecreasesDiscrepancy(env e) uint count; require count > 0 && count <= 3; - pickNextValidatorsToDeposit(e, count); + require allOpCount > 0; + pickNextValidatorsToDepositWithCount(e, count); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => to_mathint(discrepancyBefore) >= @@ -42,7 +43,8 @@ rule witness4_3StartingValidatorsDecreasesDiscrepancy(env e) uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); uint count; require count <= 1; - pickNextValidatorsToDeposit(e, count); + require getOperatorsCount() > 0; + pickNextValidatorsToDepositWithCount(e, count); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); satisfy discrepancyBefore == 4 && discrepancyAfter == 3; } @@ -129,7 +131,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) } { preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } + preserved pickNextValidatorsToDepositWithCount(uint256 x) with(env e) { require x <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -216,7 +218,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && f.selector != sig:requestValidatorExits(uint256).selector && - f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && + f.selector != sig:pickNextValidatorsToDepositWithCount(uint256).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -225,7 +227,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) invariant operatorsStatesRemainValid_LI2_pickNextValidatorsToDeposit(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && - !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector + !needsLoopIter4(f) && f.selector != sig:pickNextValidatorsToDepositWithCount(uint256).selector } // proves the invariant for reportStoppedValidatorCounts diff --git a/certora/specs/OperatorRegistryV1_base.spec b/certora/specs/OperatorRegistryV1_base.spec index 5e8c102e..0482ab59 100644 --- a/certora/specs/OperatorRegistryV1_base.spec +++ b/certora/specs/OperatorRegistryV1_base.spec @@ -4,12 +4,12 @@ using OperatorsRegistryV1Harness as OR; methods { - // OperatorsRegistryV1 + // IOperatorsRegistryV1 function _.reportStoppedValidatorCounts(uint32[], uint256) external => DISPATCHER(true); function OR.getStoppedAndRequestedExitCounts() external returns (uint32, uint256) envfree; function _.getStoppedAndRequestedExitCounts() external => DISPATCHER(true); function _.demandValidatorExits(uint256, uint256) external => DISPATCHER(true); - //function _.pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]) external => DISPATCHER(true); // has no effect - CERT-4615 + // DISPATCHER for pickNextValidatorsToDeposit not used; specs use pickNextValidatorsToDepositWithCount to avoid IOperatorsRegistryV1 in scene (no bytecode) //function _.deposit(bytes,bytes,bytes,bytes32) external => DISPATCHER(true); // has no effect - CERT-4615 @@ -30,7 +30,7 @@ methods { function OR.getActiveOperatorsCount() external returns (uint256) envfree; function OR.getOperatorsSaturationDiscrepancy() external returns (uint256) envfree; function OR.getKeysCount(uint256) external returns (uint256) envfree; - function OR.pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]) external returns (bytes[] memory, bytes[] memory); + function OR.pickNextValidatorsToDepositWithCount(uint256) external returns (bytes[] memory, bytes[] memory); function OR.requestValidatorExits(uint256) external; function OR.setOperatorAddress(uint256, address) external; function OR.getOperatorsSaturationDiscrepancy(uint256, uint256) external returns (uint256) envfree; @@ -88,7 +88,7 @@ definition isMethodID(method f, uint ID) returns bool = (f.selector == sig:addOperator(string,address).selector && ID == 4) || (f.selector == sig:demandValidatorExits(uint256,uint256).selector && ID == 5) || (f.selector == sig:initOperatorsRegistryV1(address,address).selector && ID == 6) || - (f.selector == sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && ID == 7) || + (f.selector == sig:pickNextValidatorsToDepositWithCount(uint256).selector && ID == 7) || (f.selector == sig:proposeAdmin(address).selector && ID == 8) || (f.selector == sig:removeValidators(uint256,uint256[]).selector && ID == 9) || (f.selector == sig:requestValidatorExits(uint256).selector && ID == 10) || diff --git a/certora/specs/OperatorRegistryV1_obsoleteRules.spec b/certora/specs/OperatorRegistryV1_obsoleteRules.spec index 51545120..ced3cace 100644 --- a/certora/specs/OperatorRegistryV1_obsoleteRules.spec +++ b/certora/specs/OperatorRegistryV1_obsoleteRules.spec @@ -5,10 +5,11 @@ import "OperatorRegistryV1_base.spec"; //uses a more complex check for discrepancy that the prover can't handle rule startingValidatorsDecreasesDiscrepancyFULL(env e) { require isValidState(); + require getOperatorsCount() > 0; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(); uint count; require count <= 10; - pickNextValidatorsToDeposit(e, count); + pickNextValidatorsToDepositWithCount(e, count); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(); assert discrepancyBefore >= discrepancyAfter; } From f03415ab0f9a536bb87cd7a154c544112efd1ce7 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Thu, 12 Feb 2026 17:47:42 +0100 Subject: [PATCH 53/60] chore: certora rule correction --- certora/specs/OperatorRegistryV1.spec | 14 +++++++------- certora/specs/OperatorRegistryV1_base.spec | 4 ++-- .../OperatorRegistryV1_finishedRules.spec | 18 +++++++++--------- certora/specs/OperatorRegistryV1_orig.spec | 2 +- .../OperatorRegistryV1_for_CI_3.spec | 18 +++++++++--------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/certora/specs/OperatorRegistryV1.spec b/certora/specs/OperatorRegistryV1.spec index 4132c920..8630a7c1 100644 --- a/certora/specs/OperatorRegistryV1.spec +++ b/certora/specs/OperatorRegistryV1.spec @@ -128,7 +128,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) //&& f.selector == sig:requestValidatorExits(uint256).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -215,7 +215,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && - f.selector != sig:requestValidatorExits(uint256).selector && + f.selector != sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -238,22 +238,22 @@ invariant operatorsStatesRemainValid_LI4_m1(uint opIndex) // https://prover.certora.com/output/6893/ee6dc8f5245647b8b0c9758360992b48/?anonymousKey=c5a40d1f26ee0860ea2502c48a8b99baa7e98490 invariant operatorsStatesRemainValid_LI2_cond3_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond3(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } } // https://prover.certora.com/output/6893/9b9eaf30d9274d02934641a25351218f/?anonymousKey=27d543677f1c1d051d7a5715ce4e41fd5ffaf412 invariant operatorsStatesRemainValid_LI2_cond2_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond2(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } } // https://prover.certora.com/output/6893/87eaf2d5d9ad427781570b215598a7a7/?anonymousKey=7e0aa6df6957986370875945b0c894a2b993b99c invariant operatorsStatesRemainValid_LI2_cond1_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond1(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } // proves the invariant for addValidators diff --git a/certora/specs/OperatorRegistryV1_base.spec b/certora/specs/OperatorRegistryV1_base.spec index 5e8c102e..3635fc4d 100644 --- a/certora/specs/OperatorRegistryV1_base.spec +++ b/certora/specs/OperatorRegistryV1_base.spec @@ -31,7 +31,7 @@ methods { function OR.getOperatorsSaturationDiscrepancy() external returns (uint256) envfree; function OR.getKeysCount(uint256) external returns (uint256) envfree; function OR.pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]) external returns (bytes[] memory, bytes[] memory); - function OR.requestValidatorExits(uint256) external; + function OR.requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]) external; function OR.setOperatorAddress(uint256, address) external; function OR.getOperatorsSaturationDiscrepancy(uint256, uint256) external returns (uint256) envfree; //function OR.removeValidators(uint256,uint256[]) external envfree; @@ -91,7 +91,7 @@ definition isMethodID(method f, uint ID) returns bool = (f.selector == sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && ID == 7) || (f.selector == sig:proposeAdmin(address).selector && ID == 8) || (f.selector == sig:removeValidators(uint256,uint256[]).selector && ID == 9) || - (f.selector == sig:requestValidatorExits(uint256).selector && ID == 10) || + (f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector && ID == 10) || (f.selector == sig:setOperatorAddress(uint256,address).selector && ID == 11) || (f.selector == sig:setOperatorLimits(uint256[],uint32[],uint256).selector && ID == 12) || (f.selector == sig:setOperatorName(uint256,string).selector && ID == 13) || diff --git a/certora/specs/OperatorRegistryV1_finishedRules.spec b/certora/specs/OperatorRegistryV1_finishedRules.spec index 9c72c5fc..f88b67cf 100644 --- a/certora/specs/OperatorRegistryV1_finishedRules.spec +++ b/certora/specs/OperatorRegistryV1_finishedRules.spec @@ -48,7 +48,7 @@ invariant inactiveOperatorsRemainNotFunded(uint opIndex) (isValidState() && isOpIndexInBounds(opIndex)) => (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -64,10 +64,10 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) isValidState() => (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && f.selector != sig:setOperatorStatus(uint256,bool).selector //method is allowed to break this - //&& f.selector == sig:requestValidatorExits(uint256).selector + //&& f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -158,29 +158,29 @@ rule fundedAndExitedCanOnlyIncrease_IL2(method f, env e, calldataarg args) filte // https://prover.certora.com/output/6893/ee6dc8f5245647b8b0c9758360992b48/?anonymousKey=c5a40d1f26ee0860ea2502c48a8b99baa7e98490 invariant operatorsStatesRemainValid_LI2_cond3_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond3(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } } // https://prover.certora.com/output/6893/9b9eaf30d9274d02934641a25351218f/?anonymousKey=27d543677f1c1d051d7a5715ce4e41fd5ffaf412 invariant operatorsStatesRemainValid_LI2_cond2_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond2(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } } // https://prover.certora.com/output/6893/87eaf2d5d9ad427781570b215598a7a7/?anonymousKey=7e0aa6df6957986370875945b0c894a2b993b99c invariant operatorsStatesRemainValid_LI2_cond1_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond1(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } // https://prover.certora.com/output/6893/bfd27cb65484472da1ead2b8178d7bb5/?anonymousKey=66caae5f45e04af246224f114442200d9e7fa8c0 invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && - f.selector != sig:requestValidatorExits(uint256).selector && + f.selector != sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } diff --git a/certora/specs/OperatorRegistryV1_orig.spec b/certora/specs/OperatorRegistryV1_orig.spec index 99558a87..16783ed0 100644 --- a/certora/specs/OperatorRegistryV1_orig.spec +++ b/certora/specs/OperatorRegistryV1_orig.spec @@ -37,7 +37,7 @@ invariant validatorKeysRemainUnique_LI2( => (opIndex1 == opIndex2 && valIndex1 == valIndex2)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 2; } } diff --git a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec index 0877763d..4637c962 100644 --- a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec +++ b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec @@ -118,7 +118,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) //&& f.selector == sig:requestValidatorExits(uint256).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -177,7 +177,7 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && - f.selector != sig:requestValidatorExits(uint256).selector && + f.selector != sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -200,22 +200,22 @@ invariant operatorsStatesRemainValid_LI4_m1(uint opIndex) // https://prover.certora.com/output/6893/ee6dc8f5245647b8b0c9758360992b48/?anonymousKey=c5a40d1f26ee0860ea2502c48a8b99baa7e98490 invariant operatorsStatesRemainValid_LI2_cond3_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond3(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } } // https://prover.certora.com/output/6893/9b9eaf30d9274d02934641a25351218f/?anonymousKey=27d543677f1c1d051d7a5715ce4e41fd5ffaf412 invariant operatorsStatesRemainValid_LI2_cond2_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond2(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } } // https://prover.certora.com/output/6893/87eaf2d5d9ad427781570b215598a7a7/?anonymousKey=7e0aa6df6957986370875945b0c894a2b993b99c invariant operatorsStatesRemainValid_LI2_cond1_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond1(opIndex)) - filtered { f -> f.selector == sig:requestValidatorExits(uint256).selector } + filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } // proves the invariant for addValidators @@ -391,7 +391,7 @@ invariant validatorKeysRemainUnique_LI2( => (opIndex1 == opIndex2 && valIndex1 == valIndex2)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 2; } } @@ -783,7 +783,7 @@ invariant inactiveOperatorsRemainNotFunded(uint opIndex) (isValidState() && isOpIndexInBounds(opIndex)) => (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } From bfa5892363dce324d201eb0a4d57e0a3d71dd576 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Thu, 12 Feb 2026 18:16:09 +0100 Subject: [PATCH 54/60] chore: certora rule correction --- certora/specs/OperatorRegistryV1.spec | 18 +++++++-------- .../OperatorRegistryV1_finishedRules.spec | 20 ++++++++--------- .../OperatorRegistryV1_obsoleteRules.spec | 13 ++++++----- certora/specs/OperatorRegistryV1_orig.spec | 2 +- .../OperatorRegistryV1_for_CI_3.spec | 22 +++++++++---------- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/certora/specs/OperatorRegistryV1.spec b/certora/specs/OperatorRegistryV1.spec index 8630a7c1..55393c80 100644 --- a/certora/specs/OperatorRegistryV1.spec +++ b/certora/specs/OperatorRegistryV1.spec @@ -81,9 +81,9 @@ rule exitingValidatorsDecreasesDiscrepancy(env e) require isValidState(); uint index1; uint index2; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - requestValidatorExits(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => discrepancyBefore >= discrepancyAfter; } @@ -93,9 +93,9 @@ rule witness4_3ExitingValidatorsDecreasesDiscrepancy(env e) require isValidState(); uint index1; uint index2; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - requestValidatorExits(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); satisfy discrepancyBefore == 4 && discrepancyAfter == 3; } @@ -128,7 +128,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) //&& f.selector == sig:requestValidatorExits(uint256).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -240,14 +240,14 @@ invariant operatorsStatesRemainValid_LI2_cond3_requestValidatorExits(uint opInde isValidState() => (operatorStateIsValid_cond3(opIndex)) filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } } // https://prover.certora.com/output/6893/9b9eaf30d9274d02934641a25351218f/?anonymousKey=27d543677f1c1d051d7a5715ce4e41fd5ffaf412 invariant operatorsStatesRemainValid_LI2_cond2_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond2(opIndex)) filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } } // https://prover.certora.com/output/6893/87eaf2d5d9ad427781570b215598a7a7/?anonymousKey=7e0aa6df6957986370875945b0c894a2b993b99c diff --git a/certora/specs/OperatorRegistryV1_finishedRules.spec b/certora/specs/OperatorRegistryV1_finishedRules.spec index f88b67cf..059450a2 100644 --- a/certora/specs/OperatorRegistryV1_finishedRules.spec +++ b/certora/specs/OperatorRegistryV1_finishedRules.spec @@ -10,9 +10,9 @@ rule exitingValidatorsDecreasesDiscrepancy(env e) require isValidState(); uint index1; uint index2; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - requestValidatorExits(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => discrepancyBefore >= discrepancyAfter; } @@ -22,9 +22,9 @@ rule witness4_3ExitingValidatorsDecreasesDiscrepancy(env e) require isValidState(); uint index1; uint index2; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - requestValidatorExits(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); satisfy discrepancyBefore == 4 && discrepancyAfter == 3; } @@ -48,7 +48,7 @@ invariant inactiveOperatorsRemainNotFunded(uint opIndex) (isValidState() && isOpIndexInBounds(opIndex)) => (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -67,7 +67,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) //&& f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -160,14 +160,14 @@ invariant operatorsStatesRemainValid_LI2_cond3_requestValidatorExits(uint opInde isValidState() => (operatorStateIsValid_cond3(opIndex)) filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } } // https://prover.certora.com/output/6893/9b9eaf30d9274d02934641a25351218f/?anonymousKey=27d543677f1c1d051d7a5715ce4e41fd5ffaf412 invariant operatorsStatesRemainValid_LI2_cond2_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond2(opIndex)) filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } } // https://prover.certora.com/output/6893/87eaf2d5d9ad427781570b215598a7a7/?anonymousKey=7e0aa6df6957986370875945b0c894a2b993b99c diff --git a/certora/specs/OperatorRegistryV1_obsoleteRules.spec b/certora/specs/OperatorRegistryV1_obsoleteRules.spec index 51545120..81a9fa80 100644 --- a/certora/specs/OperatorRegistryV1_obsoleteRules.spec +++ b/certora/specs/OperatorRegistryV1_obsoleteRules.spec @@ -6,9 +6,9 @@ import "OperatorRegistryV1_base.spec"; rule startingValidatorsDecreasesDiscrepancyFULL(env e) { require isValidState(); uint discrepancyBefore = getOperatorsSaturationDiscrepancy(); - uint count; - require count <= 10; - pickNextValidatorsToDeposit(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 10; + pickNextValidatorsToDeposit(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(); assert discrepancyBefore >= discrepancyAfter; } @@ -17,9 +17,10 @@ rule startingValidatorsDecreasesDiscrepancyFULL(env e) { rule exitingValidatorsDecreasesDiscrepancyFULL(env e) { require isValidState(); uint discrepancyBefore = getOperatorsSaturationDiscrepancy(); - uint count; - require count <= 10; - requestValidatorExits(e, count); + + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 10; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(); assert discrepancyBefore >= discrepancyAfter; } diff --git a/certora/specs/OperatorRegistryV1_orig.spec b/certora/specs/OperatorRegistryV1_orig.spec index 16783ed0..ab0f989e 100644 --- a/certora/specs/OperatorRegistryV1_orig.spec +++ b/certora/specs/OperatorRegistryV1_orig.spec @@ -37,7 +37,7 @@ invariant validatorKeysRemainUnique_LI2( => (opIndex1 == opIndex2 && valIndex1 == valIndex2)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 2; } } diff --git a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec index 4637c962..34a3d1c3 100644 --- a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec +++ b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec @@ -72,9 +72,9 @@ rule exitingValidatorsDecreasesDiscrepancy(env e) require isValidState(); uint index1; uint index2; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - requestValidatorExits(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => discrepancyBefore >= discrepancyAfter; } @@ -84,9 +84,9 @@ rule witness4_3ExitingValidatorsDecreasesDiscrepancy(env e) require isValidState(); uint index1; uint index2; uint discrepancyBefore = getOperatorsSaturationDiscrepancy(index1, index2); - uint count; - require count <= 1; - requestValidatorExits(e, count); + IOperatorsRegistryV1.OperatorAllocation[] allocations; + require allocations.length <= 1; + requestValidatorExits(e, allocations); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); satisfy discrepancyBefore == 4 && discrepancyAfter == 3; } @@ -118,7 +118,7 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) //&& f.selector == sig:requestValidatorExits(uint256).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } @@ -202,14 +202,14 @@ invariant operatorsStatesRemainValid_LI2_cond3_requestValidatorExits(uint opInde isValidState() => (operatorStateIsValid_cond3(opIndex)) filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } } // https://prover.certora.com/output/6893/9b9eaf30d9274d02934641a25351218f/?anonymousKey=27d543677f1c1d051d7a5715ce4e41fd5ffaf412 invariant operatorsStatesRemainValid_LI2_cond2_requestValidatorExits(uint opIndex) isValidState() => (operatorStateIsValid_cond2(opIndex)) filtered { f -> f.selector == sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } } // https://prover.certora.com/output/6893/87eaf2d5d9ad427781570b215598a7a7/?anonymousKey=7e0aa6df6957986370875945b0c894a2b993b99c @@ -391,7 +391,7 @@ invariant validatorKeysRemainUnique_LI2( => (opIndex1 == opIndex2 && valIndex1 == valIndex2)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) } { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 2; } } @@ -783,7 +783,7 @@ invariant inactiveOperatorsRemainNotFunded(uint opIndex) (isValidState() && isOpIndexInBounds(opIndex)) => (!getOperator(opIndex).active => getOperator(opIndex).funded == 0) { - preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x <= 2; } + preserved requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 2; } preserved pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[] x) with(env e) { require x.length <= 1; } preserved removeValidators(uint256 _index, uint256[] _indexes) with(env e) { require _indexes.length <= 1; } } From 9cf1690ece3fadd0cba4df941bc32d7b8ad6f815 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Fri, 13 Feb 2026 14:02:19 +0100 Subject: [PATCH 55/60] chore: certora CI change --- .github/workflows/Certora.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/Certora.yaml b/.github/workflows/Certora.yaml index e32bbffb..8be0fd0a 100644 --- a/.github/workflows/Certora.yaml +++ b/.github/workflows/Certora.yaml @@ -2,16 +2,12 @@ name: Certora verification on: push: - branches: - - main paths: - "contracts/**" - "lib/**" - ".github/**" - "certora/**" pull_request: - branches: - - main paths: - "contracts/**" - "lib/**" From 24a9c2cccde1e3367618dc86e808739bbc39f218 Mon Sep 17 00:00:00 2001 From: Mischa Tuffield Date: Mon, 16 Feb 2026 22:53:03 +0000 Subject: [PATCH 56/60] This adds in some more tests : MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test: testSequentialExitAllocationsAccumulate What it proves: Two rounds of exits to overlapping operators. requestedExits accumulates correctly (10 then 15 = 25). Demand and total track across both calls. ──────────────────────────────────────── #: 2 Test: testNonContiguousExitAllocations What it proves: Exits only from ops 0 and 4, skipping active ops 1,2,3. Skipped operators stay at requestedExits=0. Events verified. ──────────────────────────────────────── #: 3 Test: testPartialDemandFulfillmentAcrossMultipleCalls What it proves: Demand=100, keeper fulfills 40 then 60 in separate calls. Demand decrements correctly each time, total accumulates. ──────────────────────────────────────── #: 4 Test: testStoppedValidatorsAndExitsMultiStep What it proves: Full interleave: demand 200 -> stop 50 (demand drops to 150) -> exit 60 (demand drops to 90) -> stop 30 more (no demand change because stoppedCount < requestedExits) -> exit 60 more. Validates the max() semantics of _setStoppedValidatorCounts. ──────────────────────────────────────── #: 5 Test: testDepositThenExitEndToEnd What it proves: Full lifecycle: deposit 30 -> exit 15 -> simulate validators stopping -> deposit 5 more. Discovered and documents the key invariant that getAllFundable() blocks deposits to operators with requestedExits > stoppedCount. Test 4 and 5 were particularly useful -- they exposed non-obvious interactions: - Test 4: Stopped validators only increase requestedExits when stoppedCount > requestedExits (it's a max, not additive) - Test 5: You can't deposit to an operator with unfulfilled exit requests until those validators have actually stopped on-chain --- contracts/test/OperatorsRegistry.1.t.sol | 405 +++++++++++++++++++++++ 1 file changed, 405 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 5620d796..548d55ca 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3997,3 +3997,408 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { assertEq(signatures.length, 0, "Expected empty signatures"); } } + +/// @title Exit Allocation Correctness Tests +/// @notice Tests that verify the exit allocation logic correctly tracks per-operator +/// requestedExits across sequential calls, partial fulfillment, stopped validator +/// interactions, and combined deposit+exit flows. +contract OperatorsRegistryV1ExitCorrectnessTests is OperatorAllocationTestBase { + OperatorsRegistryV1 internal operatorsRegistry; + address internal admin; + address internal river; + address internal keeper; + + event RequestedValidatorExits(uint256 indexed index, uint256 count); + event SetTotalValidatorExitsRequested(uint256 previousTotalRequestedExits, uint256 newTotalRequestedExits); + event SetCurrentValidatorExitsDemand(uint256 previousValidatorExitsDemand, uint256 nextValidatorExitsDemand); + + bytes32 salt = bytes32(0); + + function genBytes(uint256 len) internal returns (bytes memory) { + bytes memory res = ""; + while (res.length < len) { + salt = keccak256(abi.encodePacked(salt)); + if (len - res.length >= 32) { + res = bytes.concat(res, abi.encode(salt)); + } else { + res = bytes.concat(res, LibBytes.slice(abi.encode(salt), 0, len - res.length)); + } + } + return res; + } + + function setUp() public { + admin = makeAddr("admin"); + river = address(new RiverMock(0)); + keeper = makeAddr("keeper"); + RiverMock(river).setKeeper(keeper); + + operatorsRegistry = new OperatorsRegistryInitializableV1(); + LibImplementationUnbricker.unbrick(vm, address(operatorsRegistry)); + operatorsRegistry.initOperatorsRegistryV1(admin, river); + + vm.startPrank(admin); + operatorsRegistry.addOperator("operatorOne", makeAddr("op1")); + operatorsRegistry.addOperator("operatorTwo", makeAddr("op2")); + operatorsRegistry.addOperator("operatorThree", makeAddr("op3")); + operatorsRegistry.addOperator("operatorFour", makeAddr("op4")); + operatorsRegistry.addOperator("operatorFive", makeAddr("op5")); + vm.stopPrank(); + } + + /// @dev Fund all 5 operators with 50 validators each and set limits + function _fundAllOperators() internal { + vm.startPrank(admin); + for (uint256 i = 0; i < 5; ++i) { + operatorsRegistry.addValidators(i, 50, genBytes((48 + 96) * 50)); + } + vm.stopPrank(); + + uint256[] memory operators = new uint256[](5); + uint32[] memory limits = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + operators[i] = i; + limits[i] = 50; + } + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + + RiverMock(river).sudoSetDepositedValidatorsCount(250); + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 1: Sequential exit allocations accumulate correctly + // ────────────────────────────────────────────────────────────────────── + + /// @notice Two rounds of exits to overlapping operators. Verifies requestedExits + /// accumulates correctly and demand decrements across both calls. + function testSequentialExitAllocationsAccumulate() external { + _fundAllOperators(); + + // Set demand to 100 + vm.prank(river); + operatorsRegistry.demandValidatorExits(100, 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 100); + + // Round 1: exit 10 from each of ops 0,1,2 + uint256[] memory ops1 = new uint256[](3); + ops1[0] = 0; + ops1[1] = 1; + ops1[2] = 2; + uint32[] memory counts1 = new uint32[](3); + counts1[0] = 10; + counts1[1] = 10; + counts1[2] = 10; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops1, counts1)); + + assertEq(operatorsRegistry.getOperator(0).requestedExits, 10, "Op0 should have 10 exits after round 1"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 10, "Op1 should have 10 exits after round 1"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 10, "Op2 should have 10 exits after round 1"); + assertEq(operatorsRegistry.getOperator(3).requestedExits, 0, "Op3 untouched after round 1"); + assertEq(operatorsRegistry.getOperator(4).requestedExits, 0, "Op4 untouched after round 1"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 70, "Demand should be 70 after round 1"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 30, "Total exits should be 30 after round 1"); + + // Round 2: exit 15 more from ops 0,1 and 5 from op3 (new operator) + uint256[] memory ops2 = new uint256[](3); + ops2[0] = 0; + ops2[1] = 1; + ops2[2] = 3; + uint32[] memory counts2 = new uint32[](3); + counts2[0] = 15; + counts2[1] = 15; + counts2[2] = 5; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops2, counts2)); + + assertEq(operatorsRegistry.getOperator(0).requestedExits, 25, "Op0 should have 10+15=25 exits"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 25, "Op1 should have 10+15=25 exits"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 10, "Op2 unchanged from round 1"); + assertEq(operatorsRegistry.getOperator(3).requestedExits, 5, "Op3 should have 5 exits from round 2"); + assertEq(operatorsRegistry.getOperator(4).requestedExits, 0, "Op4 still untouched"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 35, "Demand should be 100-30-35=35"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 65, "Total exits should be 30+35=65"); + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 2: Non-contiguous operator exits + // ────────────────────────────────────────────────────────────────────── + + /// @notice Exit from operators 0 and 4 only, skipping active operators 1,2,3. + /// Verifies skipped operators remain at requestedExits=0. + function testNonContiguousExitAllocations() external { + _fundAllOperators(); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(30, 250); + + uint256[] memory ops = new uint256[](2); + ops[0] = 0; + ops[1] = 4; + uint32[] memory counts = new uint32[](2); + counts[0] = 20; + counts[1] = 10; + + vm.expectEmit(true, true, true, true); + emit RequestedValidatorExits(0, 20); + vm.expectEmit(true, true, true, true); + emit RequestedValidatorExits(4, 10); + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, counts)); + + assertEq(operatorsRegistry.getOperator(0).requestedExits, 20, "Op0 should have 20 exits"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 0, "Op1 should remain at 0"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 0, "Op2 should remain at 0"); + assertEq(operatorsRegistry.getOperator(3).requestedExits, 0, "Op3 should remain at 0"); + assertEq(operatorsRegistry.getOperator(4).requestedExits, 10, "Op4 should have 10 exits"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0, "Demand fully satisfied"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 30); + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 3: Partial demand fulfillment across multiple calls + // ────────────────────────────────────────────────────────────────────── + + /// @notice Demand is 100. Keeper fulfills 40 in first call, then 60 in second call. + /// Verifies demand decrements correctly and total accumulates. + function testPartialDemandFulfillmentAcrossMultipleCalls() external { + _fundAllOperators(); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(100, 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 100); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + // Call 1: fulfill 40 (8 from each operator) + uint256[] memory ops = new uint256[](5); + uint32[] memory counts = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + ops[i] = i; + counts[i] = 8; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, counts)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 60, "Demand should be 60 after first call"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 40, "Total exits should be 40"); + + for (uint256 i = 0; i < 5; ++i) { + assertEq(operatorsRegistry.getOperator(i).requestedExits, 8, string(abi.encodePacked("Op ", vm.toString(i), " should have 8 exits"))); + } + + // Call 2: fulfill remaining 60 (12 from each) + for (uint256 i = 0; i < 5; ++i) { + counts[i] = 12; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, counts)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0, "Demand should be fully satisfied"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 100, "Total exits should be 100"); + + for (uint256 i = 0; i < 5; ++i) { + assertEq(operatorsRegistry.getOperator(i).requestedExits, 20, string(abi.encodePacked("Op ", vm.toString(i), " should have 8+12=20 exits"))); + } + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 4: Stopped validators + exits multi-step interaction + // ────────────────────────────────────────────────────────────────────── + + /// @notice Multi-step: demand exits -> stop some validators (reducing demand) -> exit some + /// -> stop more -> exit more. Verifies demand and requestedExits track correctly + /// through the interleaved sequence. + function testStoppedValidatorsAndExitsMultiStep() external { + _fundAllOperators(); + + // Step 1: Create demand for 200 exits + vm.prank(river); + operatorsRegistry.demandValidatorExits(200, 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 200); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + // Step 2: Stop 50 validators across operators (reduces demand by 50) + // stoppedValidatorCounts[0] = totalStopped, then per-operator + uint32[] memory stoppedCounts1 = new uint32[](6); + stoppedCounts1[0] = 50; // total + stoppedCounts1[1] = 10; // op0 + stoppedCounts1[2] = 10; // op1 + stoppedCounts1[3] = 10; // op2 + stoppedCounts1[4] = 10; // op3 + stoppedCounts1[5] = 10; // op4 + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedCounts1, 250); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 150, "Demand reduced by 50 stopped"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 50, "Stopped validators count as exits"); + + // Step 3: Keeper exits 60 (12 from each operator) + uint256[] memory ops = new uint256[](5); + uint32[] memory exitCounts1 = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + ops[i] = i; + exitCounts1[i] = 12; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, exitCounts1)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 90, "Demand should be 150-60=90"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 110, "Total exits should be 50+60=110"); + + for (uint256 i = 0; i < 5; ++i) { + // requestedExits = 10 (from stopped) + 12 (from explicit exit) = 22 + assertEq(operatorsRegistry.getOperator(i).requestedExits, 22, + string(abi.encodePacked("Op ", vm.toString(i), " should have 22 requestedExits"))); + } + + // Step 4: Stop 30 more validators (total stopped now 80) + // Each operator goes from 10 stopped to 16 stopped. + // But requestedExits is already 22 per operator (10 from stopped + 12 from keeper). + // Since 16 < 22, _setStoppedValidatorCounts does NOT increase requestedExits. + // The unsolicited exit count is 0 (no operator has stoppedCount > requestedExits). + // However, the delta from 50 total stopped to 80 total stopped still reduces demand + // only to the extent that new stopped > old requestedExits per operator. + // Since 16 < 22 for all operators, unsollicitedExitsSum = 0, so demand stays at 90. + uint32[] memory stoppedCounts2 = new uint32[](6); + stoppedCounts2[0] = 80; // total now 80 (was 50) + stoppedCounts2[1] = 16; // op0 + stoppedCounts2[2] = 16; // op1 + stoppedCounts2[3] = 16; // op2 + stoppedCounts2[4] = 16; // op3 + stoppedCounts2[5] = 16; // op4 + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedCounts2, 250); + + // Demand unchanged because stoppedCount(16) < requestedExits(22) for all operators + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 90, "Demand unchanged: stopped < requestedExits"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 110, "Total exits unchanged"); + + // requestedExits still 22 per operator (stopped didn't exceed it) + for (uint256 i = 0; i < 5; ++i) { + assertEq(operatorsRegistry.getOperator(i).requestedExits, 22, + string(abi.encodePacked("Op ", vm.toString(i), " requestedExits unchanged at 22"))); + } + + // Step 5: Keeper exits 12 more from each (total 60) + uint32[] memory exitCounts2 = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + exitCounts2[i] = 12; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, exitCounts2)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 30, "Demand should be 90-60=30"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 170, "Total exits should be 110+60=170"); + + for (uint256 i = 0; i < 5; ++i) { + // requestedExits = 22 + 12 = 34 + assertEq(operatorsRegistry.getOperator(i).requestedExits, 34, + string(abi.encodePacked("Op ", vm.toString(i), " should have 22+12=34 requestedExits"))); + } + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 5: Deposit then exit end-to-end + // ────────────────────────────────────────────────────────────────────── + + /// @notice Combined flow: deposit validators via BYOV allocation, then exit some, + /// then simulate validators stopping, then deposit more. + /// Verifies funded and requestedExits are both correct throughout. + /// + /// Key invariant: getAllFundable() requires stoppedCount >= requestedExits + /// for an operator to be eligible for new deposits. This means you can't + /// deposit to an operator with pending (unfulfilled) exit requests until + /// those validators have actually stopped. + function testDepositThenExitEndToEnd() external { + // Setup: add keys and limits for 3 operators + vm.startPrank(admin); + for (uint256 i = 0; i < 3; ++i) { + operatorsRegistry.addValidators(i, 20, genBytes((48 + 96) * 20)); + } + vm.stopPrank(); + + uint256[] memory ops = new uint256[](3); + uint32[] memory limits = new uint32[](3); + for (uint256 i = 0; i < 3; ++i) { + ops[i] = i; + limits[i] = 20; + } + vm.prank(admin); + operatorsRegistry.setOperatorLimits(ops, limits, block.number); + + // Phase 1: Deposit 10 to op0, 15 to op1, 5 to op2 = 30 total + uint32[] memory depositCounts = new uint32[](3); + depositCounts[0] = 10; + depositCounts[1] = 15; + depositCounts[2] = 5; + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(ops, depositCounts)); + + assertEq(operatorsRegistry.getOperator(0).funded, 10, "Op0 should have 10 funded"); + assertEq(operatorsRegistry.getOperator(1).funded, 15, "Op1 should have 15 funded"); + assertEq(operatorsRegistry.getOperator(2).funded, 5, "Op2 should have 5 funded"); + + // Phase 2: Request exits -- 5 from op0, 7 from op1, 3 from op2 = 15 total + RiverMock(river).sudoSetDepositedValidatorsCount(30); + vm.prank(river); + operatorsRegistry.demandValidatorExits(15, 30); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 15); + + uint32[] memory exitCounts = new uint32[](3); + exitCounts[0] = 5; + exitCounts[1] = 7; + exitCounts[2] = 3; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, exitCounts)); + + assertEq(operatorsRegistry.getOperator(0).funded, 10, "Op0 funded unchanged"); + assertEq(operatorsRegistry.getOperator(1).funded, 15, "Op1 funded unchanged"); + assertEq(operatorsRegistry.getOperator(2).funded, 5, "Op2 funded unchanged"); + assertEq(operatorsRegistry.getOperator(0).requestedExits, 5, "Op0 should have 5 exits"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 7, "Op1 should have 7 exits"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 3, "Op2 should have 3 exits"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0, "Demand fully satisfied"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 15); + + // Phase 3: Before depositing more, the exited validators must actually stop. + // getAllFundable() requires stoppedCount >= requestedExits for eligibility. + // Simulate the stopped validators matching the exit requests. + uint32[] memory stoppedCounts = new uint32[](4); + stoppedCounts[0] = 15; // total stopped + stoppedCounts[1] = 5; // op0 stopped + stoppedCounts[2] = 7; // op1 stopped + stoppedCounts[3] = 3; // op2 stopped + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedCounts, 30); + + // Phase 4: Now deposit more to op0 (limit=20, funded=10, stopped >= requestedExits) + uint32[] memory depositCounts2 = new uint32[](1); + depositCounts2[0] = 5; + uint256[] memory singleOp = new uint256[](1); + singleOp[0] = 0; + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(singleOp, depositCounts2)); + + assertEq(operatorsRegistry.getOperator(0).funded, 15, "Op0 should now have 15 funded"); + assertEq(operatorsRegistry.getOperator(0).requestedExits, 5, "Op0 exits unchanged by new deposit"); + + // Verify the other operators are unchanged + assertEq(operatorsRegistry.getOperator(1).funded, 15, "Op1 funded unchanged"); + assertEq(operatorsRegistry.getOperator(2).funded, 5, "Op2 funded unchanged"); + } +} From 9a108fb789061311b4733688da3014b1a61d1e02 Mon Sep 17 00:00:00 2001 From: Mischa Tuffield Date: Mon, 16 Feb 2026 22:53:03 +0000 Subject: [PATCH 57/60] This adds in some more tests : MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test: testSequentialExitAllocationsAccumulate What it proves: Two rounds of exits to overlapping operators. requestedExits accumulates correctly (10 then 15 = 25). Demand and total track across both calls. ──────────────────────────────────────── #: 2 Test: testNonContiguousExitAllocations What it proves: Exits only from ops 0 and 4, skipping active ops 1,2,3. Skipped operators stay at requestedExits=0. Events verified. ──────────────────────────────────────── #: 3 Test: testPartialDemandFulfillmentAcrossMultipleCalls What it proves: Demand=100, keeper fulfills 40 then 60 in separate calls. Demand decrements correctly each time, total accumulates. ──────────────────────────────────────── #: 4 Test: testStoppedValidatorsAndExitsMultiStep What it proves: Full interleave: demand 200 -> stop 50 (demand drops to 150) -> exit 60 (demand drops to 90) -> stop 30 more (no demand change because stoppedCount < requestedExits) -> exit 60 more. Validates the max() semantics of _setStoppedValidatorCounts. ──────────────────────────────────────── #: 5 Test: testDepositThenExitEndToEnd What it proves: Full lifecycle: deposit 30 -> exit 15 -> simulate validators stopping -> deposit 5 more. Discovered and documents the key invariant that getAllFundable() blocks deposits to operators with requestedExits > stoppedCount. Test 4 and 5 were particularly useful -- they exposed non-obvious interactions: - Test 4: Stopped validators only increase requestedExits when stoppedCount > requestedExits (it's a max, not additive) - Test 5: You can't deposit to an operator with unfulfilled exit requests until those validators have actually stopped on-chain --- contracts/test/OperatorsRegistry.1.t.sol | 405 +++++++++++++++++++++++ 1 file changed, 405 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 5620d796..548d55ca 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -3997,3 +3997,408 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { assertEq(signatures.length, 0, "Expected empty signatures"); } } + +/// @title Exit Allocation Correctness Tests +/// @notice Tests that verify the exit allocation logic correctly tracks per-operator +/// requestedExits across sequential calls, partial fulfillment, stopped validator +/// interactions, and combined deposit+exit flows. +contract OperatorsRegistryV1ExitCorrectnessTests is OperatorAllocationTestBase { + OperatorsRegistryV1 internal operatorsRegistry; + address internal admin; + address internal river; + address internal keeper; + + event RequestedValidatorExits(uint256 indexed index, uint256 count); + event SetTotalValidatorExitsRequested(uint256 previousTotalRequestedExits, uint256 newTotalRequestedExits); + event SetCurrentValidatorExitsDemand(uint256 previousValidatorExitsDemand, uint256 nextValidatorExitsDemand); + + bytes32 salt = bytes32(0); + + function genBytes(uint256 len) internal returns (bytes memory) { + bytes memory res = ""; + while (res.length < len) { + salt = keccak256(abi.encodePacked(salt)); + if (len - res.length >= 32) { + res = bytes.concat(res, abi.encode(salt)); + } else { + res = bytes.concat(res, LibBytes.slice(abi.encode(salt), 0, len - res.length)); + } + } + return res; + } + + function setUp() public { + admin = makeAddr("admin"); + river = address(new RiverMock(0)); + keeper = makeAddr("keeper"); + RiverMock(river).setKeeper(keeper); + + operatorsRegistry = new OperatorsRegistryInitializableV1(); + LibImplementationUnbricker.unbrick(vm, address(operatorsRegistry)); + operatorsRegistry.initOperatorsRegistryV1(admin, river); + + vm.startPrank(admin); + operatorsRegistry.addOperator("operatorOne", makeAddr("op1")); + operatorsRegistry.addOperator("operatorTwo", makeAddr("op2")); + operatorsRegistry.addOperator("operatorThree", makeAddr("op3")); + operatorsRegistry.addOperator("operatorFour", makeAddr("op4")); + operatorsRegistry.addOperator("operatorFive", makeAddr("op5")); + vm.stopPrank(); + } + + /// @dev Fund all 5 operators with 50 validators each and set limits + function _fundAllOperators() internal { + vm.startPrank(admin); + for (uint256 i = 0; i < 5; ++i) { + operatorsRegistry.addValidators(i, 50, genBytes((48 + 96) * 50)); + } + vm.stopPrank(); + + uint256[] memory operators = new uint256[](5); + uint32[] memory limits = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + operators[i] = i; + limits[i] = 50; + } + + vm.prank(admin); + operatorsRegistry.setOperatorLimits(operators, limits, block.number); + + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + + RiverMock(river).sudoSetDepositedValidatorsCount(250); + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 1: Sequential exit allocations accumulate correctly + // ────────────────────────────────────────────────────────────────────── + + /// @notice Two rounds of exits to overlapping operators. Verifies requestedExits + /// accumulates correctly and demand decrements across both calls. + function testSequentialExitAllocationsAccumulate() external { + _fundAllOperators(); + + // Set demand to 100 + vm.prank(river); + operatorsRegistry.demandValidatorExits(100, 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 100); + + // Round 1: exit 10 from each of ops 0,1,2 + uint256[] memory ops1 = new uint256[](3); + ops1[0] = 0; + ops1[1] = 1; + ops1[2] = 2; + uint32[] memory counts1 = new uint32[](3); + counts1[0] = 10; + counts1[1] = 10; + counts1[2] = 10; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops1, counts1)); + + assertEq(operatorsRegistry.getOperator(0).requestedExits, 10, "Op0 should have 10 exits after round 1"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 10, "Op1 should have 10 exits after round 1"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 10, "Op2 should have 10 exits after round 1"); + assertEq(operatorsRegistry.getOperator(3).requestedExits, 0, "Op3 untouched after round 1"); + assertEq(operatorsRegistry.getOperator(4).requestedExits, 0, "Op4 untouched after round 1"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 70, "Demand should be 70 after round 1"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 30, "Total exits should be 30 after round 1"); + + // Round 2: exit 15 more from ops 0,1 and 5 from op3 (new operator) + uint256[] memory ops2 = new uint256[](3); + ops2[0] = 0; + ops2[1] = 1; + ops2[2] = 3; + uint32[] memory counts2 = new uint32[](3); + counts2[0] = 15; + counts2[1] = 15; + counts2[2] = 5; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops2, counts2)); + + assertEq(operatorsRegistry.getOperator(0).requestedExits, 25, "Op0 should have 10+15=25 exits"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 25, "Op1 should have 10+15=25 exits"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 10, "Op2 unchanged from round 1"); + assertEq(operatorsRegistry.getOperator(3).requestedExits, 5, "Op3 should have 5 exits from round 2"); + assertEq(operatorsRegistry.getOperator(4).requestedExits, 0, "Op4 still untouched"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 35, "Demand should be 100-30-35=35"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 65, "Total exits should be 30+35=65"); + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 2: Non-contiguous operator exits + // ────────────────────────────────────────────────────────────────────── + + /// @notice Exit from operators 0 and 4 only, skipping active operators 1,2,3. + /// Verifies skipped operators remain at requestedExits=0. + function testNonContiguousExitAllocations() external { + _fundAllOperators(); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(30, 250); + + uint256[] memory ops = new uint256[](2); + ops[0] = 0; + ops[1] = 4; + uint32[] memory counts = new uint32[](2); + counts[0] = 20; + counts[1] = 10; + + vm.expectEmit(true, true, true, true); + emit RequestedValidatorExits(0, 20); + vm.expectEmit(true, true, true, true); + emit RequestedValidatorExits(4, 10); + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, counts)); + + assertEq(operatorsRegistry.getOperator(0).requestedExits, 20, "Op0 should have 20 exits"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 0, "Op1 should remain at 0"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 0, "Op2 should remain at 0"); + assertEq(operatorsRegistry.getOperator(3).requestedExits, 0, "Op3 should remain at 0"); + assertEq(operatorsRegistry.getOperator(4).requestedExits, 10, "Op4 should have 10 exits"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0, "Demand fully satisfied"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 30); + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 3: Partial demand fulfillment across multiple calls + // ────────────────────────────────────────────────────────────────────── + + /// @notice Demand is 100. Keeper fulfills 40 in first call, then 60 in second call. + /// Verifies demand decrements correctly and total accumulates. + function testPartialDemandFulfillmentAcrossMultipleCalls() external { + _fundAllOperators(); + + vm.prank(river); + operatorsRegistry.demandValidatorExits(100, 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 100); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + // Call 1: fulfill 40 (8 from each operator) + uint256[] memory ops = new uint256[](5); + uint32[] memory counts = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + ops[i] = i; + counts[i] = 8; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, counts)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 60, "Demand should be 60 after first call"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 40, "Total exits should be 40"); + + for (uint256 i = 0; i < 5; ++i) { + assertEq(operatorsRegistry.getOperator(i).requestedExits, 8, string(abi.encodePacked("Op ", vm.toString(i), " should have 8 exits"))); + } + + // Call 2: fulfill remaining 60 (12 from each) + for (uint256 i = 0; i < 5; ++i) { + counts[i] = 12; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, counts)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0, "Demand should be fully satisfied"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 100, "Total exits should be 100"); + + for (uint256 i = 0; i < 5; ++i) { + assertEq(operatorsRegistry.getOperator(i).requestedExits, 20, string(abi.encodePacked("Op ", vm.toString(i), " should have 8+12=20 exits"))); + } + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 4: Stopped validators + exits multi-step interaction + // ────────────────────────────────────────────────────────────────────── + + /// @notice Multi-step: demand exits -> stop some validators (reducing demand) -> exit some + /// -> stop more -> exit more. Verifies demand and requestedExits track correctly + /// through the interleaved sequence. + function testStoppedValidatorsAndExitsMultiStep() external { + _fundAllOperators(); + + // Step 1: Create demand for 200 exits + vm.prank(river); + operatorsRegistry.demandValidatorExits(200, 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 200); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + + // Step 2: Stop 50 validators across operators (reduces demand by 50) + // stoppedValidatorCounts[0] = totalStopped, then per-operator + uint32[] memory stoppedCounts1 = new uint32[](6); + stoppedCounts1[0] = 50; // total + stoppedCounts1[1] = 10; // op0 + stoppedCounts1[2] = 10; // op1 + stoppedCounts1[3] = 10; // op2 + stoppedCounts1[4] = 10; // op3 + stoppedCounts1[5] = 10; // op4 + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedCounts1, 250); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 150, "Demand reduced by 50 stopped"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 50, "Stopped validators count as exits"); + + // Step 3: Keeper exits 60 (12 from each operator) + uint256[] memory ops = new uint256[](5); + uint32[] memory exitCounts1 = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + ops[i] = i; + exitCounts1[i] = 12; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, exitCounts1)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 90, "Demand should be 150-60=90"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 110, "Total exits should be 50+60=110"); + + for (uint256 i = 0; i < 5; ++i) { + // requestedExits = 10 (from stopped) + 12 (from explicit exit) = 22 + assertEq(operatorsRegistry.getOperator(i).requestedExits, 22, + string(abi.encodePacked("Op ", vm.toString(i), " should have 22 requestedExits"))); + } + + // Step 4: Stop 30 more validators (total stopped now 80) + // Each operator goes from 10 stopped to 16 stopped. + // But requestedExits is already 22 per operator (10 from stopped + 12 from keeper). + // Since 16 < 22, _setStoppedValidatorCounts does NOT increase requestedExits. + // The unsolicited exit count is 0 (no operator has stoppedCount > requestedExits). + // However, the delta from 50 total stopped to 80 total stopped still reduces demand + // only to the extent that new stopped > old requestedExits per operator. + // Since 16 < 22 for all operators, unsollicitedExitsSum = 0, so demand stays at 90. + uint32[] memory stoppedCounts2 = new uint32[](6); + stoppedCounts2[0] = 80; // total now 80 (was 50) + stoppedCounts2[1] = 16; // op0 + stoppedCounts2[2] = 16; // op1 + stoppedCounts2[3] = 16; // op2 + stoppedCounts2[4] = 16; // op3 + stoppedCounts2[5] = 16; // op4 + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedCounts2, 250); + + // Demand unchanged because stoppedCount(16) < requestedExits(22) for all operators + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 90, "Demand unchanged: stopped < requestedExits"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 110, "Total exits unchanged"); + + // requestedExits still 22 per operator (stopped didn't exceed it) + for (uint256 i = 0; i < 5; ++i) { + assertEq(operatorsRegistry.getOperator(i).requestedExits, 22, + string(abi.encodePacked("Op ", vm.toString(i), " requestedExits unchanged at 22"))); + } + + // Step 5: Keeper exits 12 more from each (total 60) + uint32[] memory exitCounts2 = new uint32[](5); + for (uint256 i = 0; i < 5; ++i) { + exitCounts2[i] = 12; + } + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, exitCounts2)); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 30, "Demand should be 90-60=30"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 170, "Total exits should be 110+60=170"); + + for (uint256 i = 0; i < 5; ++i) { + // requestedExits = 22 + 12 = 34 + assertEq(operatorsRegistry.getOperator(i).requestedExits, 34, + string(abi.encodePacked("Op ", vm.toString(i), " should have 22+12=34 requestedExits"))); + } + } + + // ────────────────────────────────────────────────────────────────────── + // TEST 5: Deposit then exit end-to-end + // ────────────────────────────────────────────────────────────────────── + + /// @notice Combined flow: deposit validators via BYOV allocation, then exit some, + /// then simulate validators stopping, then deposit more. + /// Verifies funded and requestedExits are both correct throughout. + /// + /// Key invariant: getAllFundable() requires stoppedCount >= requestedExits + /// for an operator to be eligible for new deposits. This means you can't + /// deposit to an operator with pending (unfulfilled) exit requests until + /// those validators have actually stopped. + function testDepositThenExitEndToEnd() external { + // Setup: add keys and limits for 3 operators + vm.startPrank(admin); + for (uint256 i = 0; i < 3; ++i) { + operatorsRegistry.addValidators(i, 20, genBytes((48 + 96) * 20)); + } + vm.stopPrank(); + + uint256[] memory ops = new uint256[](3); + uint32[] memory limits = new uint32[](3); + for (uint256 i = 0; i < 3; ++i) { + ops[i] = i; + limits[i] = 20; + } + vm.prank(admin); + operatorsRegistry.setOperatorLimits(ops, limits, block.number); + + // Phase 1: Deposit 10 to op0, 15 to op1, 5 to op2 = 30 total + uint32[] memory depositCounts = new uint32[](3); + depositCounts[0] = 10; + depositCounts[1] = 15; + depositCounts[2] = 5; + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(ops, depositCounts)); + + assertEq(operatorsRegistry.getOperator(0).funded, 10, "Op0 should have 10 funded"); + assertEq(operatorsRegistry.getOperator(1).funded, 15, "Op1 should have 15 funded"); + assertEq(operatorsRegistry.getOperator(2).funded, 5, "Op2 should have 5 funded"); + + // Phase 2: Request exits -- 5 from op0, 7 from op1, 3 from op2 = 15 total + RiverMock(river).sudoSetDepositedValidatorsCount(30); + vm.prank(river); + operatorsRegistry.demandValidatorExits(15, 30); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 15); + + uint32[] memory exitCounts = new uint32[](3); + exitCounts[0] = 5; + exitCounts[1] = 7; + exitCounts[2] = 3; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(ops, exitCounts)); + + assertEq(operatorsRegistry.getOperator(0).funded, 10, "Op0 funded unchanged"); + assertEq(operatorsRegistry.getOperator(1).funded, 15, "Op1 funded unchanged"); + assertEq(operatorsRegistry.getOperator(2).funded, 5, "Op2 funded unchanged"); + assertEq(operatorsRegistry.getOperator(0).requestedExits, 5, "Op0 should have 5 exits"); + assertEq(operatorsRegistry.getOperator(1).requestedExits, 7, "Op1 should have 7 exits"); + assertEq(operatorsRegistry.getOperator(2).requestedExits, 3, "Op2 should have 3 exits"); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0, "Demand fully satisfied"); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 15); + + // Phase 3: Before depositing more, the exited validators must actually stop. + // getAllFundable() requires stoppedCount >= requestedExits for eligibility. + // Simulate the stopped validators matching the exit requests. + uint32[] memory stoppedCounts = new uint32[](4); + stoppedCounts[0] = 15; // total stopped + stoppedCounts[1] = 5; // op0 stopped + stoppedCounts[2] = 7; // op1 stopped + stoppedCounts[3] = 3; // op2 stopped + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedCounts, 30); + + // Phase 4: Now deposit more to op0 (limit=20, funded=10, stopped >= requestedExits) + uint32[] memory depositCounts2 = new uint32[](1); + depositCounts2[0] = 5; + uint256[] memory singleOp = new uint256[](1); + singleOp[0] = 0; + + vm.prank(river); + operatorsRegistry.pickNextValidatorsToDeposit(_createAllocation(singleOp, depositCounts2)); + + assertEq(operatorsRegistry.getOperator(0).funded, 15, "Op0 should now have 15 funded"); + assertEq(operatorsRegistry.getOperator(0).requestedExits, 5, "Op0 exits unchanged by new deposit"); + + // Verify the other operators are unchanged + assertEq(operatorsRegistry.getOperator(1).funded, 15, "Op1 funded unchanged"); + assertEq(operatorsRegistry.getOperator(2).funded, 5, "Op2 funded unchanged"); + } +} From 0aa3f1dba1c22ea710439ce1b2a6f3c906e8bc61 Mon Sep 17 00:00:00 2001 From: iamsahu Date: Tue, 17 Feb 2026 18:20:32 +0100 Subject: [PATCH 58/60] chore: fix certora --- .../confs_for_CI/OperatorRegistryV1_3.conf | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/certora/confs_for_CI/OperatorRegistryV1_3.conf b/certora/confs_for_CI/OperatorRegistryV1_3.conf index 05a9f4ab..f98af9bd 100644 --- a/certora/confs_for_CI/OperatorRegistryV1_3.conf +++ b/certora/confs_for_CI/OperatorRegistryV1_3.conf @@ -11,15 +11,16 @@ "prover_args": [ "-enableCopyLoopRewrites true", ], -"loop_iter": "2", -"verify": "OperatorsRegistryV1Harness:certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec", -"rule": [ - "validatorStateTransition_3_4_M12", - "validatorStateTransition_3_4_M14", - "validatorStateTransition_4_3_M10", - "validatorStateTransition_4_3_M12", - "validatorStateTransition_4_3_M13", - "validatorStateTransition_4_3_M14" - ], -"msg": "OperatorRegistryV1 3", -} + "solc_via_ir": true, + "loop_iter": "2", + "verify": "OperatorsRegistryV1Harness:certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec", + "rule": [ + "validatorStateTransition_3_4_M12", + "validatorStateTransition_3_4_M14", + "validatorStateTransition_4_3_M10", + "validatorStateTransition_4_3_M12", + "validatorStateTransition_4_3_M13", + "validatorStateTransition_4_3_M14" + ], + "msg": "OperatorRegistryV1 3", + } From 076fb9888efc908e31b2f47f1afb7b2820a759ee Mon Sep 17 00:00:00 2001 From: iamsahu Date: Tue, 17 Feb 2026 18:50:27 +0100 Subject: [PATCH 59/60] chore: fix certora spec --- certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec index 71bb4112..f0f84d78 100644 --- a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec +++ b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec @@ -24,9 +24,7 @@ rule startingValidatorsDecreasesDiscrepancy(env e) IOperatorsRegistryV1.OperatorAllocation[] allocations; require allocations.length > 0 && allocations.length <= 3; pickNextValidatorsToDeposit(e, allocations); - IOperatorsRegistryV1.OperatorAllocation[] allocations; - require allocations.length > 0 && allocations.length <= 3; - pickNextValidatorsToDeposit(e, allocations); + uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => to_mathint(discrepancyBefore) >= From 35cd977573826489a8e271682b1be3cf2734b5ed Mon Sep 17 00:00:00 2001 From: iamsahu Date: Tue, 17 Feb 2026 22:00:19 +0100 Subject: [PATCH 60/60] chore: coverage increase --- contracts/test/OperatorsRegistry.1.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index c3e6a524..4c4dcf2c 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -125,6 +125,20 @@ contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator operatorsRegistry.initOperatorsRegistryV1(admin, river); } + function testForceFundedValidatorKeysEventEmission() public { + operatorsRegistry.getOperatorCount(); + operatorsRegistry.forceFundedValidatorKeysEventEmission(100); + + bytes32 operatorIndex = vm.load( + address(operatorsRegistry), + bytes32( + uint256(keccak256("river.state.migration.operatorsRegistry.fundedKeyEventRebroadcasting.operatorIndex")) + - 1 + ) + ); + assertEq(uint256(operatorIndex), type(uint256).max); + } + function testInternalSetKeys(uint256 _nodeOperatorAddressSalt, bytes32 _name, uint32 _keyCount, uint32 _blockRoll) public {