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/**" diff --git a/certora/conf/OperatorRegistryV1.conf b/certora/conf/OperatorRegistryV1.conf index 519ead4f..9db6a312 100644 --- a/certora/conf/OperatorRegistryV1.conf +++ b/certora/conf/OperatorRegistryV1.conf @@ -12,7 +12,7 @@ "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_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", + } diff --git a/certora/specs/OperatorRegistryV1.spec b/certora/specs/OperatorRegistryV1.spec index 2c566147..3be67791 100644 --- a/certora/specs/OperatorRegistryV1.spec +++ b/certora/specs/OperatorRegistryV1.spec @@ -27,6 +27,8 @@ rule startingValidatorsDecreasesDiscrepancy(env e) require count > 0 && count <= 3; require allOpCount > 0; pickNextValidatorsToDepositWithCount(e, count); + require allOpCount > 0; + pickNextValidatorsToDepositWithCount(e, count); uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => to_mathint(discrepancyBefore) >= @@ -83,9 +85,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; } @@ -95,9 +97,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; } @@ -130,8 +132,8 @@ invariant inactiveOperatorsRemainNotFunded_LI2(uint opIndex) //&& f.selector == sig:requestValidatorExits(uint256).selector } { - preserved requestValidatorExits(uint256 x) with(env e) { require x <= 2; } - preserved pickNextValidatorsToDepositWithCount(uint256 x) with(env e) { require x <= 1; } + 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; } } @@ -217,8 +219,8 @@ invariant operatorsStatesRemainValid_LI2_easyMethods(uint opIndex) isValidState() => (operatorStateIsValid(opIndex)) filtered { f -> !ignoredMethod(f) && !needsLoopIter4(f) && - f.selector != sig:requestValidatorExits(uint256).selector && - f.selector != sig:pickNextValidatorsToDepositWithCount(uint256).selector && + f.selector != sig:requestValidatorExits(IOperatorsRegistryV1.OperatorAllocation[]).selector && + f.selector != sig:pickNextValidatorsToDeposit(IOperatorsRegistryV1.OperatorAllocation[]).selector && f.selector != sig:removeValidators(uint256,uint256[]).selector } @@ -240,22 +242,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.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(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.length <= 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 0482ab59..f863852e 100644 --- a/certora/specs/OperatorRegistryV1_base.spec +++ b/certora/specs/OperatorRegistryV1_base.spec @@ -91,7 +91,7 @@ definition isMethodID(method f, uint ID) returns bool = (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) || + (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..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(uint256 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; } } @@ -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.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; } } @@ -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.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(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.length <= 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_obsoleteRules.spec b/certora/specs/OperatorRegistryV1_obsoleteRules.spec index ced3cace..be71d230 100644 --- a/certora/specs/OperatorRegistryV1_obsoleteRules.spec +++ b/certora/specs/OperatorRegistryV1_obsoleteRules.spec @@ -18,9 +18,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 99558a87..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(uint256 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 0877763d..f0f84d78 100644 --- a/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec +++ b/certora/specs_for_CI/OperatorRegistryV1_for_CI_3.spec @@ -24,6 +24,7 @@ rule startingValidatorsDecreasesDiscrepancy(env e) IOperatorsRegistryV1.OperatorAllocation[] allocations; require allocations.length > 0 && allocations.length <= 3; pickNextValidatorsToDeposit(e, allocations); + uint discrepancyAfter = getOperatorsSaturationDiscrepancy(index1, index2); assert discrepancyBefore > 0 => to_mathint(discrepancyBefore) >= @@ -72,9 +73,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 +85,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 +119,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.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; } } @@ -177,7 +178,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 +201,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.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(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.length <= 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 +392,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.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 +784,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.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; } } diff --git a/contracts/src/OperatorsRegistry.1.sol b/contracts/src/OperatorsRegistry.1.sol index c0870b9b..8d6303fb 100644 --- a/contracts/src/OperatorsRegistry.1.sol +++ b/contracts/src/OperatorsRegistry.1.sol @@ -450,15 +450,62 @@ contract OperatorsRegistryV1 is IOperatorsRegistryV1, Initializable, Administrab } /// @inheritdoc IOperatorsRegistryV1 - function requestValidatorExits(uint256 _count) external { + function requestValidatorExits(OperatorAllocation[] calldata _allocations) external { + if (msg.sender != IConsensusLayerDepositManagerV1(RiverAddress.get()).getKeeper()) { + revert IConsensusLayerDepositManagerV1.OnlyKeeper(); + } + uint256 currentValidatorExitsDemand = CurrentValidatorExitsDemand.get(); - uint256 exitRequestsToPerform = LibUint256.min(currentValidatorExitsDemand, _count); - if (exitRequestsToPerform == 0) { + if (currentValidatorExitsDemand == 0) { revert NoExitRequestsToPerform(); } + + uint256 allocationsLength = _allocations.length; + if (allocationsLength == 0) { + revert InvalidEmptyArray(); + } + + 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(); + } + if (i > 0 && !(operatorIndex > _allocations[i - 1].operatorIndex)) { + revert UnorderedOperatorList(); + } + + requestedExitCount += count; + + OperatorsV2.Operator storage operator = OperatorsV2.get(operatorIndex); + if (!operator.active) { + revert InactiveOperator(operatorIndex); + } + if (count > (operator.funded - operator.requestedExits)) { + // Operator has insufficient available funded validators + revert ExitsRequestedExceedAvailableFundedCount( + operatorIndex, count, operator.funded - 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 + if (requestedExitCount > currentValidatorExitsDemand) { + revert ExitsRequestedExceedDemand(requestedExitCount, currentValidatorExitsDemand); + } + uint256 savedCurrentValidatorExitsDemand = currentValidatorExitsDemand; - currentValidatorExitsDemand -= _pickNextValidatorsToExitFromActiveOperators(exitRequestsToPerform); + currentValidatorExitsDemand -= requestedExitCount; + uint256 totalRequestedExitsValue = TotalValidatorExitsRequested.get(); + _setTotalValidatorExitsRequested(totalRequestedExitsValue, totalRequestedExitsValue + requestedExitCount); _setCurrentValidatorExitsDemand(savedCurrentValidatorExitsDemand, currentValidatorExitsDemand); } @@ -727,99 +774,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 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; ++idx) { - 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; - } - } - - // 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; ++idx) { - if (_getActiveValidatorCountForExitRequests(operators[idx]) == highestActiveCount) { - uint32 additionalRequestedExits = baseExitRequestAmount + (rest > 0 ? 1 : 0); - operators[idx].picked += additionalRequestedExits; - if (rest > 0) { - unchecked { - --rest; - } - } - } - } - - 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; ++idx) { - 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); - } - } - - 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/src/interfaces/IOperatorRegistry.1.sol b/contracts/src/interfaces/IOperatorRegistry.1.sol index 24d67c5d..01dbad61 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 available funded validator count of the operator + /// @param operatorIndex The operator index + /// @param requested The requested count + /// @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 + /// @param demand The demand count + error ExitsRequestedExceedDemand(uint256 requested, uint256 demand); + /// @notice Initializes the operators registry /// @param _admin Admin in charge of managing operators /// @param _river Address of River system @@ -344,11 +355,9 @@ 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 - /// @param _count Max amount of exits to request - function requestValidatorExits(uint256 _count) external; + /// @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; /// @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 diff --git a/contracts/src/state/operatorsRegistry/Operators.2.sol b/contracts/src/state/operatorsRegistry/Operators.2.sol index 72ca9082..f06c933c 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 @@ -207,44 +195,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; ++idx) { - 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; - } - } - } - - 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 @@ -282,13 +232,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); diff --git a/contracts/test/OperatorsRegistry.1.t.sol b/contracts/test/OperatorsRegistry.1.t.sol index 97b172e2..4c4dcf2c 100644 --- a/contracts/test/OperatorsRegistry.1.t.sol +++ b/contracts/test/OperatorsRegistry.1.t.sol @@ -25,10 +25,6 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { return _pickNextValidatorsToDepositFromActiveOperators(_allocations); } - function debugGetNextValidatorsToExitFromActiveOperators(uint256 _requestedExitsAmount) external returns (uint256) { - return _pickNextValidatorsToExitFromActiveOperators(_requestedExitsAmount); - } - function sudoSetKeys(uint256 _operatorIndex, uint32 _keyCount) external { OperatorsV2.setKeys(_operatorIndex, _keyCount); } @@ -46,6 +42,7 @@ contract OperatorsRegistryInitializableV1 is OperatorsRegistryV1 { contract RiverMock { uint256 public getDepositedValidatorCount; + address public keeper; constructor(uint256 _getDepositedValidatorsCount) { getDepositedValidatorCount = _getDepositedValidatorsCount; @@ -54,6 +51,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 { @@ -62,6 +67,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"; @@ -86,7 +92,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)); } @@ -104,7 +112,9 @@ contract OperatorsRegistryV1InitializationTests is OperatorsRegistryV1TestBase { contract OperatorsRegistryV1Tests is OperatorsRegistryV1TestBase, BytesGenerator, OperatorAllocationTestBase { 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); @@ -115,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 { @@ -1664,6 +1688,7 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); @@ -1688,6 +1713,8 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { function setUp() public { admin = makeAddr("admin"); river = address(new RiverMock(0)); + keeper = makeAddr("keeper"); + RiverMock(river).setKeeper(keeper); operatorOne = makeAddr("operatorOne"); operatorTwo = makeAddr("operatorTwo"); @@ -2133,6 +2160,15 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); @@ -2143,7 +2179,8 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); @@ -2182,7 +2219,7 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { event SetTotalValidatorExitsRequested(uint256 previousTotalRequestedExits, uint256 newTotalRequestedExits); - function testRegularExitDistribution() external { + function testNonKeeperCantRequestExits() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2207,50 +2244,67 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugPickNextValidatorsToDepositFromActiveOperators(_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); + vm.expectRevert(abi.encodeWithSignature("OnlyKeeper()")); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + function testRequestValidatorNoExits() external { + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; - vm.prank(river); - operatorsRegistry.demandValidatorExits(250, 250); + 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)); + } - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 250); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); + 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(); - 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); - vm.expectEmit(true, true, true, true); - emit SetTotalValidatorExitsRequested(0, 250); - operatorsRegistry.requestValidatorExits(250); + uint32[] memory limits = new uint32[](5); + limits[0] = 50; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; - 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); + uint256[] memory operators = new uint256[](5); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 250); + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); + + 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 testExitDistributionWithUnsollicitedExits() external { + function testRequestExitsWithMoreRequestsThanDemand() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2291,54 +2345,16 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(river); operatorsRegistry.demandValidatorExits(250, 250); - uint32[] memory stoppedValidatorCounts = new uint32[](6); - stoppedValidatorCounts[0] = 100; - stoppedValidatorCounts[1] = 20; - stoppedValidatorCounts[2] = 20; - stoppedValidatorCounts[3] = 20; - stoppedValidatorCounts[4] = 20; - stoppedValidatorCounts[5] = 20; - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 250); - - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .sudoStoppedValidatorCounts(stoppedValidatorCounts, 250); - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 150); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 100); - - 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); - vm.expectEmit(true, true, true, true); - emit SetTotalValidatorExitsRequested(100, 250); - vm.expectEmit(true, true, true, true); - emit SetCurrentValidatorExitsDemand(150, 0); - operatorsRegistry.requestValidatorExits(150); - - 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); - - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 250); - } + limits[0] = 60; - function testRequestValidatorNoExits() external { - vm.expectRevert(abi.encodeWithSignature("NoExitRequestsToPerform()")); - operatorsRegistry.requestValidatorExits(0); + vm.prank(keeper); + vm.expectRevert( + abi.encodeWithSignature("ExitsRequestedExceedAvailableFundedCount(uint256,uint256,uint256)", 0, 60, 50) + ); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); } - function testOneExitDistribution() external { + function testRequestExitsRequestedExceedDemand() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2363,7 +2379,6 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); @@ -2372,25 +2387,22 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { assert(operatorsRegistry.getOperator(3).funded == 50); assert(operatorsRegistry.getOperator(4).funded == 50); - vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 1); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(1); + RiverMock(address(river)).sudoSetDepositedValidatorsCount(250); - 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); - } + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); - event UpdatedRequestedValidatorExitsUponStopped( - uint256 indexed index, uint32 oldRequestedExits, uint32 newRequestedExits - ); + vm.prank(river); + operatorsRegistry.demandValidatorExits(10, 250); - event SetCurrentValidatorExitsDemand(uint256 previousValidatorExitsDemand, uint256 nextValidatorExitsDemand); + limits[0] = 50; + + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("ExitsRequestedExceedDemand(uint256,uint256)", 250, 10)); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } - function testExitDistributionWithCatchupToStoppedAlreadyExistingArray() external { + function testRequestExitsWithUnorderedOperators() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2415,7 +2427,6 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); @@ -2424,108 +2435,49 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); + RiverMock(address(river)).sudoSetDepositedValidatorsCount(250); - 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); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); - uint32[] memory stoppedValidatorCounts = new uint32[](6); - stoppedValidatorCounts[0] = 50; - stoppedValidatorCounts[1] = 10; - stoppedValidatorCounts[2] = 10; - stoppedValidatorCounts[3] = 10; - stoppedValidatorCounts[4] = 10; - stoppedValidatorCounts[5] = 10; + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); - 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); + operators[0] = 1; + operators[1] = 0; + operators[2] = 2; + operators[3] = 3; + operators[4] = 4; - 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.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); + } - 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); + function testRequestExitsWithInvalidEmptyArray() external { + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 115); + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("InvalidEmptyArray()")); + operatorsRegistry.requestValidatorExits(_createAllocation(new uint256[](0), new uint32[](0))); + } + + function testRequestExitsWithAllocationWithZeroValidatorCount() external { + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); - 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); + uint256[] memory operators = new uint256[](1); + operators[0] = 0; + uint32[] memory exitCounts = new uint32[](1); + exitCounts[0] = 0; - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 115); + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, exitCounts)); } - function testExitDistributionWithCatchupToStopped() external { + function testRegularExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2550,7 +2502,6 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); @@ -2559,159 +2510,49 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); + RiverMock(address(river)).sudoSetDepositedValidatorsCount(250); - 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); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); - 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); + vm.prank(river); + operatorsRegistry.demandValidatorExits(250, 250); - assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); - assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 65); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 250); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 0); vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(0, 23); + emit RequestedValidatorExits(0, 50); + vm.expectEmit(true, true, true, true); + emit RequestedValidatorExits(1, 50); vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(1, 23); + emit RequestedValidatorExits(2, 50); vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(2, 23); + emit RequestedValidatorExits(3, 50); vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 23); + emit RequestedValidatorExits(4, 50); 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)) - .debugPickNextValidatorsToDepositFromActiveOperators(_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); + emit SetTotalValidatorExitsRequested(0, 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 == 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)) - .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); - } + assert(operatorsRegistry.getOperator(1).requestedExits == 50); + assert(operatorsRegistry.getOperator(2).requestedExits == 50); + assert(operatorsRegistry.getOperator(3).requestedExits == 50); + assert(operatorsRegistry.getOperator(4).requestedExits == 50); - 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); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 250); } - function testMoreThanMaxExitDistribution() external { + function testExitDistributionWithUnsollicitedExits() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); operatorsRegistry.addValidators(1, 50, genBytes((48 + 96) * 50)); @@ -2736,7 +2577,6 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).funded == 50); @@ -2745,6 +2585,30 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); + + uint32[] memory stoppedValidatorCounts = new uint32[](6); + stoppedValidatorCounts[0] = 100; + stoppedValidatorCounts[1] = 20; + stoppedValidatorCounts[2] = 20; + stoppedValidatorCounts[3] = 20; + stoppedValidatorCounts[4] = 20; + stoppedValidatorCounts[5] = 20; + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 250); + + OperatorsRegistryInitializableV1(address(operatorsRegistry)) + .sudoStoppedValidatorCounts(stoppedValidatorCounts, 250); + + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 150); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 100); + vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(0, 50); vm.expectEmit(true, true, true, true); @@ -2755,8 +2619,18 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { emit RequestedValidatorExits(3, 50); vm.expectEmit(true, true, true, true); emit RequestedValidatorExits(4, 50); - OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugGetNextValidatorsToExitFromActiveOperators(500); + vm.expectEmit(true, true, true, true); + emit SetTotalValidatorExitsRequested(100, 250); + vm.expectEmit(true, true, true, true); + emit SetCurrentValidatorExitsDemand(150, 0); + 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); @@ -2764,24 +2638,25 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { assert(operatorsRegistry.getOperator(3).requestedExits == 50); assert(operatorsRegistry.getOperator(4).requestedExits == 50); - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 250); + assertEq(operatorsRegistry.getCurrentValidatorExitsDemand(), 0); + assertEq(operatorsRegistry.getTotalValidatorExitsRequested(), 250); } - function testMoreThanMaxExitDistributionOnUnevenSetup() external { + function testOneExitDistribution() 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)); + 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] = 40; - limits[2] = 30; - limits[3] = 20; - limits[4] = 10; + limits[1] = 50; + limits[2] = 50; + limits[3] = 50; + limits[4] = 50; uint256[] memory operators = new uint256[](5); operators[0] = 0; @@ -2796,33 +2671,39 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { OperatorsRegistryInitializableV1(address(operatorsRegistry)) .debugPickNextValidatorsToDepositFromActiveOperators(_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); + 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, 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); + vm.prank(river); + operatorsRegistry.demandValidatorExits(5, 250); - 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); + limits[0] = 1; + limits[1] = 1; + limits[2] = 1; + limits[3] = 1; + limits[4] = 1; + + vm.expectEmit(true, true, true, true); + emit RequestedValidatorExits(0, 1); + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createAllocation(operators, limits)); - assert(operatorsRegistry.getTotalValidatorExitsRequested() == 150); + assert(operatorsRegistry.getOperator(0).requestedExits == 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( + uint256 indexed index, uint32 oldRequestedExits, uint32 newRequestedExits + ); + + event SetCurrentValidatorExitsDemand(uint256 previousValidatorExitsDemand, uint256 nextValidatorExitsDemand); + function testUnevenExitDistribution() external { vm.startPrank(admin); operatorsRegistry.addValidators(0, 50, genBytes((48 + 96) * 50)); @@ -2857,6 +2738,15 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { assert(operatorsRegistry.getOperator(3).funded == 50); assert(operatorsRegistry.getOperator(4).funded == 50); + vm.prank(river); + operatorsRegistry.demandValidatorExits(14, 250); + + 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); vm.expectEmit(true, true, true, true); @@ -2867,7 +2757,8 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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); @@ -2905,18 +2796,30 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { operatorsRegistry.setOperatorLimits(operators, limits, block.number); OperatorsRegistryInitializableV1(address(operatorsRegistry)) - .debugPickNextValidatorsToDepositFromActiveOperators(_createAllocation(operators, limits)); + .debugPickNextValidatorsToDepositFromActiveOperators(_createMultiAllocation(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.prank(river); + operatorsRegistry.demandValidatorExits(30, 250); 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); + operators = new uint256[](2); + operators[0] = 0; + operators[1] = 1; + + limits = new uint32[](2); + + limits[0] = 20; + limits[1] = 10; + + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createMultiAllocation(operators, limits)); assert(operatorsRegistry.getOperator(0).requestedExits == 20); assert(operatorsRegistry.getOperator(1).requestedExits == 10); @@ -2926,37 +2829,29 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { 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.prank(river); + operatorsRegistry.demandValidatorExits(70, 250); + operators = new uint256[](4); + operators[0] = 0; + operators[1] = 1; + operators[2] = 2; + operators[3] = 3; + limits = new uint32[](4); + limits[0] = 30; + limits[1] = 20; + limits[2] = 10; + limits[3] = 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); + emit RequestedValidatorExits(1, 30); vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(3, 30); + emit RequestedValidatorExits(2, 10); vm.expectEmit(true, true, true, true); - emit RequestedValidatorExits(4, 10); - OperatorsRegistryInitializableV1(address(operatorsRegistry)).debugGetNextValidatorsToExitFromActiveOperators(50); + emit RequestedValidatorExits(3, 10); + vm.prank(keeper); + operatorsRegistry.requestValidatorExits(_createMultiAllocation(operators, limits)); } function testDecreasingStoppedValidatorCounts(uint8 decreasingIndex, uint8[5] memory fuzzedStoppedValidatorCount) @@ -3611,19 +3506,7 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { assert(signatures.length == 0); } - function testPickNextValidatorsToDepositForNoOperators() public { - // Create an allocation with no operators - IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 10}); - - (bytes[] memory publicKeys, bytes[] memory signatures) = OperatorsRegistryInitializableV1( - address(operatorsRegistry) - ).debugPickNextValidatorsToDepositFromActiveOperators(allocation); - assert(publicKeys.length == 0); - assert(signatures.length == 0); - } - - function testGetNextValidatorsToDepositRevertsDuplicateOperatorIndex() public { + function testPickNextValidatorsToDepositRevertsUnorderedOperatorIndex() public { bytes[] memory rawKeys = new bytes[](2); rawKeys[0] = genBytes((48 + 96) * 10); rawKeys[1] = genBytes((48 + 96) * 10); @@ -3642,76 +3525,90 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - // Create allocation with duplicate operator index + // Create allocation with unordered operator indices (1 before 0) 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! + 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.getNextValidatorsToDepositFromActiveOperators(allocation); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); } - function testGetNextValidatorsToDepositRevertsUnorderedOperatorIndex() public { - bytes[] memory rawKeys = new bytes[](2); + function testPickNextValidatorsToDepositRevertsInactiveOperator() public { + bytes[] memory rawKeys = new bytes[](1); 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); + uint256[] memory operators = new uint256[](1); operators[0] = 0; - operators[1] = 1; - uint32[] memory limits = new uint32[](2); + uint32[] memory limits = new uint32[](1); 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! + // 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("UnorderedOperatorList()")); - operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); + vm.prank(river); + vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); + operatorsRegistry.pickNextValidatorsToDeposit(allocation); } - function testPickNextValidatorsToDepositRevertsDuplicateOperatorIndex() public { - bytes[] memory rawKeys = new bytes[](2); - rawKeys[0] = genBytes((48 + 96) * 10); - rawKeys[1] = genBytes((48 + 96) * 10); + 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, rawKeys[0]); - operatorsRegistry.addValidators(1, 10, rawKeys[1]); + operatorsRegistry.addValidators(0, 10, tenKeys); + operatorsRegistry.addValidators(1, 10, tenKeys); + operatorsRegistry.addValidators(2, 10, tenKeys); vm.stopPrank(); - uint256[] memory operators = new uint256[](2); + uint256[] memory operators = new uint256[](3); operators[0] = 0; operators[1] = 1; - uint32[] memory limits = new uint32[](2); + 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); - // 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! + // 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("UnorderedOperatorList()")); + vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); operatorsRegistry.pickNextValidatorsToDeposit(allocation); } - function testPickNextValidatorsToDepositRevertsUnorderedOperatorIndex() public { - bytes[] memory rawKeys = new bytes[](2); - rawKeys[0] = genBytes((48 + 96) * 10); - rawKeys[1] = genBytes((48 + 96) * 10); + function testPickNextValidatorsToDepositForNoOperators() public { + // Create an allocation with no operators + IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 10}); + + (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); + rawKeys[1] = genBytes((48 + 96) * 10); vm.startPrank(admin); operatorsRegistry.addValidators(0, 10, rawKeys[0]); @@ -3727,64 +3624,73 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - // Create allocation with unordered operator indices (1 before 0) + // Create allocation with duplicate operator index 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! + 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); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); } - function testGetNextValidatorsToDepositRevertsZeroValidatorCount() public { - bytes[] memory rawKeys = new bytes[](1); + 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[](1); + uint256[] memory operators = new uint256[](2); operators[0] = 0; - uint32[] memory limits = new uint32[](1); + 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 zero validator count - IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); + // 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("AllocationWithZeroValidatorCount()")); + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); } - function testPickNextValidatorsToDepositRevertsZeroValidatorCount() public { - bytes[] memory rawKeys = new bytes[](1); + 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[](1); + uint256[] memory operators = new uint256[](2); operators[0] = 0; - uint32[] memory limits = new uint32[](1); + 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 zero validator count - IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); + // 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("AllocationWithZeroValidatorCount()")); + vm.expectRevert(abi.encodeWithSignature("UnorderedOperatorList()")); operatorsRegistry.pickNextValidatorsToDeposit(allocation); } - function testGetNextValidatorsToDepositRevertsInactiveOperator() public { + function testGetNextValidatorsToDepositRevertsZeroValidatorCount() public { bytes[] memory rawKeys = new bytes[](1); rawKeys[0] = genBytes((48 + 96) * 10); @@ -3799,15 +3705,15 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - // Create allocation with operator index that doesn't exist (only operator 0 exists) + // Create allocation with zero validator count IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 99, validatorCount: 5}); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); - vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); } - function testPickNextValidatorsToDepositRevertsInactiveOperator() public { + function testPickNextValidatorsToDepositRevertsZeroValidatorCount() public { bytes[] memory rawKeys = new bytes[](1); rawKeys[0] = genBytes((48 + 96) * 10); @@ -3822,47 +3728,36 @@ contract OperatorsRegistryV1TestDistribution is OperatorAllocationTestBase { vm.prank(admin); operatorsRegistry.setOperatorLimits(operators, limits, block.number); - // Create allocation with operator index that doesn't exist (only operator 0 exists) + // Create allocation with zero validator count IOperatorsRegistryV1.OperatorAllocation[] memory allocation = new IOperatorsRegistryV1.OperatorAllocation[](1); - allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 99, validatorCount: 5}); + allocation[0] = IOperatorsRegistryV1.OperatorAllocation({operatorIndex: 0, validatorCount: 0}); vm.prank(river); - vm.expectRevert(abi.encodeWithSignature("InactiveOperator(uint256)", 99)); + vm.expectRevert(abi.encodeWithSignature("AllocationWithZeroValidatorCount()")); 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); + function testGetNextValidatorsToDepositRevertsInactiveOperator() public { + bytes[] memory rawKeys = new bytes[](1); + rawKeys[0] = genBytes((48 + 96) * 10); vm.startPrank(admin); - operatorsRegistry.addValidators(0, 10, tenKeys); - operatorsRegistry.addValidators(1, 10, tenKeys); - operatorsRegistry.addValidators(2, 10, tenKeys); + operatorsRegistry.addValidators(0, 10, rawKeys[0]); vm.stopPrank(); - uint256[] memory operators = new uint256[](3); + uint256[] memory operators = new uint256[](1); operators[0] = 0; - operators[1] = 1; - operators[2] = 2; - uint32[] memory limits = new uint32[](3); + uint32[] memory limits = new uint32[](1); 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 + // 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); + operatorsRegistry.getNextValidatorsToDepositFromActiveOperators(allocation); } function testVersion() external { @@ -4546,4 +4441,750 @@ contract OperatorsRegistryV1AllocationCorrectnessTests is OperatorAllocationTest vm.prank(river); operatorsRegistry.pickNextValidatorsToDeposit(alloc); } + + // ============ 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 { + _setupOperators(1, 10); + 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 { + _setupOperators(1, 5); + 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 { + _setupOperators(1, 5); + 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 { + _setupOperators(2, 5); + 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 { + _setupOperators(1, 10); + 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 { + _setupOperators(1, 5); + 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 { + _setupOperators(1, 10); + 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 { + _setupOperators(2, 5); + 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 { + _setupOperators(2, 5); + 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); + } +} + +/// @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"); + } }