diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index 852a8fed9..8f66bff68 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -1175,8 +1175,8 @@ contract Lido is Versioned, StETHPermit, AragonApp { * 4. Pass the accounting values to sanity checker to smoothen positive token rebase * (i.e., postpone the extra rewards to be applied during the next rounds) * 5. Invoke finalization of the withdrawal requests - * 6. Distribute protocol fee (treasury & node operators) - * 7. Burn excess shares within the allowed limit (can postpone some shares to be burnt later) + * 6. Burn excess shares within the allowed limit (can postpone some shares to be burnt later) + * 7. Distribute protocol fee (treasury & node operators) * 8. Complete token rebase by informing observers (emit an event and call the external receivers if any) * 9. Sanity check for the provided simulated share rate */ @@ -1261,6 +1261,13 @@ contract Lido is Versioned, StETHPermit, AragonApp { ); // Step 6. + // Burn the previously requested shares + if (reportContext.sharesToBurn > 0) { + IBurner(contracts.burner).commitSharesToBurn(reportContext.sharesToBurn); + _burnShares(contracts.burner, reportContext.sharesToBurn); + } + + // Step 7. // Distribute protocol fee (treasury & node operators) reportContext.sharesMintedAsFees = _processRewards( reportContext, @@ -1269,13 +1276,6 @@ contract Lido is Versioned, StETHPermit, AragonApp { elRewards ); - // Step 7. - // Burn the previously requested shares - if (reportContext.sharesToBurn > 0) { - IBurner(contracts.burner).commitSharesToBurn(reportContext.sharesToBurn); - _burnShares(contracts.burner, reportContext.sharesToBurn); - } - // Step 8. // Complete token rebase by informing observers (emit an event and call the external receivers if any) ( diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 82153ac11..ba3cf7481 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -86,13 +86,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // SigningKeysStats /// @dev Operator's max validator keys count approved for deposit by the DAO - uint8 internal constant VETTED_KEYS_COUNT_OFFSET = 0; + uint8 internal constant TOTAL_VETTED_KEYS_COUNT_OFFSET = 0; /// @dev Number of keys in the EXITED state for this operator for all time - uint8 internal constant EXITED_KEYS_COUNT_OFFSET = 1; + uint8 internal constant TOTAL_EXITED_KEYS_COUNT_OFFSET = 1; /// @dev Total number of keys of this operator for all time uint8 internal constant TOTAL_KEYS_COUNT_OFFSET = 2; /// @dev Number of keys of this operator which were in DEPOSITED state for all time - uint8 internal constant DEPOSITED_KEYS_COUNT_OFFSET = 3; + uint8 internal constant TOTAL_DEPOSITED_KEYS_COUNT_OFFSET = 3; // TargetValidatorsStats /// @dev Flag enable/disable limiting target active validators count for operator @@ -109,11 +109,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @dev refunded keys count from dao uint8 internal constant REFUNDED_VALIDATORS_COUNT_OFFSET = 1; /// @dev extra penalty time after stuck keys resolved (refunded and/or exited) - /// @notice field is also used as flag for "half-cleaned" panlty status + /// @notice field is also used as flag for "half-cleaned" penalty status /// Operator is PENALIZED if `STUCK_VALIDATORS_COUNT > REFUNDED_VALIDATORS_COUNT` or - /// `STUCK_VALIDATORS_COUNT <= REFUNDED_VALIDATORS_COUNT && STUCK_PENALTY_END_TIMESTAMP <= refund timastamp + STUCK_PENALTY_DELAY` + /// `STUCK_VALIDATORS_COUNT <= REFUNDED_VALIDATORS_COUNT && STUCK_PENALTY_END_TIMESTAMP <= refund timestamp + STUCK_PENALTY_DELAY` /// When operator refund all stuck validators and time has pass STUCK_PENALTY_DELAY, but STUCK_PENALTY_END_TIMESTAMP not zeroed, - /// then Operator can receive reawards but can't get new deposits until the new Oracle report or `clearNodeOperatorPenalty` is called. + /// then Operator can receive rewards but can't get new deposits until the new Oracle report or `clearNodeOperatorPenalty` is called. uint8 internal constant STUCK_PENALTY_END_TIMESTAMP_OFFSET = 2; // Summary SigningKeysStats @@ -227,9 +227,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint64 depositedSigningKeysCount; for (uint256 nodeOperatorId; nodeOperatorId < totalOperators; ++nodeOperatorId) { signingKeysStats = _loadOperatorSigningKeysStats(nodeOperatorId); - vettedSigningKeysCountBefore = signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); + vettedSigningKeysCountBefore = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); totalSigningKeysCount = signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET); - depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); uint64 vettedSigningKeysCountAfter; if (!_nodeOperators[nodeOperatorId].active) { @@ -245,7 +245,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } if (vettedSigningKeysCountBefore != vettedSigningKeysCountAfter) { - signingKeysStats.set(VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCountAfter); + signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCountAfter); _saveOperatorSigningKeysStats(nodeOperatorId, signingKeysStats); emit VettedSigningKeysCountChanged(nodeOperatorId, vettedSigningKeysCountAfter); } @@ -265,7 +265,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { summarySigningKeysStats.set( SUMMARY_EXITED_KEYS_COUNT_OFFSET, summarySigningKeysStats.get(SUMMARY_EXITED_KEYS_COUNT_OFFSET).add( - signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET) + signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET) ) ); summarySigningKeysStats.set( @@ -354,12 +354,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit NodeOperatorActiveSet(_nodeOperatorId, false); Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - uint64 vettedSigningKeysCount = signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); - uint64 depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + uint64 vettedSigningKeysCount = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); + uint64 depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); // reset vetted keys count to the deposited validators count if (vettedSigningKeysCount > depositedSigningKeysCount) { - signingKeysStats.set(VETTED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); + signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); _saveOperatorSigningKeysStats(_nodeOperatorId, signingKeysStats); emit VettedSigningKeysCountChanged(_nodeOperatorId, depositedSigningKeysCount); @@ -407,8 +407,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _onlyCorrectNodeOperatorState(getNodeOperatorIsActive(_nodeOperatorId)); Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - uint64 vettedSigningKeysCountBefore = signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); - uint64 depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + uint64 vettedSigningKeysCountBefore = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); + uint64 depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); uint64 totalSigningKeysCount = signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET); uint64 vettedSigningKeysCountAfter = uint64( @@ -421,7 +421,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return; } - signingKeysStats.set(VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCountAfter); + signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCountAfter); _saveOperatorSigningKeysStats(_nodeOperatorId, signingKeysStats); emit VettedSigningKeysCountChanged(_nodeOperatorId, vettedSigningKeysCountAfter); @@ -563,15 +563,15 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); int64 totalExitedValidatorsDelta = - int64(_exitedValidatorsKeysCount) - int64(signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET)); + int64(_exitedValidatorsKeysCount) - int64(signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET)); if (totalExitedValidatorsDelta != 0) { - _requireValidRange(_exitedValidatorsKeysCount <= signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET)); + _requireValidRange(_exitedValidatorsKeysCount <= signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET)); if (totalExitedValidatorsDelta < 0 && !_allowDecrease) { revert("EXITED_VALIDATORS_COUNT_DECREASED"); } - signingKeysStats.set(EXITED_KEYS_COUNT_OFFSET, _exitedValidatorsKeysCount); + signingKeysStats.set(TOTAL_EXITED_KEYS_COUNT_OFFSET, _exitedValidatorsKeysCount); _saveOperatorSigningKeysStats(_nodeOperatorId, signingKeysStats); emit ExitedSigningKeysCountChanged(_nodeOperatorId, _exitedValidatorsKeysCount); @@ -611,16 +611,16 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { */ function _updateStuckValidatorsCount(uint256 _nodeOperatorId, uint64 _stuckValidatorsCount) internal { Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - uint64 curStuckValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); + uint64 curStuckValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); if (_stuckValidatorsCount == curStuckValidatorsCount) return; Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); _requireValidRange( _stuckValidatorsCount - <= signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET) - signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET) + <= signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET) - signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET) ); - uint64 curRefundedValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); + uint64 curRefundedValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); if (_stuckValidatorsCount <= curRefundedValidatorsCount && curStuckValidatorsCount > curRefundedValidatorsCount) { stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, uint64(block.timestamp + getStuckPenaltyDelay())); } @@ -643,7 +643,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { if (_refundedValidatorsCount == curRefundedValidatorsCount) return; Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - _requireValidRange(_refundedValidatorsCount <= signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET)); + _requireValidRange(_refundedValidatorsCount <= signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET)); uint64 curStuckValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); if (_refundedValidatorsCount >= curStuckValidatorsCount && curRefundedValidatorsCount < curStuckValidatorsCount) { @@ -661,7 +661,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateSummaryMaxValidatorsCount(_nodeOperatorId); } - // @dev Recalculate and update the max validoator count for operator and summary stats + // @dev Recalculate and update the max validator count for operator and summary stats function _updateSummaryMaxValidatorsCount(uint256 _nodeOperatorId) internal returns (int64 maxSigningKeysDelta) { maxSigningKeysDelta = _applyNodeOperatorLimits(_nodeOperatorId); if (maxSigningKeysDelta != 0) { @@ -702,13 +702,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { for (uint256 nodeOperatorId = _indexFrom; nodeOperatorId <= _indexTo; ++nodeOperatorId) { signingKeysStats = _loadOperatorSigningKeysStats(nodeOperatorId); - uint64 depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + uint64 depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); trimmedKeysCount = signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET) - depositedSigningKeysCount; if (trimmedKeysCount == 0) continue; totalTrimmedKeysCount += trimmedKeysCount; signingKeysStats.set(TOTAL_KEYS_COUNT_OFFSET, depositedSigningKeysCount); - signingKeysStats.set(VETTED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); + signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); _saveOperatorSigningKeysStats(nodeOperatorId, signingKeysStats); _updateSummaryMaxValidatorsCount(nodeOperatorId); @@ -766,8 +766,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); Packed64x4.Packed memory operatorTargetStats = _loadOperatorTargetValidatorsStats(_nodeOperatorId); - exitedSigningKeysCount = signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET); - depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + exitedSigningKeysCount = signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET); + depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); maxSigningKeysCount = operatorTargetStats.get(MAX_VALIDATORS_COUNT_OFFSET); } @@ -775,9 +775,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); Packed64x4.Packed memory operatorTargetStats = _loadOperatorTargetValidatorsStats(_nodeOperatorId); - uint64 exitedSigningKeysCount = signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET); - uint64 depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); - uint64 vettedSigningKeysCount = signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); + uint64 exitedSigningKeysCount = signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET); + uint64 depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); + uint64 vettedSigningKeysCount = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); uint64 oldMaxSigningKeysCount = operatorTargetStats.get(MAX_VALIDATORS_COUNT_OFFSET); uint64 newMaxSigningKeysCount = depositedSigningKeysCount; @@ -862,9 +862,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats; for (uint256 i; i < _nodeOperatorIds.length; ++i) { signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorIds[i]); - depositedSigningKeysCountBefore = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + depositedSigningKeysCountBefore = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); depositedSigningKeysCountAfter = - signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET) + uint64(_activeKeyCountsAfterAllocation[i]); + signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET) + uint64(_activeKeyCountsAfterAllocation[i]); keysCount = depositedSigningKeysCountAfter.sub(depositedSigningKeysCountBefore); if (keysCount == 0) continue; @@ -875,7 +875,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { loadedKeysCount += keysCount; emit DepositedSigningKeysCountChanged(_nodeOperatorIds[i], depositedSigningKeysCountAfter); - signingKeysStats.set(DEPOSITED_KEYS_COUNT_OFFSET, depositedSigningKeysCountAfter); + signingKeysStats.set(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET, depositedSigningKeysCountAfter); _saveOperatorSigningKeysStats(_nodeOperatorIds[i], signingKeysStats); _updateSummaryMaxValidatorsCount(_nodeOperatorIds[i]); } @@ -916,10 +916,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - stakingLimit = signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); - stoppedValidators = signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET); + stakingLimit = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); + stoppedValidators = signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET); totalSigningKeys = signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET); - usedSigningKeys = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + usedSigningKeys = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); } /// @notice Returns the rewards distribution proportional to the effective stake for each node operator. @@ -943,7 +943,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(operatorId); uint256 activeValidatorsCount = - signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET) - signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET); + signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET) - signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET); totalActiveValidatorsCount = totalActiveValidatorsCount.add(activeValidatorsCount); recipients[idx] = _nodeOperators[operatorId].rewardAddress; @@ -1067,17 +1067,17 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); uint256 totalSigningKeysCount = signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET); // comapring _fromIndex.add(_keysCount) <= totalSigningKeysCount is enough as totalSigningKeysCount is always less than MAX_UINT64 - _requireValidRange(_fromIndex >= signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET) && _fromIndex.add(_keysCount) <= totalSigningKeysCount); + _requireValidRange(_fromIndex >= signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET) && _fromIndex.add(_keysCount) <= totalSigningKeysCount); totalSigningKeysCount = SIGNING_KEYS_MAPPING_NAME.removeKeysSigs(_nodeOperatorId, _fromIndex, _keysCount, totalSigningKeysCount); signingKeysStats.set(TOTAL_KEYS_COUNT_OFFSET, uint64(totalSigningKeysCount)); emit TotalSigningKeysCountChanged(_nodeOperatorId, totalSigningKeysCount); - uint64 vettedSigningKeysCount = signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); + uint64 vettedSigningKeysCount = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); if (_fromIndex < vettedSigningKeysCount) { // decreasing the staking limit so the key at _index can't be used anymore - signingKeysStats.set(VETTED_KEYS_COUNT_OFFSET, uint64(_fromIndex)); + signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, uint64(_fromIndex)); emit VettedSigningKeysCountChanged(_nodeOperatorId, _fromIndex); } _saveOperatorSigningKeysStats(_nodeOperatorId, signingKeysStats); @@ -1106,7 +1106,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _onlyExistedNodeOperator(_nodeOperatorId); Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - return signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET) - signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + return signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET) - signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); } /// @notice Returns n-th signing key of the node operator #`_nodeOperatorId` @@ -1142,7 +1142,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); _requireValidRange(_offset.add(_limit) <= signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET)); - uint256 depositedSigningKeysCount = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + uint256 depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); (pubkeys, signatures) = SigningKeys.initKeysSigsBuf(_limit); used = new bool[](_limit); diff --git a/contracts/0.4.24/test_helpers/MockLegacyOracle.sol b/contracts/0.4.24/test_helpers/MockLegacyOracle.sol index f12162aa4..fc8b1f8fa 100644 --- a/contracts/0.4.24/test_helpers/MockLegacyOracle.sol +++ b/contracts/0.4.24/test_helpers/MockLegacyOracle.sol @@ -91,7 +91,11 @@ contract MockLegacyOracle is ILegacyOracle, LegacyOracle { } function initializeAsV3() external { - CONTRACT_VERSION_POSITION_DEPRECATED.setStorageUint256(3); + CONTRACT_VERSION_POSITION_DEPRECATED.setStorageUint256(3); + } + + function setLido(address lido) external { + LIDO_POSITION.setStorageAddress(lido); } } diff --git a/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol b/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol index c8a313d26..1fd6a7b88 100644 --- a/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol +++ b/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol @@ -9,12 +9,12 @@ contract NodeOperatorsRegistryMock is NodeOperatorsRegistry { function increaseNodeOperatorDepositedSigningKeysCount(uint256 _nodeOperatorId, uint64 _keysCount) external { Packed64x4.Packed memory signingKeysStats = _nodeOperators[_nodeOperatorId].signingKeysStats; - signingKeysStats.set(DEPOSITED_KEYS_COUNT_OFFSET, signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET) + _keysCount); + signingKeysStats.set(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET, signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET) + _keysCount); _nodeOperators[_nodeOperatorId].signingKeysStats = signingKeysStats; Packed64x4.Packed memory totalSigningKeysStats = _loadSummarySigningKeysStats(); totalSigningKeysStats.set( - DEPOSITED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET).add(_keysCount) + TOTAL_DEPOSITED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET).add(_keysCount) ); _saveSummarySigningKeysStats(totalSigningKeysStats); @@ -26,34 +26,34 @@ contract NodeOperatorsRegistryMock is NodeOperatorsRegistry { Packed64x4.Packed memory signingKeysStats; for (uint256 i; i < nodeOperatorsCount; ++i) { signingKeysStats = _loadOperatorSigningKeysStats(i); - testing_setDepositedSigningKeysCount(i, signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET)); + testing_setDepositedSigningKeysCount(i, signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET)); } } function testing_markAllKeysDeposited(uint256 _nodeOperatorId) external { _onlyExistedNodeOperator(_nodeOperatorId); Packed64x4.Packed memory signingKeysStats = _nodeOperators[_nodeOperatorId].signingKeysStats; - testing_setDepositedSigningKeysCount(_nodeOperatorId, signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET)); + testing_setDepositedSigningKeysCount(_nodeOperatorId, signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET)); } function testing_setDepositedSigningKeysCount(uint256 _nodeOperatorId, uint256 _depositedSigningKeysCount) public { _onlyExistedNodeOperator(_nodeOperatorId); // NodeOperator storage nodeOperator = _nodeOperators[_nodeOperatorId]; Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - uint64 depositedSigningKeysCountBefore = signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); + uint64 depositedSigningKeysCountBefore = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); if (_depositedSigningKeysCount == depositedSigningKeysCountBefore) { return; } require( - _depositedSigningKeysCount <= signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET), + _depositedSigningKeysCount <= signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET), "DEPOSITED_SIGNING_KEYS_COUNT_TOO_HIGH" ); require( - _depositedSigningKeysCount >= signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET), "DEPOSITED_SIGNING_KEYS_COUNT_TOO_LOW" + _depositedSigningKeysCount >= signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET), "DEPOSITED_SIGNING_KEYS_COUNT_TOO_LOW" ); - signingKeysStats.set(DEPOSITED_KEYS_COUNT_OFFSET, uint64(_depositedSigningKeysCount)); + signingKeysStats.set(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET, uint64(_depositedSigningKeysCount)); _saveOperatorSigningKeysStats(_nodeOperatorId, signingKeysStats); emit DepositedSigningKeysCountChanged(_nodeOperatorId, _depositedSigningKeysCount); @@ -87,9 +87,9 @@ contract NodeOperatorsRegistryMock is NodeOperatorsRegistry { operator.rewardAddress = _rewardAddress; Packed64x4.Packed memory signingKeysStats; - signingKeysStats.set(DEPOSITED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); - signingKeysStats.set(VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCount); - signingKeysStats.set(EXITED_KEYS_COUNT_OFFSET, exitedSigningKeysCount); + signingKeysStats.set(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); + signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCount); + signingKeysStats.set(TOTAL_EXITED_KEYS_COUNT_OFFSET, exitedSigningKeysCount); signingKeysStats.set(TOTAL_KEYS_COUNT_OFFSET, totalSigningKeysCount); operator.signingKeysStats = signingKeysStats; @@ -101,9 +101,9 @@ contract NodeOperatorsRegistryMock is NodeOperatorsRegistry { emit NodeOperatorAdded(id, _name, _rewardAddress, 0); Packed64x4.Packed memory totalSigningKeysStats = _loadSummarySigningKeysStats(); - totalSigningKeysStats.set(VETTED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(VETTED_KEYS_COUNT_OFFSET).add(vettedSigningKeysCount)); - totalSigningKeysStats.set(DEPOSITED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET).add(depositedSigningKeysCount)); - totalSigningKeysStats.set(EXITED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(EXITED_KEYS_COUNT_OFFSET).add(exitedSigningKeysCount)); + totalSigningKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET).add(vettedSigningKeysCount)); + totalSigningKeysStats.set(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET).add(depositedSigningKeysCount)); + totalSigningKeysStats.set(TOTAL_EXITED_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET).add(exitedSigningKeysCount)); totalSigningKeysStats.set(TOTAL_KEYS_COUNT_OFFSET, totalSigningKeysStats.get(TOTAL_KEYS_COUNT_OFFSET).add(totalSigningKeysCount)); _saveSummarySigningKeysStats(totalSigningKeysStats); } diff --git a/contracts/0.8.9/DepositSecurityModule.sol b/contracts/0.8.9/DepositSecurityModule.sol index 4439ff348..1de248148 100644 --- a/contracts/0.8.9/DepositSecurityModule.sol +++ b/contracts/0.8.9/DepositSecurityModule.sol @@ -26,6 +26,7 @@ interface IStakingRouter { function getStakingModuleIsActive(uint256 _stakingModuleId) external view returns (bool); function getStakingModuleNonce(uint256 _stakingModuleId) external view returns (uint256); function getStakingModuleLastDepositBlock(uint256 _stakingModuleId) external view returns (uint256); + function hasStakingModule(uint256 _stakingModuleId) external view returns (bool); } @@ -377,6 +378,8 @@ contract DepositSecurityModule { * such attestations will be enough to reach quorum. */ function canDeposit(uint256 stakingModuleId) external view returns (bool) { + if (!STAKING_ROUTER.hasStakingModule(stakingModuleId)) return false; + bool isModuleActive = STAKING_ROUTER.getStakingModuleIsActive(stakingModuleId); uint256 lastDepositBlock = STAKING_ROUTER.getStakingModuleLastDepositBlock(stakingModuleId); bool isLidoCanDeposit = LIDO.canDeposit(); diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index e47c09e46..6af21bba1 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -54,6 +54,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version error InvalidDepositsValue(uint256 etherValue, uint256 depositsCount); error StakingModuleAddressExists(); error ArraysLengthMismatch(uint256 firstArrayLength, uint256 secondArrayLength); + error UnrecoverableModuleError(); enum StakingModuleStatus { Active, // deposits and rewards allowed @@ -210,6 +211,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// https://docs.soliditylang.org/en/v0.8.17/types.html#enums newStakingModule.status = uint8(StakingModuleStatus.Active); + /// @dev Simulate zero value deposit to prevent real deposits into the new StakingModule via + /// DepositSecurityModule just after the addition. + /// See DepositSecurityModule.getMaxDeposits() for details + newStakingModule.lastDepositAt = uint64(block.timestamp); + newStakingModule.lastDepositBlock = block.number; + emit StakingRouterETHDeposited(newStakingModuleId, 0); + _setStakingModuleIndexById(newStakingModuleId, newStakingModuleIndex); LAST_STAKING_MODULE_ID_POSITION.setStorageUint256(newStakingModuleId); STAKING_MODULES_COUNT_POSITION.setStorageUint256(newStakingModuleIndex + 1); @@ -289,6 +297,12 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version address moduleAddr = _getStakingModuleById(_stakingModuleIds[i]).stakingModuleAddress; try IStakingModule(moduleAddr).onRewardsMinted(_totalShares[i]) {} catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the onRewardsMinted() reverts because of the + /// "out of gas" error. Here we assume that the onRewardsMinted() method doesn't + /// have reverts with empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); emit RewardsMintedReportFailed( _stakingModuleIds[i], lowLevelRevertData @@ -299,20 +313,63 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } + /// @notice Updates total numbers of exited validators for staking modules with the specified + /// module ids. + /// + /// @param _stakingModuleIds Ids of the staking modules to be updated. + /// @param _exitedValidatorsCounts New counts of exited validators for the specified staking modules. + /// + /// @return The total increase in the aggregate number of exited validators accross all updated modules. + /// + /// The total numbers are stored in the staking router and can differ from the totals obtained by calling + /// `IStakingModule.getStakingModuleSummary()`. The overall process of updating validator counts is the following: + /// + /// 1. In the first data submission phase, the oracle calls `updateExitedValidatorsCountByStakingModule` on the + /// staking router, passing the totals by module. The staking router stores these totals and uses them to + /// distribute new stake and staking fees between the modules. There can only be single call of this function + /// per oracle reporting frame. + /// + /// 2. In the first part of the second data submittion phase, the oracle calls + /// `StakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator` on the staking router which passes the + /// counts by node operator to the staking module by calling `IStakingModule.updateStuckValidatorsCount`. + /// This can be done multiple times for the same module, passing data for different subsets of node operators. + /// + /// 3. In the second part of the second data submittion phase, the oracle calls + /// `StakingRouter.reportStakingModuleExitedValidatorsCountByNodeOperator` on the staking router which passes + /// the counts by node operator to the staking module by calling `IStakingModule.updateExitedValidatorsCount`. + /// This can be done multiple times for the same module, passing data for different subsets of node + /// operators. + /// + /// 4. At the end of the second data submission phase, it's expected for the aggragate exited validators count + /// accross all module's node operators (stored in the module) to match the total count for this module + /// (stored in the staking router). However, it might happen that the second phase of data submission doesn't + /// finish until the new oracle reporting frame is started, in which case staking router will emit a warning + /// event `StakingModuleExitedValidatorsIncompleteReporting` when the first data submission phase is performed + /// for a new reporting frame. This condition will result in the staking module having an incomplete data about + /// the exited and maybe stuck validator counts during the whole reporting frame. Handling this condition is + /// the responsibility of each staking module. + /// + /// 5. When the second reporting phase is finshed, i.e. when the oracle submitted the complete data on the stuck + /// and exited validator counts per node operator for the current reporting frame, the oracle calls + /// `StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished` which, in turn, calls + /// `IStakingModule.onExitedAndStuckValidatorsCountsUpdated` on all modules. + /// function updateExitedValidatorsCountByStakingModule( uint256[] calldata _stakingModuleIds, uint256[] calldata _exitedValidatorsCounts ) external onlyRole(REPORT_EXITED_VALIDATORS_ROLE) + returns (uint256) { if (_stakingModuleIds.length != _exitedValidatorsCounts.length) { revert ArraysLengthMismatch(_stakingModuleIds.length, _exitedValidatorsCounts.length); } - uint256 stakingModuleId; + uint256 newlyExitedValidatorsCount; + for (uint256 i = 0; i < _stakingModuleIds.length; ) { - stakingModuleId = _stakingModuleIds[i]; + uint256 stakingModuleId = _stakingModuleIds[i]; StakingModule storage stakingModule = _getStakingModuleById(stakingModuleId); uint256 prevReportedExitedValidatorsCount = stakingModule.exitedValidatorsCount; @@ -320,6 +377,8 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version revert ExitedValidatorsCountCannotDecrease(); } + newlyExitedValidatorsCount += _exitedValidatorsCounts[i] - prevReportedExitedValidatorsCount; + ( uint256 totalExitedValidatorsCount, /* uint256 totalDepositedValidators */, @@ -337,8 +396,20 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version stakingModule.exitedValidatorsCount = _exitedValidatorsCounts[i]; unchecked { ++i; } } + + return newlyExitedValidatorsCount; } + /// @notice Updates exited validators counts per node operator for the staking module with + /// the specified id. + /// + /// See the docs for `updateExitedValidatorsCountByStakingModule` for the description of the + /// overall update process. + /// + /// @param _stakingModuleId The id of the staking modules to be updated. + /// @param _nodeOperatorIds Ids of the node operators to be updated. + /// @param _exitedValidatorsCounts New counts of exited validators for the specified node operators. + /// function reportStakingModuleExitedValidatorsCountByNodeOperator( uint256 _stakingModuleId, bytes calldata _nodeOperatorIds, @@ -440,6 +511,16 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } + /// @notice Updates stuck validators counts per node operator for the staking module with + /// the specified id. + /// + /// See the docs for `updateExitedValidatorsCountByStakingModule` for the description of the + /// overall update process. + /// + /// @param _stakingModuleId The id of the staking modules to be updated. + /// @param _nodeOperatorIds Ids of the node operators to be updated. + /// @param _stuckValidatorsCounts New counts of stuck validators for the specified node operators. + /// function reportStakingModuleStuckValidatorsCountByNodeOperator( uint256 _stakingModuleId, bytes calldata _nodeOperatorIds, @@ -453,6 +534,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version IStakingModule(moduleAddr).updateStuckValidatorsCount(_nodeOperatorIds, _stuckValidatorsCounts); } + /// @notice Called by the oracle when the second phase of data reporting finishes, i.e. when the + /// oracle submitted the complete data on the stuck and exited validator counts per node operator + /// for the current reporting frame. + /// + /// See the docs for `updateExitedValidatorsCountByStakingModule` for the description of the + /// overall update process. + /// function onValidatorsCountsByNodeOperatorReportingFinished() external onlyRole(REPORT_EXITED_VALIDATORS_ROLE) @@ -468,6 +556,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version // oracle finished updating exited validators for all node ops try moduleContract.onExitedAndStuckValidatorsCountsUpdated() {} catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the onExitedAndStuckValidatorsCountsUpdated() + /// reverts because of the "out of gas" error. Here we assume that the + /// onExitedAndStuckValidatorsCountsUpdated() method doesn't have reverts with + /// empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); emit ExitedAndStuckValidatorsCountsUpdateFailed( stakingModule.id, lowLevelRevertData @@ -479,16 +574,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } - function getExitedValidatorsCountAcrossAllModules() external view returns (uint256) { - uint256 stakingModulesCount = getStakingModulesCount(); - uint256 exitedValidatorsCount = 0; - for (uint256 i; i < stakingModulesCount; ) { - exitedValidatorsCount += _getStakingModuleByIndex(i).exitedValidatorsCount; - unchecked { ++i; } - } - return exitedValidatorsCount; - } - /** * @notice Returns all registered staking modules */ @@ -535,6 +620,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version return STAKING_MODULES_COUNT_POSITION.getStorageUint256(); } + /** + * @dev Returns true if staking module with the given id was registered via `addStakingModule`, false otherwise + */ + function hasStakingModule(uint256 _stakingModuleId) external view returns (bool) { + return _getStorageStakingIndicesMapping()[_stakingModuleId] != 0; + } + /** * @dev Returns status of staking module */ @@ -1049,6 +1141,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version try IStakingModule(stakingModule.stakingModuleAddress) .onWithdrawalCredentialsChanged() {} catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the onWithdrawalCredentialsChanged() + /// reverts because of the "out of gas" error. Here we assume that the + /// onWithdrawalCredentialsChanged() method doesn't have reverts with + /// empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); _setStakingModuleStatus(stakingModule, StakingModuleStatus.DepositsPaused); emit WithdrawalsCredentialsChangeFailed(stakingModule.id, lowLevelRevertData); } diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index e7c878de9..f48f4a474 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -79,6 +79,8 @@ interface IStakingModule { /// @notice Called by StakingRouter to signal that stETH rewards were minted for this module. /// @param _totalShares Amount of stETH shares that were minted to reward all node operators. + /// @dev IMPORTANT: this method SHOULD revert with empty error data ONLY because of "out of gas". + /// Details about error data: https://docs.soliditylang.org/en/v0.8.9/control-structures.html#error-handling-assert-require-revert-and-exceptions function onRewardsMinted(uint256 _totalShares) external; /// @notice Updates the number of the validators of the given node operator that were requested @@ -142,11 +144,17 @@ interface IStakingModule { /// operator in this module has actually received any updated counts as a result of the report /// but given that the total number of exited validators returned from getStakingModuleSummary /// is the same as StakingRouter expects based on the total count received from the oracle. + /// + /// @dev IMPORTANT: this method SHOULD revert with empty error data ONLY because of "out of gas". + /// Details about error data: https://docs.soliditylang.org/en/v0.8.9/control-structures.html#error-handling-assert-require-revert-and-exceptions function onExitedAndStuckValidatorsCountsUpdated() external; /// @notice Called by StakingRouter when withdrawal credentials are changed. /// @dev This method MUST discard all StakingModule's unused deposit data cause they become - /// invalid after the withdrawal credentials are changed + /// invalid after the withdrawal credentials are changed + /// + /// @dev IMPORTANT: this method SHOULD revert with empty error data ONLY because of "out of gas". + /// Details about error data: https://docs.soliditylang.org/en/v0.8.9/control-structures.html#error-handling-assert-require-revert-and-exceptions function onWithdrawalCredentialsChanged() external; /// @dev Event to be emitted on StakingModule's nonce change diff --git a/contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol b/contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol index caf199d87..a5e6bd052 100644 --- a/contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol +++ b/contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol @@ -10,26 +10,62 @@ import {Math256} from "../../common/lib/Math256.sol"; * This library implements positive rebase limiter for `stETH` token. * One needs to initialize `LimiterState` with the desired parameters: * - _rebaseLimit (limiter max value, nominated in LIMITER_PRECISION_BASE) - * - _totalPooledEther (see `Lido.getTotalPooledEther()`) - * - _totalShares (see `Lido.getTotalShares()`) + * - _preTotalPooledEther (see `Lido.getTotalPooledEther()`), pre-rebase value + * - _preTotalShares (see `Lido.getTotalShares()`), pre-rebase value * * The limiter allows to account for: * - consensus layer balance updates (can be either positive or negative) * - total pooled ether changes (withdrawing funds from vaults on execution layer) - * - total shares changes (coverage application) + * - total shares changes (burning due to coverage, NOR penalization, withdrawals finalization, etc.) */ - /** - * @dev Internal limiter representation struct (storing in memory) - */ + * @dev Internal limiter representation struct (storing in memory) + */ struct TokenRebaseLimiterData { - uint256 totalPooledEther; // total pooled ether pre-rebase - uint256 totalShares; // total shares before pre-rebase - uint256 rebaseLimit; // positive rebase limit (target value) - uint256 accumulatedRebase; // accumulated rebase (previous value) + uint256 preTotalPooledEther; // pre-rebase total pooled ether + uint256 preTotalShares; // pre-rebase total shares + uint256 currentTotalPooledEther; // intermediate total pooled ether amount while token rebase is in progress + uint256 positiveRebaseLimit; // positive rebase limit (target value) with 1e9 precision (`LIMITER_PRECISION_BASE`) } +/** + * + * Two-steps flow: account for total supply changes and then determine the shares allowed to be burnt. + * + * Conventions: + * R - token rebase limit (i.e, {postShareRate / preShareRate - 1} <= R); + * inc - total pooled ether increase; + * dec - total shares decrease. + * + * ### Step 1. Calculating the allowed total pooled ether changes (preTotalShares === postTotalShares) + * Used for `PositiveTokenRebaseLimiter.increaseEther()`, `PositiveTokenRebaseLimiter.decreaseEther()`. + * + * R = ((preTotalPooledEther + inc) / preTotalShares) / (preTotalPooledEther / preTotalShares) - 1 + * = ((preTotalPooledEther + inc) / preTotalShares) * (preTotalShares / preTotalPooledEther) - 1 + * = (preTotalPooledEther + inc) / preTotalPooledEther) - 1 + * = inc/preTotalPooledEther + * + * isolating inc: + * + * ``` inc = R * preTotalPooledEther ``` + * + * ### Step 2. Calculating the allowed to burn shares (preTotalPooledEther != currentTotalPooledEther) + * Used for `PositiveTokenRebaseLimiter.getSharesToBurnLimit()`. + * + * R = (currentTotalPooledEther / (preTotalShares - dec)) / (preTotalPooledEther / preTotalShares) - 1, + * let X = currentTotalPooledEther / preTotalPooledEther + * + * then: + * R = X * (preTotalShares / (preTotalShares - dec)) - 1, or + * (R+1) * (preTotalShares - dec) = X * preTotalShares + * + * isolating dec: + * dec * (R + 1) = (R + 1 - X) * preTotalShares => + * + * ``` dec = preTotalShares * (R + 1 - currentTotalPooledEther/preTotalPooledEther) / (R + 1) ``` + * + */ library PositiveTokenRebaseLimiter { /// @dev Precision base for the limiter (e.g.: 1e6 - 0.1%; 1e9 - 100%) uint256 public constant LIMITER_PRECISION_BASE = 10**9; @@ -37,26 +73,26 @@ library PositiveTokenRebaseLimiter { uint256 public constant UNLIMITED_REBASE = type(uint64).max; /** - * @dev Initialize the new `LimiterState` structure instance - * @param _rebaseLimit max limiter value (saturation point), see `LIMITER_PRECISION_BASE` - * @param _totalPooledEther total pooled ether, see `Lido.getTotalPooledEther()` - * @param _totalShares total shares, see `Lido.getTotalShares()` - * @return limiterState newly initialized limiter structure - */ + * @dev Initialize the new `LimiterState` structure instance + * @param _rebaseLimit max limiter value (saturation point), see `LIMITER_PRECISION_BASE` + * @param _preTotalPooledEther pre-rebase total pooled ether, see `Lido.getTotalPooledEther()` + * @param _preTotalShares pre-rebase total shares, see `Lido.getTotalShares()` + * @return limiterState newly initialized limiter structure + */ function initLimiterState( uint256 _rebaseLimit, - uint256 _totalPooledEther, - uint256 _totalShares + uint256 _preTotalPooledEther, + uint256 _preTotalShares ) internal pure returns (TokenRebaseLimiterData memory limiterState) { - if(_rebaseLimit == 0) revert TooLowTokenRebaseLimit(); - if(_rebaseLimit > UNLIMITED_REBASE) revert TooHighTokenRebaseLimit(); + if (_rebaseLimit == 0) revert TooLowTokenRebaseLimit(); + if (_rebaseLimit > UNLIMITED_REBASE) revert TooHighTokenRebaseLimit(); // special case - if(_totalPooledEther == 0) { _rebaseLimit = UNLIMITED_REBASE; } + if (_preTotalPooledEther == 0) { _rebaseLimit = UNLIMITED_REBASE; } - limiterState.totalPooledEther = _totalPooledEther; - limiterState.totalShares = _totalShares; - limiterState.rebaseLimit = _rebaseLimit; + limiterState.currentTotalPooledEther = limiterState.preTotalPooledEther = _preTotalPooledEther; + limiterState.preTotalShares = _preTotalShares; + limiterState.positiveRebaseLimit = _rebaseLimit; } /** @@ -65,50 +101,61 @@ library PositiveTokenRebaseLimiter { * @return true if limit is reached */ function isLimitReached(TokenRebaseLimiterData memory _limiterState) internal pure returns (bool) { - return _limiterState.accumulatedRebase == _limiterState.rebaseLimit; + if (_limiterState.positiveRebaseLimit == UNLIMITED_REBASE) return false; + if (_limiterState.currentTotalPooledEther < _limiterState.preTotalPooledEther) return false; + + uint256 accumulatedEther = _limiterState.currentTotalPooledEther - _limiterState.preTotalPooledEther; + uint256 accumulatedRebase; + + if (_limiterState.preTotalPooledEther > 0) { + accumulatedRebase = accumulatedEther * LIMITER_PRECISION_BASE / _limiterState.preTotalPooledEther; + } + + return accumulatedRebase >= _limiterState.positiveRebaseLimit; } /** - * @notice raise limit using the given amount of ether + * @notice decrease total pooled ether by the given amount of ether * @param _limiterState limit repr struct + * @param _etherAmount amount of ether to decrease */ - function raiseLimit(TokenRebaseLimiterData memory _limiterState, uint256 _etherAmount) internal pure { - if(_limiterState.rebaseLimit == UNLIMITED_REBASE) { return; } + function decreaseEther( + TokenRebaseLimiterData memory _limiterState, uint256 _etherAmount + ) internal pure { + if (_limiterState.positiveRebaseLimit == UNLIMITED_REBASE) return; - uint256 projectedLimit = _limiterState.rebaseLimit + ( - _etherAmount * LIMITER_PRECISION_BASE - ) / _limiterState.totalPooledEther; + if (_etherAmount > _limiterState.currentTotalPooledEther) revert NegativeTotalPooledEther(); - _limiterState.rebaseLimit = Math256.min(projectedLimit, UNLIMITED_REBASE); + _limiterState.currentTotalPooledEther -= _etherAmount; } /** - * @dev append ether and return the consumed value not exceeding the limit + * @dev increase total pooled ether up to the limit and return the consumed value (not exceeding the limit) * @param _limiterState limit repr struct * @param _etherAmount desired ether addition - * @return consumedEther allowed to add ether to not exceed the limit + * @return consumedEther appended ether still not exceeding the limit */ - function consumeLimit(TokenRebaseLimiterData memory _limiterState, uint256 _etherAmount) + function increaseEther( + TokenRebaseLimiterData memory _limiterState, uint256 _etherAmount + ) internal pure returns (uint256 consumedEther) { - if (_limiterState.rebaseLimit == UNLIMITED_REBASE) { - return _etherAmount; - } + if (_limiterState.positiveRebaseLimit == UNLIMITED_REBASE) return _etherAmount; - uint256 remainingRebase = _limiterState.rebaseLimit - _limiterState.accumulatedRebase; - uint256 remainingEther = (remainingRebase * _limiterState.totalPooledEther) / LIMITER_PRECISION_BASE; + uint256 prevPooledEther = _limiterState.currentTotalPooledEther; + _limiterState.currentTotalPooledEther += _etherAmount; - consumedEther = Math256.min(remainingEther, _etherAmount); + uint256 maxTotalPooledEther = _limiterState.preTotalPooledEther + + (_limiterState.positiveRebaseLimit * _limiterState.preTotalPooledEther) / LIMITER_PRECISION_BASE; - if (consumedEther == remainingEther) { - _limiterState.accumulatedRebase = _limiterState.rebaseLimit; - } else { - _limiterState.accumulatedRebase += ( - consumedEther * LIMITER_PRECISION_BASE - ) / _limiterState.totalPooledEther; - } + _limiterState.currentTotalPooledEther + = Math256.min(_limiterState.currentTotalPooledEther, maxTotalPooledEther); + + assert(_limiterState.currentTotalPooledEther >= prevPooledEther); + + return _limiterState.currentTotalPooledEther - prevPooledEther; } /** @@ -121,16 +168,18 @@ library PositiveTokenRebaseLimiter { pure returns (uint256 maxSharesToBurn) { - if (_limiterState.rebaseLimit == UNLIMITED_REBASE) { - return _limiterState.totalShares; - } + if (_limiterState.positiveRebaseLimit == UNLIMITED_REBASE) return _limiterState.preTotalShares; + + if (isLimitReached(_limiterState)) return 0; + + uint256 rebaseLimitPlus1 = _limiterState.positiveRebaseLimit + LIMITER_PRECISION_BASE; + uint256 pooledEtherRate = + (_limiterState.currentTotalPooledEther * LIMITER_PRECISION_BASE) / _limiterState.preTotalPooledEther; - uint256 remainingRebase = _limiterState.rebaseLimit - _limiterState.accumulatedRebase; - maxSharesToBurn = ( - _limiterState.totalShares * remainingRebase - ) / (LIMITER_PRECISION_BASE + remainingRebase); + maxSharesToBurn = (_limiterState.preTotalShares * (rebaseLimitPlus1 - pooledEtherRate)) / rebaseLimitPlus1; } error TooLowTokenRebaseLimit(); error TooHighTokenRebaseLimit(); + error NegativeTotalPooledEther(); } diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 53463f59d..8f38f82bd 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -57,12 +57,10 @@ interface IOracleReportSanityChecker { } interface IStakingRouter { - function getExitedValidatorsCountAcrossAllModules() external view returns (uint256); - function updateExitedValidatorsCountByStakingModule( uint256[] calldata moduleIds, uint256[] calldata exitedValidatorsCounts - ) external; + ) external returns (uint256); function reportStakingModuleExitedValidatorsCountByNodeOperator( uint256 stakingModuleId, @@ -92,10 +90,10 @@ contract AccountingOracle is BaseOracle { error LidoLocatorCannotBeZero(); error AdminCannotBeZero(); error LegacyOracleCannotBeZero(); + error LidoCannotBeZero(); error IncorrectOracleMigration(uint256 code); error SenderNotAllowed(); error InvalidExitedValidatorsData(); - error NumExitedValidatorsCannotDecrease(); error UnsupportedExtraDataFormat(uint256 format); error UnsupportedExtraDataType(uint256 itemIndex, uint256 dataType); error CannotSubmitExtraDataBeforeMainData(); @@ -154,6 +152,7 @@ contract AccountingOracle is BaseOracle { { if (lidoLocator == address(0)) revert LidoLocatorCannotBeZero(); if (legacyOracle == address(0)) revert LegacyOracleCannotBeZero(); + if (lido == address(0)) revert LidoCannotBeZero(); LOCATOR = ILidoLocator(lidoLocator); LIDO = lido; LEGACY_ORACLE = legacyOracle; @@ -655,32 +654,24 @@ contract AccountingOracle is BaseOracle { unchecked { ++i; } } - uint256 exitedValidators = 0; for (uint256 i = 0; i < stakingModuleIds.length;) { if (numExitedValidatorsByStakingModule[i] == 0) { revert InvalidExitedValidatorsData(); - } else { - exitedValidators += numExitedValidatorsByStakingModule[i]; } unchecked { ++i; } } - uint256 prevExitedValidators = stakingRouter.getExitedValidatorsCountAcrossAllModules(); - if (exitedValidators < prevExitedValidators) { - revert NumExitedValidatorsCannotDecrease(); - } + uint256 newlyExitedValidatorsCount = stakingRouter.updateExitedValidatorsCountByStakingModule( + stakingModuleIds, + numExitedValidatorsByStakingModule + ); - uint256 exitedValidatorsPerDay = - (exitedValidators - prevExitedValidators) * (1 days) / + uint256 exitedValidatorsRatePerDay = + newlyExitedValidatorsCount * (1 days) / (SECONDS_PER_SLOT * slotsElapsed); IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) - .checkExitedValidatorsRatePerDay(exitedValidatorsPerDay); - - stakingRouter.updateExitedValidatorsCountByStakingModule( - stakingModuleIds, - numExitedValidatorsByStakingModule - ); + .checkExitedValidatorsRatePerDay(exitedValidatorsRatePerDay); } function _submitReportExtraDataEmpty() internal { diff --git a/contracts/0.8.9/oracle/HashConsensus.sol b/contracts/0.8.9/oracle/HashConsensus.sol index b8a949d85..301f93bcb 100644 --- a/contracts/0.8.9/oracle/HashConsensus.sol +++ b/contracts/0.8.9/oracle/HashConsensus.sol @@ -134,7 +134,9 @@ contract HashConsensus is AccessControlEnumerable { } struct ReportVariant { + // the reported hash bytes32 hash; + // how many unique members from the current set reported this hash in the current frame uint64 support; } @@ -230,8 +232,8 @@ contract HashConsensus is AccessControlEnumerable { /// Time /// - /// @notice Returns the chain configuration required to calculate - /// epoch and slot given a timestamp. + /// @notice Returns the immutable chain parameters required to calculate epoch and slot + /// given a timestamp. /// function getChainConfig() external view returns ( uint256 slotsPerEpoch, @@ -241,14 +243,31 @@ contract HashConsensus is AccessControlEnumerable { return (SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME); } - /// @notice Returns the parameters required to calculate reporting frame given an epoch. + /// @notice Returns the time-related configuration. /// - function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame, uint256 fastLaneLengthSlots) { - return (_frameConfig.initialEpoch, _frameConfig.epochsPerFrame, _frameConfig.fastLaneLengthSlots); + /// @return initialEpoch Epoch of the frame with zero index. + /// @return epochsPerFrame Length of a frame in epochs. + /// @return fastLaneLengthSlots Length of the fast lane interval in slots; see `getIsFastLaneMember`. + /// + function getFrameConfig() external view returns ( + uint256 initialEpoch, + uint256 epochsPerFrame, + uint256 fastLaneLengthSlots + ) { + FrameConfig memory config = _frameConfig; + return (config.initialEpoch, config.epochsPerFrame, config.fastLaneLengthSlots); } /// @notice Returns the current reporting frame. /// + /// @return refSlot The frame's reference slot: if the data the consensus is being reached upon + /// includes or depends on any onchain state, this state should be queried at the + /// reference slot. If the slot contains a block, the state should include all changes + /// from that block. + /// + /// @return reportProcessingDeadlineSlot The last slot at which the report can be processed by + /// the report processor contract. + /// function getCurrentFrame() external view returns ( uint256 refSlot, uint256 reportProcessingDeadlineSlot @@ -257,13 +276,14 @@ contract HashConsensus is AccessControlEnumerable { return (frame.refSlot, frame.reportProcessingDeadlineSlot); } - /// @notice Returns the earliest possible reference slot. + /// @notice Returns the earliest possible reference slot, i.e. the reference slot of the + /// reporting frame with zero index. /// function getInitialRefSlot() external view returns (uint256) { return _getInitialFrame().refSlot; } - /// @notice Sets initial epoch given that the current initial epoch is in the future. + /// @notice Sets a new initial epoch given that the current initial epoch is in the future. /// /// @param initialEpoch The new initial epoch. /// @@ -286,6 +306,11 @@ contract HashConsensus is AccessControlEnumerable { } } + /// @notice Updates the time-related configuration. + /// + /// @param epochsPerFrame Length of a frame in epochs. + /// @param fastLaneLengthSlots Length of the fast lane interval in slots; see `getIsFastLaneMember`. + /// function setFrameConfig(uint256 epochsPerFrame, uint256 fastLaneLengthSlots) external onlyRole(MANAGE_FRAME_CONFIG_ROLE) { @@ -300,6 +325,8 @@ contract HashConsensus is AccessControlEnumerable { /// Members /// + /// @notice Returns whether the given address is currently a member of the consensus. + /// function getIsMember(address addr) external view returns (bool) { return _isMember(addr); } @@ -307,13 +334,31 @@ contract HashConsensus is AccessControlEnumerable { /// @notice Returns whether the given address is a fast lane member for the current reporting /// frame. /// - /// Fast lane members can, and expected to, submit a report during the first part of the frame - /// defined via `setFastLaneConfig`. Non-fast-lane members are only allowed to submit a report - /// after the "fast-lane" part of the frame passes. + /// Fast lane members is a subset of all members that changes each reporting frame. These + /// members can, and are expected to, submit a report during the first part of the frame called + /// the "fast lane interval" and defined via `setFrameConfig` or `setFastLaneLengthSlots`. Under + /// regular circumstances, all other members are only allowed to submit a report after the fast + /// lane interval passes. + /// + /// The fast lane subset consists of `quorum` members; selection is implemented as a sliding + /// window of the `quorum` width over member indices (mod total members). The window advances + /// by one index each reporting frame. /// - /// This is done to encourage each oracle from the full set to participate in reporting on a + /// This is done to encourage each member from the full set to participate in reporting on a /// regular basis, and identify any malfunctioning members. /// + /// With the fast lane mechanism active, it's sufficient for the monitoring to check that + /// consensus is consistently reached during the fast lane part of each frame to conclude that + /// all members are active and share the same consensus rules. + /// + /// However, there is no guarantee that, at any given time, it holds true that only the current + /// fast lane members can or were able to report during the currently-configured fast lane + /// interval of the current frame. In particular, this assumption can be violated in any frame + /// during which the members set, initial epoch, or the quorum number was changed, or the fast + /// lane interval length was increased. Thus, the fast lane mechanism should not be used for any + /// purpose other than monitoring of the members liveness, and monitoring tools should take into + /// consideration the potential irregularities within frames with any configuration changes. + /// function getIsFastLaneMember(address addr) external view returns (bool) { uint256 index1b = _memberIndices1b[addr]; unchecked { @@ -321,6 +366,9 @@ contract HashConsensus is AccessControlEnumerable { } } + /// @notice Returns all current members, together with the last reference slot each member + /// submitted a report for. + /// function getMembers() external view returns ( address[] memory addresses, uint256[] memory lastReportedRefSlots @@ -328,8 +376,10 @@ contract HashConsensus is AccessControlEnumerable { return _getMembers(false); } - /// @notice Returns the subset of oracle committee members (consisting of `quorum` items) that - /// changes on each frame. See `getIsFastLaneMember`. + /// @notice Returns the subset of the oracle committee members (consisting of `quorum` items) + /// that changes each frame. + /// + /// See `getIsFastLaneMember`. /// function getFastLaneMembers() external view returns ( address[] memory addresses, @@ -338,16 +388,9 @@ contract HashConsensus is AccessControlEnumerable { return _getMembers(true); } - /// @notice Sets the duration of the interval starting at the beginning of the frame during - /// which only the selected "fast lane" subset of oracle committee memebrs can (and expected - /// to) submit a report. - /// - /// The fast lane subset is a subset consisting of `quorum` oracles that changes on each frame. - /// This is done to encourage each oracle from the full set to participate in reporting on a - /// regular basis, and identify any malfunctioning members. + /// @notice Sets the duration of the fast lane interval of the reporting frame. /// - /// The subset selection is implemented as a sliding window of the `quorum` width over member - /// indices (mod total members). The window advances by one index each reporting frame. + /// See `getIsFastLaneMember`. /// /// @param fastLaneLengthSlots The length of the fast lane reporting interval in slots. Setting /// it to zero disables the fast lane subset, allowing any oracle to report starting from diff --git a/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol b/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol index f4b40ac52..023c9654b 100644 --- a/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol +++ b/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol @@ -101,11 +101,11 @@ contract OracleReportSanityChecker is AccessControlEnumerable { using LimitsListUnpacker for LimitsListPacked; using PositiveTokenRebaseLimiter for TokenRebaseLimiterData; - bytes32 public constant ALL_LIMITS_MANAGER_ROLE = keccak256("LIMITS_MANAGER_ROLE"); + bytes32 public constant ALL_LIMITS_MANAGER_ROLE = keccak256("ALL_LIMITS_MANAGER_ROLE"); bytes32 public constant CHURN_VALIDATORS_PER_DAY_LIMIT_MANGER_ROLE = keccak256("CHURN_VALIDATORS_PER_DAY_LIMIT_MANGER_ROLE"); bytes32 public constant ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE = - keccak256("ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE_ROLE"); + keccak256("ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE"); bytes32 public constant ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE = keccak256("ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE"); bytes32 public constant SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE = @@ -368,17 +368,28 @@ contract OracleReportSanityChecker is AccessControlEnumerable { ); if (_postCLBalance < _preCLBalance) { - tokenRebaseLimiter.raiseLimit(_preCLBalance - _postCLBalance); + tokenRebaseLimiter.decreaseEther(_preCLBalance - _postCLBalance); } else { - tokenRebaseLimiter.consumeLimit(_postCLBalance - _preCLBalance); + tokenRebaseLimiter.increaseEther(_postCLBalance - _preCLBalance); } - withdrawals = tokenRebaseLimiter.consumeLimit(_withdrawalVaultBalance); - elRewards = tokenRebaseLimiter.consumeLimit(_elRewardsVaultBalance); + withdrawals = tokenRebaseLimiter.increaseEther(_withdrawalVaultBalance); + elRewards = tokenRebaseLimiter.increaseEther(_elRewardsVaultBalance); + // determining the shares to burn limit that would have been + // if no withdrawals finalized during the report + // it's used to check later the provided `simulatedShareRate` value + // after the off-chain calculation via `eth_call` of `Lido.handleOracleReport()` + // see also step 9 of the `Lido._handleOracleReport()` simulatedSharesToBurn = Math256.min(tokenRebaseLimiter.getSharesToBurnLimit(), _sharesRequestedToBurn); - tokenRebaseLimiter.raiseLimit(_etherToLockForWithdrawals); - sharesToBurn = Math256.min(tokenRebaseLimiter.getSharesToBurnLimit(), _newSharesToBurnForWithdrawals + _sharesRequestedToBurn); + + // remove ether to lock for withdrawals from total pooled ether + tokenRebaseLimiter.decreaseEther(_etherToLockForWithdrawals); + // re-evaluate shares to burn after TVL was updated due to withdrawals finalization + sharesToBurn = Math256.min( + tokenRebaseLimiter.getSharesToBurnLimit(), + _newSharesToBurnForWithdrawals + _sharesRequestedToBurn + ); } /// @notice Applies sanity checks to the accounting params of Lido's oracle report @@ -564,14 +575,14 @@ contract OracleReportSanityChecker is AccessControlEnumerable { uint256 _postCLBalance, uint256 _timeElapsed ) internal pure { - if (_preCLBalance >= _postCLBalance) return; - // allow zero values for scratch deploy // NB: annual increase have to be large enough for scratch deploy if (_preCLBalance == 0) { _preCLBalance = DEFAULT_CL_BALANCE; } + if (_preCLBalance >= _postCLBalance) return; + if (_timeElapsed == 0) { _timeElapsed = DEFAULT_TIME_ELAPSED; } @@ -660,15 +671,15 @@ contract OracleReportSanityChecker is AccessControlEnumerable { emit ChurnValidatorsPerDayLimitSet(_newLimitsList.churnValidatorsPerDayLimit); } if (_oldLimitsList.oneOffCLBalanceDecreaseBPLimit != _newLimitsList.oneOffCLBalanceDecreaseBPLimit) { - _checkLimitValue(_newLimitsList.oneOffCLBalanceDecreaseBPLimit, type(uint16).max); + _checkLimitValue(_newLimitsList.oneOffCLBalanceDecreaseBPLimit, MAX_BASIS_POINTS); emit OneOffCLBalanceDecreaseBPLimitSet(_newLimitsList.oneOffCLBalanceDecreaseBPLimit); } if (_oldLimitsList.annualBalanceIncreaseBPLimit != _newLimitsList.annualBalanceIncreaseBPLimit) { - _checkLimitValue(_newLimitsList.annualBalanceIncreaseBPLimit, type(uint16).max); + _checkLimitValue(_newLimitsList.annualBalanceIncreaseBPLimit, MAX_BASIS_POINTS); emit AnnualBalanceIncreaseBPLimitSet(_newLimitsList.annualBalanceIncreaseBPLimit); } if (_oldLimitsList.simulatedShareRateDeviationBPLimit != _newLimitsList.simulatedShareRateDeviationBPLimit) { - _checkLimitValue(_newLimitsList.simulatedShareRateDeviationBPLimit, type(uint16).max); + _checkLimitValue(_newLimitsList.simulatedShareRateDeviationBPLimit, MAX_BASIS_POINTS); emit SimulatedShareRateDeviationBPLimitSet(_newLimitsList.simulatedShareRateDeviationBPLimit); } if (_oldLimitsList.maxValidatorExitRequestsPerReport != _newLimitsList.maxValidatorExitRequestsPerReport) { diff --git a/contracts/0.8.9/test_helpers/ContractStub.sol b/contracts/0.8.9/test_helpers/ContractStub.sol new file mode 100644 index 000000000..202189a18 --- /dev/null +++ b/contracts/0.8.9/test_helpers/ContractStub.sol @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + +/// +/// DATA TYPES +/// + +/// @notice Stores method stubs of the ContractStub's frame +struct ContractStubFrame { + /// @notice list of method stubs declared in the given frame (order is not guaranteed) + MethodStub[] methodStubs; + /// @notice method stub indices increased to 1 by the id of methods stub + mapping(bytes32 => uint256) indicesByIdOneBased; +} + +/// @notice Method stub config +struct MethodStub { + /// @notice value of the msg.data on which method stub will be triggered on + bytes input; + /// @notice abi encoded data to be returned or reverted from the method stub + bytes output; + /// @notice whether method ends with Yul's revert() or return() instruction + bool isRevert; + /// @notice potentially state modifying side effects. Side effects take place + /// ONLY when isRevert is set to false. + SideEffects sideEffects; +} + +/// @notice Side effects of the method stub +struct SideEffects { + /// @notice whether the ContractStub__call event should be emitted on method stub execution + bool traceable; + /// @notice number of the frame to set as active after the method stub executed + uint256 nextFrame; + /// @notice logs to generate during method stub execution + Log[] logs; + /// @notice list of calls to external contracts + ExternalCall[] externalCalls; + /// @notice ETH transfers to make via ETHForwarder contract instances. Use when a recipient + /// doesn't accept ETH by default + ForwardETH[] ethForwards; +} + +/// @notice Stores Yul's log instruction data +struct Log { + LogType logType; + bytes data; + bytes32 t1; + bytes32 t2; + bytes32 t3; + bytes32 t4; +} + +/// @notice Type of Yul's log instruction +enum LogType { + LOG0, + LOG1, + LOG2, + LOG3, + LOG4 +} + +struct ForwardETH { + address payable recipient; + uint256 value; +} + +struct ExternalCall { + address payable callee; + bytes data; + uint256 value; + uint256 gas; +} + +/// +/// MAIN CONTRACTS +/// + +/// @notice Allows to stub the functionality of the Solidity contract +/// @dev WARNING: !!! DO NOT USE IT IN PRODUCTION !!! +contract ContractStub { + ContractStubStorage private immutable STORAGE; + bytes4 private immutable GET_STORAGE_ADDRESS_METHOD_ID; + + constructor(bytes4 _getStorageAddressMethodId) { + STORAGE = new ContractStubStorage(); + GET_STORAGE_ADDRESS_METHOD_ID = _getStorageAddressMethodId; + } + + // solhint-disable-next-line + fallback() external payable { + if (bytes4(msg.data) == GET_STORAGE_ADDRESS_METHOD_ID) { + _return(abi.encodePacked(address(STORAGE))); + } + MethodStub memory stub = _getMethodStub(); + if (stub.isRevert) { + _revert(stub.output); + } + _logEvents(stub.sideEffects.logs); + _forwardETH(stub.sideEffects.ethForwards); + _makeExternalCalls(stub.sideEffects.externalCalls); + _switchFrame(stub.sideEffects.nextFrame); + _leaveTrace(stub.sideEffects.traceable); + _return(stub.output); + } + + function _getMethodStub() internal view returns (MethodStub memory) { + return + STORAGE.hasMethodStub(msg.data) + ? STORAGE.getMethodStub(msg.data) + : STORAGE.getMethodStub(msg.data[:4]); + } + + function _revert(bytes memory _data) internal pure { + assembly { + revert(add(_data, 32), mload(_data)) + } + } + + function _return(bytes memory _data) internal pure { + assembly { + return(add(_data, 32), mload(_data)) + } + } + + function _switchFrame(uint256 _nextFrame) internal { + if (_nextFrame != type(uint256).max) { + STORAGE.activateFrame(_nextFrame); + } + } + + function _leaveTrace(bool _isTraceable) internal { + if (!_isTraceable) return; + emit ContractStub__called( + msg.sender, + bytes4(msg.data[:4]), + msg.data[4:], + msg.value, + block.number + ); + } + + function _logEvents(Log[] memory _logs) internal { + for (uint256 i = 0; i < _logs.length; ++i) { + bytes32 t1 = _logs[i].t1; + bytes32 t2 = _logs[i].t2; + bytes32 t3 = _logs[i].t3; + bytes32 t4 = _logs[i].t4; + bytes memory data = _logs[i].data; + uint256 dataLength = data.length; + if (_logs[i].logType == LogType.LOG0) { + assembly { + log0(add(data, 32), dataLength) + } + } else if (_logs[i].logType == LogType.LOG1) { + assembly { + log1(add(data, 32), dataLength, t1) + } + } else if (_logs[i].logType == LogType.LOG2) { + assembly { + log2(add(data, 32), dataLength, t1, t2) + } + } else if (_logs[i].logType == LogType.LOG3) { + assembly { + log3(add(data, 32), dataLength, t1, t2, t3) + } + } else if (_logs[i].logType == LogType.LOG4) { + assembly { + log4(add(data, 32), dataLength, t1, t2, t3, t4) + } + } + } + } + + function _forwardETH(ForwardETH[] memory _ethForwards) internal { + for (uint256 i = 0; i < _ethForwards.length; ++i) { + ForwardETH memory ethForward = _ethForwards[i]; + new ETHForwarder{ value: ethForward.value }(ethForward.recipient); + emit ContractStub__ethSent(ethForward.recipient, ethForward.value); + } + } + + function _makeExternalCalls(ExternalCall[] memory _calls) internal { + for (uint256 i = 0; i < _calls.length; ++i) { + ExternalCall memory externalCall = _calls[i]; + (bool success, bytes memory data) = externalCall.callee.call{ + value: externalCall.value, + gas: externalCall.gas == 0 ? gasleft() : externalCall.gas + }(externalCall.data); + emit ContractStub__callResult(externalCall, success, data); + } + } + + // solhint-disable-next-line + event ContractStub__ethSent(address recipient, uint256 value); + + // solhint-disable-next-line + event ContractStub__called( + address caller, + bytes4 methodId, + bytes callData, + uint256 value, + uint256 blockNumber + ); + + // solhint-disable-next-line + event ContractStub__callResult( + ExternalCall call, + bool success, + bytes response + ); +} + +/// @notice Keeps the state of the ContractStub instance +contract ContractStubStorage { + uint256 private constant EMPTY_FRAME_ID = type(uint256).max; + + mapping(uint256 => ContractStubFrame) private frames; + uint256 public currentFrameNumber; + + function hasMethodStub(bytes memory callData) external view returns (bool) { + bytes32 methodStubId = keccak256(callData); + return + frames[currentFrameNumber].indicesByIdOneBased[methodStubId] != 0; + } + + function getMethodStub( + bytes memory callData + ) external view returns (MethodStub memory) { + bytes32 methodStubId = keccak256(callData); + uint256 methodStubIndex = frames[currentFrameNumber] + .indicesByIdOneBased[methodStubId]; + if (methodStubIndex == 0) + revert ContractStub__MethodStubNotFound(callData); + return frames[currentFrameNumber].methodStubs[methodStubIndex - 1]; + } + + function addMethodStub( + uint256 _frameNumber, + MethodStub memory _methodStub + ) external { + uint256 frameNumber = _frameNumber == EMPTY_FRAME_ID + ? currentFrameNumber + : _frameNumber; + frames[frameNumber].methodStubs.push(); + bytes32 stubId = keccak256(_methodStub.input); + uint256 newStubIndex = frames[frameNumber].methodStubs.length - 1; + frames[frameNumber].indicesByIdOneBased[stubId] = newStubIndex + 1; + + MethodStub storage methodStub = frames[frameNumber].methodStubs[ + newStubIndex + ]; + + methodStub.input = _methodStub.input; + methodStub.output = _methodStub.output; + methodStub.isRevert = _methodStub.isRevert; + + SideEffects storage sideEffects = methodStub.sideEffects; + sideEffects.traceable = _methodStub.sideEffects.traceable; + sideEffects.nextFrame = _methodStub.sideEffects.nextFrame; + for (uint256 i = 0; i < _methodStub.sideEffects.logs.length; ++i) { + sideEffects.logs.push(_methodStub.sideEffects.logs[i]); + } + for ( + uint256 i = 0; + i < _methodStub.sideEffects.externalCalls.length; + ++i + ) { + sideEffects.externalCalls.push( + _methodStub.sideEffects.externalCalls[i] + ); + } + for ( + uint256 i = 0; + i < _methodStub.sideEffects.ethForwards.length; + ++i + ) { + sideEffects.ethForwards.push( + _methodStub.sideEffects.ethForwards[i] + ); + } + } + + function activateFrame(uint256 _frameNumber) external { + currentFrameNumber = _frameNumber; + } + + error ContractStub__MethodStubNotFound(bytes callData); +} + +/// +/// HELPER CONTRACTS +/// + +/// @notice Helper contract to transfer ether via selfdestruct +contract ETHForwarder { + constructor(address payable _recipient) payable { + selfdestruct(_recipient); + } +} diff --git a/contracts/0.8.9/test_helpers/ERC721ReceiverMock.sol b/contracts/0.8.9/test_helpers/ERC721ReceiverMock.sol index aae6ff1cc..92e5713d9 100644 --- a/contracts/0.8.9/test_helpers/ERC721ReceiverMock.sol +++ b/contracts/0.8.9/test_helpers/ERC721ReceiverMock.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.9; -import {IERC721Receiver} from "@openzeppelin/contracts-v4.4/token/ERC721/IERC721Receiver.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts-v4.4/token/ERC721/IERC721Receiver.sol"; -contract ERC721ReceiverMock is IERC721Receiver { +contract ERC721ReceiverMock is IERC721Receiver { bool public doesAcceptTokens; string public ERROR_MSG = "ERC721_NOT_ACCEPT_TOKENS"; @@ -22,6 +22,15 @@ contract ERC721ReceiverMock is IERC721Receiver { if (!doesAcceptTokens) { revert(ERROR_MSG); } - return bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")); + return + bytes4( + keccak256("onERC721Received(address,address,uint256,bytes)") + ); + } + + receive() external payable { + if (!doesAcceptTokens) { + revert(ERROR_MSG); + } } } diff --git a/contracts/0.8.9/test_helpers/GenericStub.sol b/contracts/0.8.9/test_helpers/GenericStub.sol deleted file mode 100644 index 2450113a8..000000000 --- a/contracts/0.8.9/test_helpers/GenericStub.sol +++ /dev/null @@ -1,224 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.9; - -enum LogType { - LOG0, - LOG1, - LOG2, - LOG3, - LOG4 -} - -contract ETHForwarder { - constructor(address payable _recipient) payable { - selfdestruct(_recipient); - } -} - -contract GenericStub { - type MethodID is bytes4; - type InputHash is bytes32; - type Topic is bytes32; - - // InputHash private immutable WILDCARD_INPUT_HASH; - - struct Log { - LogType logType; - bytes data; - bytes32 t1; - bytes32 t2; - bytes32 t3; - bytes32 t4; - } - - struct ForwardETH { - address payable recipient; - uint256 value; - } - - struct MethodStub { - /// @notice msg.data used for call - bytes input; - /// @notice abi encoded data to be returned from the method - bytes output; - /// @notice events to emit during method execution - Log[] logs; - /// @notice optional ETH send on method execution - ForwardETH forwardETH; - /// @notice shall method ends with revert instead of return - bool isRevert; - /// @notice index of the state to set as current after stub call - /// @dev this value is one based - uint256 nextStateIndexOneBased; - } - - struct StubState { - /// @notice list of all stubs (order is not guaranteed) - MethodStub[] stubs; - /// @notice indices of stubs increased to 1 - mapping(bytes32 => uint256) indicesByIdOneBased; - } - - StubState[] private _states; - uint256 private _currentStateIndexOneBased = 1; - - constructor() { - _states.push(); - } - - function GenericStub__addStub(MethodStub memory _stub) external { - GenericStub__addStub(_currentStateIndexOneBased - 1, _stub); - } - - function GenericStub__addStub(uint256 _stateIndex, MethodStub memory _stub) public { - StubState storage state = _getState(_stateIndex); - state.stubs.push(); - bytes32 stubId = keccak256(_stub.input); - uint256 newStubIndex = state.stubs.length - 1; - state.stubs[newStubIndex].input = _stub.input; - state.stubs[newStubIndex].output = _stub.output; - state.stubs[newStubIndex].forwardETH = _stub.forwardETH; - state.stubs[newStubIndex].isRevert = _stub.isRevert; - state.stubs[newStubIndex].nextStateIndexOneBased = _stub - .nextStateIndexOneBased; - - for (uint256 i = 0; i < _stub.logs.length; ++i) { - state.stubs[newStubIndex].logs.push(_stub.logs[i]); - } - state.indicesByIdOneBased[stubId] = newStubIndex + 1; - } - - function GenericStub__addState() external { - _states.push(); - } - - function GenericStub__setState(uint256 _stateIndex) external { - require(_stateIndex != 0, "INVALID_INDEX"); - if (_stateIndex > _states.length) { - revert GenericStub__StateIndexOutOfBounds( - _stateIndex, - _states.length - ); - } - _currentStateIndexOneBased = _stateIndex; - } - - // solhint-disable-next-line - fallback() external payable { - MethodStub memory stub = _getMethodStub(); - _forwardETH(stub.forwardETH); - _logEvents(stub.logs); - bytes memory output = stub.output; - uint256 outputLength = output.length; - if (stub.nextStateIndexOneBased != 0) { - _currentStateIndexOneBased = stub.nextStateIndexOneBased; - } - if (stub.isRevert) { - assembly { - revert(add(output, 32), outputLength) - } - } - // emit GenericStub__called( - // msg.sender, - // bytes4(msg.data[:4]), - // msg.data[4:], - // msg.value, - // block.number - // ); - assembly { - return(add(output, 32), outputLength) - } - } - - function _logEvents(Log[] memory _logs) internal { - for (uint256 i = 0; i < _logs.length; ++i) { - bytes32 t1 = _logs[i].t1; - bytes32 t2 = _logs[i].t2; - bytes32 t3 = _logs[i].t3; - bytes32 t4 = _logs[i].t4; - bytes memory data = _logs[i].data; - uint256 dataLength = data.length; - if (_logs[i].logType == LogType.LOG0) { - assembly { - log0(add(data, 32), dataLength) - } - } else if (_logs[i].logType == LogType.LOG1) { - assembly { - log1(add(data, 32), dataLength, t1) - } - } else if (_logs[i].logType == LogType.LOG2) { - assembly { - log2(add(data, 32), dataLength, t1, t2) - } - } else if (_logs[i].logType == LogType.LOG3) { - assembly { - log3(add(data, 32), dataLength, t1, t2, t3) - } - } else if (_logs[i].logType == LogType.LOG4) { - assembly { - log4(add(data, 32), dataLength, t1, t2, t3, t4) - } - } - } - } - - function _forwardETH(ForwardETH memory _ethForward) internal { - if (_ethForward.value == 0) return; - new ETHForwarder{ value: _ethForward.value }(_ethForward.recipient); - emit GenericStub__ethSent(_ethForward.recipient, _ethForward.value); - } - - function _getMethodStub() internal view returns (MethodStub memory) { - StubState storage currentState = _getState( - _currentStateIndexOneBased - 1 - ); - bytes32 methodStubId = keccak256(msg.data); - bytes32 methodStubWildcardId = keccak256(msg.data[:4]); - - uint256 methodStubIndex = currentState.indicesByIdOneBased[ - methodStubId - ]; - uint256 methodStubWildcardIndex = currentState.indicesByIdOneBased[ - methodStubWildcardId - ]; - - if (methodStubIndex == 0 && methodStubWildcardIndex == 0) { - revert GenericStub__MethodStubIsNotDefined(msg.data); - } - - return - methodStubIndex != 0 - ? currentState.stubs[methodStubIndex - 1] - : currentState.stubs[methodStubWildcardIndex - 1]; - } - - function _getState( - uint256 _stateIndex - ) internal view returns (StubState storage) { - if (_stateIndex >= _states.length) { - revert GenericStub__StateIndexOutOfBounds( - _stateIndex, - _states.length - ); - } - return _states[_stateIndex]; - } - - // solhint-disable-next-line - event GenericStub__ethSent(address recipient, uint256 value); - - // solhint-disable-next-line - event GenericStub__called( - address caller, - bytes4 methodId, - bytes callData, - uint256 value, - uint256 blockNumber - ); - - error GenericStub__StateIndexOutOfBounds(uint256 index, uint256 length); - error GenericStub__MethodStubIsNotDefined(bytes callData); - error GenericStub__ETHSendFailed(address recipient, uint256 value); -} diff --git a/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol b/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol index 5556f3339..f2f5e4df7 100644 --- a/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol +++ b/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol @@ -38,10 +38,19 @@ contract WithdrawalQueueStub is IWithdrawalQueue { } contract BurnerStub { + uint256 private nonCover; + uint256 private cover; + function getSharesRequestedToBurn() external view returns ( uint256 coverShares, uint256 nonCoverShares ) { - return (0, 0); + coverShares = cover; + nonCoverShares = nonCover; + } + + function setSharesRequestedToBurn(uint256 _cover, uint256 _nonCover) external { + cover = _cover; + nonCover = _nonCover; } } diff --git a/contracts/0.8.9/test_helpers/PositiveTokenRebaseLimiterMock.sol b/contracts/0.8.9/test_helpers/PositiveTokenRebaseLimiterMock.sol index a2e33b4d7..d87ab92b0 100644 --- a/contracts/0.8.9/test_helpers/PositiveTokenRebaseLimiterMock.sol +++ b/contracts/0.8.9/test_helpers/PositiveTokenRebaseLimiterMock.sol @@ -10,50 +10,44 @@ contract PositiveTokenRebaseLimiterMock { TokenRebaseLimiterData public limiter; - event ReturnValue ( - uint256 retValue - ); - function getLimiterValues() external view returns ( - uint256 totalPooledEther, - uint256 totalShares, - uint256 rebaseLimit, - uint256 accumulatedRebase + uint256 preTotalPooledEther, + uint256 preTotalShares, + uint256 currentTotalPooledEther, + uint256 positiveRebaseLimit ) { - totalPooledEther = limiter.totalPooledEther; - totalShares = limiter.totalShares; - rebaseLimit = limiter.rebaseLimit; - accumulatedRebase = limiter.accumulatedRebase; + preTotalPooledEther = limiter.preTotalPooledEther; + preTotalShares = limiter.preTotalShares; + currentTotalPooledEther = limiter.currentTotalPooledEther; + positiveRebaseLimit = limiter.positiveRebaseLimit; } function initLimiterState( uint256 _rebaseLimit, - uint256 _totalPooledEther, - uint256 _totalShares + uint256 _preTotalPooledEther, + uint256 _preTotalShares ) external { - limiter = PositiveTokenRebaseLimiter.initLimiterState(_rebaseLimit, _totalPooledEther, _totalShares); + limiter = PositiveTokenRebaseLimiter.initLimiterState(_rebaseLimit, _preTotalPooledEther, _preTotalShares); } function isLimitReached() external view returns (bool) { return limiter.isLimitReached(); } - function raiseLimit(uint256 _etherAmount) external { + function decreaseEther(uint256 _etherAmount) external { TokenRebaseLimiterData memory limiterMemory = limiter; - limiterMemory.raiseLimit(_etherAmount); + limiterMemory.decreaseEther(_etherAmount); limiter = limiterMemory; } - function consumeLimit(uint256 _etherAmount) external { + function increaseEther(uint256 _etherAmount) external returns (uint256 consumedEther) { TokenRebaseLimiterData memory limiterMemory = limiter; - uint256 consumedEther = limiterMemory.consumeLimit(_etherAmount); + consumedEther = limiterMemory.increaseEther(_etherAmount); limiter = limiterMemory; - - emit ReturnValue(consumedEther); } function getSharesToBurnLimit() external view returns (uint256) { diff --git a/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol b/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol index 49cc729f7..b078d4d37 100644 --- a/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol +++ b/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol @@ -9,58 +9,81 @@ import {StakingRouter} from "../StakingRouter.sol"; contract StakingRouterMockForDepositSecurityModule is IStakingRouter { + error StakingModuleUnregistered(); + event StakingModuleDeposited(uint256 maxDepositsCount, uint24 stakingModuleId, bytes depositCalldata); event StakingModuleStatusSet(uint24 indexed stakingModuleId, StakingRouter.StakingModuleStatus status, address setBy); StakingRouter.StakingModuleStatus private status; uint256 private stakingModuleNonce; uint256 private stakingModuleLastDepositBlock; + uint256 private registeredStakingModuleId; + + constructor(uint256 stakingModuleId) { + registeredStakingModuleId = stakingModuleId; + } function deposit( uint256 maxDepositsCount, uint256 stakingModuleId, bytes calldata depositCalldata - ) external payable returns (uint256 keysCount) { + ) external whenModuleIsRegistered(stakingModuleId) payable returns (uint256 keysCount) { emit StakingModuleDeposited(maxDepositsCount, uint24(stakingModuleId), depositCalldata); return maxDepositsCount; } - function getStakingModuleStatus(uint256) external view returns (StakingRouter.StakingModuleStatus) { + function hasStakingModule(uint256 _stakingModuleId) public view returns (bool) { + return _stakingModuleId == registeredStakingModuleId; + } + + function getStakingModuleStatus(uint256 stakingModuleId) external view whenModuleIsRegistered(stakingModuleId) returns (StakingRouter.StakingModuleStatus) { return status; } - function setStakingModuleStatus(uint256 _stakingModuleId, StakingRouter.StakingModuleStatus _status) external { + function setStakingModuleStatus( + uint256 _stakingModuleId, StakingRouter.StakingModuleStatus _status + ) external whenModuleIsRegistered(_stakingModuleId) { emit StakingModuleStatusSet(uint24(_stakingModuleId), _status, msg.sender); status = _status; } - function pauseStakingModule(uint256 stakingModuleId) external { + function pauseStakingModule(uint256 stakingModuleId) external whenModuleIsRegistered(stakingModuleId) { emit StakingModuleStatusSet(uint24(stakingModuleId), StakingRouter.StakingModuleStatus.DepositsPaused, msg.sender); status = StakingRouter.StakingModuleStatus.DepositsPaused; } - function resumeStakingModule(uint256 stakingModuleId) external { + function resumeStakingModule(uint256 stakingModuleId) external whenModuleIsRegistered(stakingModuleId) { emit StakingModuleStatusSet(uint24(stakingModuleId), StakingRouter.StakingModuleStatus.Active, msg.sender); status = StakingRouter.StakingModuleStatus.Active; } - function getStakingModuleIsStopped(uint256) external view returns (bool) { + function getStakingModuleIsStopped( + uint256 stakingModuleId + ) external view whenModuleIsRegistered(stakingModuleId) returns (bool) { return status == StakingRouter.StakingModuleStatus.Stopped; } - function getStakingModuleIsDepositsPaused(uint256) external view returns (bool) { + function getStakingModuleIsDepositsPaused( + uint256 stakingModuleId + ) external view whenModuleIsRegistered(stakingModuleId) returns (bool) { return status == StakingRouter.StakingModuleStatus.DepositsPaused; } - function getStakingModuleIsActive(uint256) external view returns (bool) { + function getStakingModuleIsActive( + uint256 stakingModuleId + ) external view whenModuleIsRegistered(stakingModuleId) returns (bool) { return status == StakingRouter.StakingModuleStatus.Active; } - function getStakingModuleNonce(uint256) external view returns (uint256) { + function getStakingModuleNonce( + uint256 stakingModuleId + ) external view whenModuleIsRegistered(stakingModuleId) returns (uint256) { return stakingModuleNonce; } - function getStakingModuleLastDepositBlock(uint256) external view returns (uint256) { + function getStakingModuleLastDepositBlock( + uint256 stakingModuleId + ) external view whenModuleIsRegistered(stakingModuleId) returns (uint256) { return stakingModuleLastDepositBlock; } @@ -71,4 +94,9 @@ contract StakingRouterMockForDepositSecurityModule is IStakingRouter { function setStakingModuleLastDepositBlock(uint256 value) external { stakingModuleLastDepositBlock = value; } + + modifier whenModuleIsRegistered(uint256 _stakingModuleId) { + if (!hasStakingModule(_stakingModuleId)) revert StakingModuleUnregistered(); + _; + } } diff --git a/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol b/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol index c8e2117e9..80bc498b4 100644 --- a/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol +++ b/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol @@ -4,8 +4,20 @@ pragma solidity 0.8.9; import { ILido } from "../../oracle/AccountingOracle.sol"; +interface IPostTokenRebaseReceiver { + function handlePostTokenRebase( + uint256 _reportTimestamp, + uint256 _timeElapsed, + uint256 _preTotalShares, + uint256 _preTotalEther, + uint256 _postTotalShares, + uint256 _postTotalEther, + uint256 _sharesMintedAsFees + ) external; +} contract MockLidoForAccountingOracle is ILido { + address legacyOracle; struct HandleOracleReportLastCall { uint256 currentReportTimestamp; @@ -22,10 +34,18 @@ contract MockLidoForAccountingOracle is ILido { HandleOracleReportLastCall internal _handleOracleReportLastCall; - function getLastCall_handleOracleReport() external view returns (HandleOracleReportLastCall memory) { + function getLastCall_handleOracleReport() + external + view + returns (HandleOracleReportLastCall memory) + { return _handleOracleReportLastCall; } + function setLegacyOracle(address addr) external { + legacyOracle = addr; + } + /// /// ILido /// @@ -41,15 +61,33 @@ contract MockLidoForAccountingOracle is ILido { uint256[] calldata withdrawalFinalizationBatches, uint256 simulatedShareRate ) external { - _handleOracleReportLastCall.currentReportTimestamp = currentReportTimestamp; - _handleOracleReportLastCall.secondsElapsedSinceLastReport = secondsElapsedSinceLastReport; + _handleOracleReportLastCall + .currentReportTimestamp = currentReportTimestamp; + _handleOracleReportLastCall + .secondsElapsedSinceLastReport = secondsElapsedSinceLastReport; _handleOracleReportLastCall.numValidators = numValidators; _handleOracleReportLastCall.clBalance = clBalance; - _handleOracleReportLastCall.withdrawalVaultBalance = withdrawalVaultBalance; - _handleOracleReportLastCall.elRewardsVaultBalance = elRewardsVaultBalance; - _handleOracleReportLastCall.sharesRequestedToBurn = sharesRequestedToBurn; - _handleOracleReportLastCall.withdrawalFinalizationBatches = withdrawalFinalizationBatches; + _handleOracleReportLastCall + .withdrawalVaultBalance = withdrawalVaultBalance; + _handleOracleReportLastCall + .elRewardsVaultBalance = elRewardsVaultBalance; + _handleOracleReportLastCall + .sharesRequestedToBurn = sharesRequestedToBurn; + _handleOracleReportLastCall + .withdrawalFinalizationBatches = withdrawalFinalizationBatches; _handleOracleReportLastCall.simulatedShareRate = simulatedShareRate; ++_handleOracleReportLastCall.callCount; + + if (legacyOracle != address(0)) { + IPostTokenRebaseReceiver(legacyOracle).handlePostTokenRebase( + currentReportTimestamp /* IGNORED reportTimestamp */, + secondsElapsedSinceLastReport /* timeElapsed */, + 0 /* IGNORED preTotalShares */, + 0 /* preTotalEther */, + 1 /* postTotalShares */, + 1 /* postTotalEther */, + 1 /* IGNORED sharesMintedAsFees */ + ); + } } } diff --git a/contracts/0.8.9/test_helpers/oracle/MockStakingRouterForAccountingOracle.sol b/contracts/0.8.9/test_helpers/oracle/MockStakingRouterForAccountingOracle.sol index 0efb0d2f4..0b9475322 100644 --- a/contracts/0.8.9/test_helpers/oracle/MockStakingRouterForAccountingOracle.sol +++ b/contracts/0.8.9/test_helpers/oracle/MockStakingRouterForAccountingOracle.sol @@ -19,7 +19,8 @@ contract MockStakingRouterForAccountingOracle is IStakingRouter { bytes keysCounts; } - uint256 internal _exitedKeysCountAcrossAllModules; + mapping(uint256 => uint256) internal _exitedKeysCountsByModuleId; + UpdateExitedKeysByModuleCallData internal _lastCall_updateExitedKeysByModule; ReportKeysByNodeOperatorCallData[] public calls_reportExitedKeysByNodeOperator; @@ -28,10 +29,6 @@ contract MockStakingRouterForAccountingOracle is IStakingRouter { uint256 public totalCalls_onValidatorsCountsByNodeOperatorReportingFinished; - function setExitedKeysCountAcrossAllModules(uint256 count) external { - _exitedKeysCountAcrossAllModules = count; - } - function lastCall_updateExitedKeysByModule() external view returns (UpdateExitedKeysByModuleCallData memory) { @@ -50,17 +47,23 @@ contract MockStakingRouterForAccountingOracle is IStakingRouter { /// IStakingRouter /// - function getExitedValidatorsCountAcrossAllModules() external view returns (uint256) { - return _exitedKeysCountAcrossAllModules; - } - function updateExitedValidatorsCountByStakingModule( uint256[] calldata moduleIds, uint256[] calldata exitedKeysCounts - ) external { + ) external returns (uint256) { _lastCall_updateExitedKeysByModule.moduleIds = moduleIds; _lastCall_updateExitedKeysByModule.exitedKeysCounts = exitedKeysCounts; ++_lastCall_updateExitedKeysByModule.callCount; + + uint256 newlyExitedValidatorsCount; + + for (uint256 i = 0; i < moduleIds.length; ++i) { + uint256 moduleId = moduleIds[i]; + newlyExitedValidatorsCount += exitedKeysCounts[i] - _exitedKeysCountsByModuleId[moduleId]; + _exitedKeysCountsByModuleId[moduleId] = exitedKeysCounts[i]; + } + + return newlyExitedValidatorsCount; } function reportStakingModuleExitedValidatorsCountByNodeOperator( diff --git a/contracts/common/lib/MemUtils.sol b/contracts/common/lib/MemUtils.sol index 959e4ab37..0c90d9c8b 100644 --- a/contracts/common/lib/MemUtils.sol +++ b/contracts/common/lib/MemUtils.sol @@ -63,29 +63,4 @@ library MemUtils { function copyBytes(bytes memory _src, bytes memory _dst, uint256 _dstStart) internal pure { copyBytes(_src, _dst, 0, _dstStart, _src.length); } - - /** - * Calculates keccak256 over a uint256 memory array contents. - * - * keccakUint256Array(array) is a more gas-efficient equivalent - * to keccak256(abi.encodePacked(array)) since copying memory - * is avoided. - */ - function keccakUint256Array(uint256[] memory _arr) internal pure returns (bytes32 result) { - assembly { - let ptr := add(_arr, 32) - let len := mul(mload(_arr), 32) - result := keccak256(ptr, len) - } - } - - /** - * Decreases length of a uint256 memory array `_arr` by the `_trimBy` items. - */ - function trimUint256Array(uint256[] memory _arr, uint256 _trimBy) internal pure { - uint256 newLen = _arr.length - _trimBy; - assembly { - mstore(_arr, newLen) - } - } } diff --git a/hardhat.config.js b/hardhat.config.js index b7ec8ee9d..857fa0509 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -70,6 +70,7 @@ const getNetConfig = (networkName, ethAccountName) => { ...base, accounts: { mnemonic: 'explain tackle mirror kit van hammer degree position ginger unfair soup bonus', + count: 30, }, url: 'http://localhost:8545', chainId: 1337, @@ -82,7 +83,7 @@ const getNetConfig = (networkName, ethAccountName) => { accounts: { // default hardhat's node mnemonic mnemonic: 'test test test test test test test test test test test junk', - count: 20, + count: 30, accountsBalance: '100000000000000000000000', gasPrice: 0, }, diff --git a/lib/abi/AccountingOracle.json b/lib/abi/AccountingOracle.json index 84fcf5043..5f583046f 100644 --- a/lib/abi/AccountingOracle.json +++ b/lib/abi/AccountingOracle.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"lidoLocator","type":"address"},{"internalType":"address","name":"lido","type":"address"},{"internalType":"address","name":"legacyOracle","type":"address"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressCannotBeSame","type":"error"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"AdminCannotBeZero","type":"error"},{"inputs":[],"name":"CannotSubmitExtraDataBeforeMainData","type":"error"},{"inputs":[],"name":"ExtraDataAlreadyProcessed","type":"error"},{"inputs":[],"name":"ExtraDataHashCannotBeZeroForNonEmptyData","type":"error"},{"inputs":[],"name":"ExtraDataItemsCountCannotBeZeroForNonEmptyData","type":"error"},{"inputs":[],"name":"ExtraDataListOnlySupportsSingleTx","type":"error"},{"inputs":[{"internalType":"uint256","name":"code","type":"uint256"}],"name":"IncorrectOracleMigration","type":"error"},{"inputs":[{"internalType":"uint256","name":"initialRefSlot","type":"uint256"},{"internalType":"uint256","name":"processingRefSlot","type":"uint256"}],"name":"InitialRefSlotCannotBeLessThanProcessingOne","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[],"name":"InvalidExitedValidatorsData","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"}],"name":"InvalidExtraDataItem","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"}],"name":"InvalidExtraDataSortOrder","type":"error"},{"inputs":[],"name":"LegacyOracleCannotBeZero","type":"error"},{"inputs":[],"name":"LidoLocatorCannotBeZero","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"NumExitedValidatorsCannotDecrease","type":"error"},{"inputs":[],"name":"OnlyConsensusContractCanSubmitReport","type":"error"},{"inputs":[{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"ProcessingDeadlineMissed","type":"error"},{"inputs":[],"name":"RefSlotAlreadyProcessing","type":"error"},{"inputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"prevRefSlot","type":"uint256"}],"name":"RefSlotCannotDecrease","type":"error"},{"inputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"processingRefSlot","type":"uint256"}],"name":"RefSlotMustBeGreaterThanProcessingOne","type":"error"},{"inputs":[],"name":"SenderNotAllowed","type":"error"},{"inputs":[],"name":"UnexpectedChainConfig","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedVersion","type":"uint256"},{"internalType":"uint256","name":"receivedVersion","type":"uint256"}],"name":"UnexpectedConsensusVersion","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[{"internalType":"bytes32","name":"consensusHash","type":"bytes32"},{"internalType":"bytes32","name":"receivedHash","type":"bytes32"}],"name":"UnexpectedDataHash","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedFormat","type":"uint256"},{"internalType":"uint256","name":"receivedFormat","type":"uint256"}],"name":"UnexpectedExtraDataFormat","type":"error"},{"inputs":[{"internalType":"bytes32","name":"consensusHash","type":"bytes32"},{"internalType":"bytes32","name":"receivedHash","type":"bytes32"}],"name":"UnexpectedExtraDataHash","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedIndex","type":"uint256"},{"internalType":"uint256","name":"receivedIndex","type":"uint256"}],"name":"UnexpectedExtraDataIndex","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedCount","type":"uint256"},{"internalType":"uint256","name":"receivedCount","type":"uint256"}],"name":"UnexpectedExtraDataItemsCount","type":"error"},{"inputs":[{"internalType":"uint256","name":"consensusRefSlot","type":"uint256"},{"internalType":"uint256","name":"dataRefSlot","type":"uint256"}],"name":"UnexpectedRefSlot","type":"error"},{"inputs":[{"internalType":"uint256","name":"format","type":"uint256"}],"name":"UnsupportedExtraDataFormat","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"},{"internalType":"uint256","name":"dataType","type":"uint256"}],"name":"UnsupportedExtraDataType","type":"error"},{"inputs":[],"name":"VersionCannotBeSame","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":true,"internalType":"address","name":"prevAddr","type":"address"}],"name":"ConsensusHashContractSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"version","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"prevVersion","type":"uint256"}],"name":"ConsensusVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"itemsProcessed","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"itemsCount","type":"uint256"}],"name":"ExtraDataSubmitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"hash","type":"bytes32"}],"name":"ProcessingStarted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"hash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"}],"name":"ReportSubmitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"processedItemsCount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"itemsCount","type":"uint256"}],"name":"WarnExtraDataIncompleteProcessing","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"}],"name":"WarnProcessingMissed","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_FORMAT_EMPTY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_FORMAT_LIST","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_TYPE_EXITED_VALIDATORS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_TYPE_STUCK_VALIDATORS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LEGACY_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LOCATOR","outputs":[{"internalType":"contract ILidoLocator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_CONSENSUS_CONTRACT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_CONSENSUS_VERSION_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SECONDS_PER_SLOT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SUBMIT_DATA_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusContract","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusReport","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"},{"internalType":"bool","name":"processingStarted","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastProcessingRefSlot","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getProcessingState","outputs":[{"components":[{"internalType":"uint256","name":"currentFrameRefSlot","type":"uint256"},{"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"},{"internalType":"bytes32","name":"mainDataHash","type":"bytes32"},{"internalType":"bool","name":"mainDataSubmitted","type":"bool"},{"internalType":"bytes32","name":"extraDataHash","type":"bytes32"},{"internalType":"uint256","name":"extraDataFormat","type":"uint256"},{"internalType":"bool","name":"extraDataSubmitted","type":"bool"},{"internalType":"uint256","name":"extraDataItemsCount","type":"uint256"},{"internalType":"uint256","name":"extraDataItemsSubmitted","type":"uint256"}],"internalType":"struct AccountingOracle.ProcessingState","name":"result","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"consensusContract","type":"address"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"consensusContract","type":"address"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"},{"internalType":"uint256","name":"lastProcessingRefSlot","type":"uint256"}],"name":"initializeWithoutMigration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setConsensusContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"version","type":"uint256"}],"name":"setConsensusVersion","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"reportHash","type":"bytes32"},{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"submitConsensusReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"consensusVersion","type":"uint256"},{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"numValidators","type":"uint256"},{"internalType":"uint256","name":"clBalanceGwei","type":"uint256"},{"internalType":"uint256[]","name":"stakingModuleIdsWithNewlyExitedValidators","type":"uint256[]"},{"internalType":"uint256[]","name":"numExitedValidatorsByStakingModule","type":"uint256[]"},{"internalType":"uint256","name":"withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"sharesRequestedToBurn","type":"uint256"},{"internalType":"uint256[]","name":"withdrawalFinalizationBatches","type":"uint256[]"},{"internalType":"uint256","name":"simulatedShareRate","type":"uint256"},{"internalType":"bool","name":"isBunkerMode","type":"bool"},{"internalType":"uint256","name":"extraDataFormat","type":"uint256"},{"internalType":"bytes32","name":"extraDataHash","type":"bytes32"},{"internalType":"uint256","name":"extraDataItemsCount","type":"uint256"}],"internalType":"struct AccountingOracle.ReportData","name":"data","type":"tuple"},{"internalType":"uint256","name":"contractVersion","type":"uint256"}],"name":"submitReportData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"submitReportExtraDataEmpty","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"items","type":"bytes"}],"name":"submitReportExtraDataList","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"lidoLocator","type":"address"},{"internalType":"address","name":"lido","type":"address"},{"internalType":"address","name":"legacyOracle","type":"address"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressCannotBeSame","type":"error"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"AdminCannotBeZero","type":"error"},{"inputs":[],"name":"CannotSubmitExtraDataBeforeMainData","type":"error"},{"inputs":[],"name":"ExtraDataAlreadyProcessed","type":"error"},{"inputs":[],"name":"ExtraDataHashCannotBeZeroForNonEmptyData","type":"error"},{"inputs":[],"name":"ExtraDataItemsCountCannotBeZeroForNonEmptyData","type":"error"},{"inputs":[],"name":"ExtraDataListOnlySupportsSingleTx","type":"error"},{"inputs":[{"internalType":"uint256","name":"code","type":"uint256"}],"name":"IncorrectOracleMigration","type":"error"},{"inputs":[{"internalType":"uint256","name":"initialRefSlot","type":"uint256"},{"internalType":"uint256","name":"processingRefSlot","type":"uint256"}],"name":"InitialRefSlotCannotBeLessThanProcessingOne","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[],"name":"InvalidExitedValidatorsData","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"}],"name":"InvalidExtraDataItem","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"}],"name":"InvalidExtraDataSortOrder","type":"error"},{"inputs":[],"name":"LegacyOracleCannotBeZero","type":"error"},{"inputs":[],"name":"LidoCannotBeZero","type":"error"},{"inputs":[],"name":"LidoLocatorCannotBeZero","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"OnlyConsensusContractCanSubmitReport","type":"error"},{"inputs":[{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"ProcessingDeadlineMissed","type":"error"},{"inputs":[],"name":"RefSlotAlreadyProcessing","type":"error"},{"inputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"prevRefSlot","type":"uint256"}],"name":"RefSlotCannotDecrease","type":"error"},{"inputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"processingRefSlot","type":"uint256"}],"name":"RefSlotMustBeGreaterThanProcessingOne","type":"error"},{"inputs":[],"name":"SenderNotAllowed","type":"error"},{"inputs":[],"name":"UnexpectedChainConfig","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedVersion","type":"uint256"},{"internalType":"uint256","name":"receivedVersion","type":"uint256"}],"name":"UnexpectedConsensusVersion","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[{"internalType":"bytes32","name":"consensusHash","type":"bytes32"},{"internalType":"bytes32","name":"receivedHash","type":"bytes32"}],"name":"UnexpectedDataHash","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedFormat","type":"uint256"},{"internalType":"uint256","name":"receivedFormat","type":"uint256"}],"name":"UnexpectedExtraDataFormat","type":"error"},{"inputs":[{"internalType":"bytes32","name":"consensusHash","type":"bytes32"},{"internalType":"bytes32","name":"receivedHash","type":"bytes32"}],"name":"UnexpectedExtraDataHash","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedIndex","type":"uint256"},{"internalType":"uint256","name":"receivedIndex","type":"uint256"}],"name":"UnexpectedExtraDataIndex","type":"error"},{"inputs":[{"internalType":"uint256","name":"expectedCount","type":"uint256"},{"internalType":"uint256","name":"receivedCount","type":"uint256"}],"name":"UnexpectedExtraDataItemsCount","type":"error"},{"inputs":[{"internalType":"uint256","name":"consensusRefSlot","type":"uint256"},{"internalType":"uint256","name":"dataRefSlot","type":"uint256"}],"name":"UnexpectedRefSlot","type":"error"},{"inputs":[{"internalType":"uint256","name":"format","type":"uint256"}],"name":"UnsupportedExtraDataFormat","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"},{"internalType":"uint256","name":"dataType","type":"uint256"}],"name":"UnsupportedExtraDataType","type":"error"},{"inputs":[],"name":"VersionCannotBeSame","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":true,"internalType":"address","name":"prevAddr","type":"address"}],"name":"ConsensusHashContractSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"version","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"prevVersion","type":"uint256"}],"name":"ConsensusVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"itemsProcessed","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"itemsCount","type":"uint256"}],"name":"ExtraDataSubmitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"hash","type":"bytes32"}],"name":"ProcessingStarted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"hash","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"}],"name":"ReportSubmitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"processedItemsCount","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"itemsCount","type":"uint256"}],"name":"WarnExtraDataIncompleteProcessing","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"}],"name":"WarnProcessingMissed","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_FORMAT_EMPTY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_FORMAT_LIST","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_TYPE_EXITED_VALIDATORS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"EXTRA_DATA_TYPE_STUCK_VALIDATORS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LEGACY_ORACLE","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LIDO","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"LOCATOR","outputs":[{"internalType":"contract ILidoLocator","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_CONSENSUS_CONTRACT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_CONSENSUS_VERSION_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SECONDS_PER_SLOT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SUBMIT_DATA_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusContract","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusReport","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"},{"internalType":"bool","name":"processingStarted","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastProcessingRefSlot","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getProcessingState","outputs":[{"components":[{"internalType":"uint256","name":"currentFrameRefSlot","type":"uint256"},{"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"},{"internalType":"bytes32","name":"mainDataHash","type":"bytes32"},{"internalType":"bool","name":"mainDataSubmitted","type":"bool"},{"internalType":"bytes32","name":"extraDataHash","type":"bytes32"},{"internalType":"uint256","name":"extraDataFormat","type":"uint256"},{"internalType":"bool","name":"extraDataSubmitted","type":"bool"},{"internalType":"uint256","name":"extraDataItemsCount","type":"uint256"},{"internalType":"uint256","name":"extraDataItemsSubmitted","type":"uint256"}],"internalType":"struct AccountingOracle.ProcessingState","name":"result","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"consensusContract","type":"address"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"consensusContract","type":"address"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"},{"internalType":"uint256","name":"lastProcessingRefSlot","type":"uint256"}],"name":"initializeWithoutMigration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"setConsensusContract","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"version","type":"uint256"}],"name":"setConsensusVersion","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"reportHash","type":"bytes32"},{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"name":"submitConsensusReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"consensusVersion","type":"uint256"},{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"numValidators","type":"uint256"},{"internalType":"uint256","name":"clBalanceGwei","type":"uint256"},{"internalType":"uint256[]","name":"stakingModuleIdsWithNewlyExitedValidators","type":"uint256[]"},{"internalType":"uint256[]","name":"numExitedValidatorsByStakingModule","type":"uint256[]"},{"internalType":"uint256","name":"withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"sharesRequestedToBurn","type":"uint256"},{"internalType":"uint256[]","name":"withdrawalFinalizationBatches","type":"uint256[]"},{"internalType":"uint256","name":"simulatedShareRate","type":"uint256"},{"internalType":"bool","name":"isBunkerMode","type":"bool"},{"internalType":"uint256","name":"extraDataFormat","type":"uint256"},{"internalType":"bytes32","name":"extraDataHash","type":"bytes32"},{"internalType":"uint256","name":"extraDataItemsCount","type":"uint256"}],"internalType":"struct AccountingOracle.ReportData","name":"data","type":"tuple"},{"internalType":"uint256","name":"contractVersion","type":"uint256"}],"name":"submitReportData","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"submitReportExtraDataEmpty","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"items","type":"bytes"}],"name":"submitReportExtraDataList","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/IStakingRouter.json b/lib/abi/IStakingRouter.json index 4cdff3a03..cc4b059f3 100644 --- a/lib/abi/IStakingRouter.json +++ b/lib/abi/IStakingRouter.json @@ -1 +1 @@ -[{"inputs":[],"name":"getExitedValidatorsCountAcrossAllModules","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"onValidatorsCountsByNodeOperatorReportingFinished","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"exitedValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleExitedValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"stuckValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleStuckValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"moduleIds","type":"uint256[]"},{"internalType":"uint256[]","name":"exitedValidatorsCounts","type":"uint256[]"}],"name":"updateExitedValidatorsCountByStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[],"name":"onValidatorsCountsByNodeOperatorReportingFinished","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"exitedValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleExitedValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"stuckValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleStuckValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"moduleIds","type":"uint256[]"},{"internalType":"uint256[]","name":"exitedValidatorsCounts","type":"uint256[]"}],"name":"updateExitedValidatorsCountByStakingModule","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/lib/abi/OracleReportSanityChecker.json b/lib/abi/OracleReportSanityChecker.json index e92dbaba8..c58226d77 100644 --- a/lib/abi/OracleReportSanityChecker.json +++ b/lib/abi/OracleReportSanityChecker.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_lidoLocator","type":"address"},{"internalType":"address","name":"_admin","type":"address"},{"components":[{"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"},{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"},{"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"},{"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"},{"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"},{"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"},{"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"internalType":"struct LimitsList","name":"_limitsList","type":"tuple"},{"components":[{"internalType":"address[]","name":"allLimitsManagers","type":"address[]"},{"internalType":"address[]","name":"churnValidatorsPerDayLimitManagers","type":"address[]"},{"internalType":"address[]","name":"oneOffCLBalanceDecreaseLimitManagers","type":"address[]"},{"internalType":"address[]","name":"annualBalanceIncreaseLimitManagers","type":"address[]"},{"internalType":"address[]","name":"shareRateDeviationLimitManagers","type":"address[]"},{"internalType":"address[]","name":"maxValidatorExitRequestsPerReportManagers","type":"address[]"},{"internalType":"address[]","name":"maxAccountingExtraDataListItemsCountManagers","type":"address[]"},{"internalType":"address[]","name":"maxNodeOperatorsPerExtraDataItemCountManagers","type":"address[]"},{"internalType":"address[]","name":"requestTimestampMarginManagers","type":"address[]"},{"internalType":"address[]","name":"maxPositiveTokenRebaseManagers","type":"address[]"}],"internalType":"struct OracleReportSanityChecker.ManagersRoster","name":"_managersRoster","type":"tuple"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ActualShareRateIsZero","type":"error"},{"inputs":[{"internalType":"uint256","name":"limitPerDay","type":"uint256"},{"internalType":"uint256","name":"exitedPerDay","type":"uint256"}],"name":"ExitedValidatorsLimitExceeded","type":"error"},{"inputs":[{"internalType":"uint256","name":"churnLimit","type":"uint256"}],"name":"IncorrectAppearedValidators","type":"error"},{"inputs":[{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBP","type":"uint256"}],"name":"IncorrectCLBalanceDecrease","type":"error"},{"inputs":[{"internalType":"uint256","name":"annualBalanceDiff","type":"uint256"}],"name":"IncorrectCLBalanceIncrease","type":"error"},{"inputs":[{"internalType":"uint256","name":"actualELRewardsVaultBalance","type":"uint256"}],"name":"IncorrectELRewardsVaultBalance","type":"error"},{"inputs":[{"internalType":"uint256","name":"churnLimit","type":"uint256"}],"name":"IncorrectExitedValidators","type":"error"},{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"maxAllowedValue","type":"uint256"}],"name":"IncorrectLimitValue","type":"error"},{"inputs":[{"internalType":"uint256","name":"maxRequestsCount","type":"uint256"}],"name":"IncorrectNumberOfExitRequestsPerReport","type":"error"},{"inputs":[{"internalType":"uint256","name":"requestCreationBlock","type":"uint256"}],"name":"IncorrectRequestFinalization","type":"error"},{"inputs":[{"internalType":"uint256","name":"actualSharesToBurn","type":"uint256"}],"name":"IncorrectSharesRequestedToBurn","type":"error"},{"inputs":[{"internalType":"uint256","name":"actualWithdrawalVaultBalance","type":"uint256"}],"name":"IncorrectWithdrawalsVaultBalance","type":"error"},{"inputs":[{"internalType":"uint256","name":"maxItemsCount","type":"uint256"},{"internalType":"uint256","name":"receivedItemsCount","type":"uint256"}],"name":"MaxAccountingExtraDataItemsCountExceeded","type":"error"},{"inputs":[{"internalType":"uint256","name":"simulatedShareRate","type":"uint256"},{"internalType":"uint256","name":"actualShareRate","type":"uint256"}],"name":"TooHighSimulatedShareRate","type":"error"},{"inputs":[],"name":"TooHighTokenRebaseLimit","type":"error"},{"inputs":[{"internalType":"uint256","name":"simulatedShareRate","type":"uint256"},{"internalType":"uint256","name":"actualShareRate","type":"uint256"}],"name":"TooLowSimulatedShareRate","type":"error"},{"inputs":[],"name":"TooLowTokenRebaseLimit","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"},{"internalType":"uint256","name":"nodeOpsCount","type":"uint256"}],"name":"TooManyNodeOpsPerExtraDataItem","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"}],"name":"AnnualBalanceIncreaseBPLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"}],"name":"ChurnValidatorsPerDayLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"}],"name":"MaxAccountingExtraDataListItemsCountSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"}],"name":"MaxNodeOperatorsPerExtraDataItemCountSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"name":"MaxPositiveTokenRebaseSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"}],"name":"MaxValidatorExitRequestsPerReportSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"}],"name":"OneOffCLBalanceDecreaseBPLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"}],"name":"RequestTimestampMarginSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"}],"name":"SimulatedShareRateDeviationBPLimitSet","type":"event"},{"inputs":[],"name":"ALL_LIMITS_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"CHURN_VALIDATORS_PER_DAY_LIMIT_MANGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_ACCOUNTING_EXTRA_DATA_LIST_ITEMS_COUNT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_COUNT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_extraDataListItemsCount","type":"uint256"}],"name":"checkAccountingExtraDataListItemsCount","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_timeElapsed","type":"uint256"},{"internalType":"uint256","name":"_preCLBalance","type":"uint256"},{"internalType":"uint256","name":"_postCLBalance","type":"uint256"},{"internalType":"uint256","name":"_withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_sharesRequestedToBurn","type":"uint256"},{"internalType":"uint256","name":"_preCLValidators","type":"uint256"},{"internalType":"uint256","name":"_postCLValidators","type":"uint256"}],"name":"checkAccountingOracleReport","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_exitRequestsCount","type":"uint256"}],"name":"checkExitBusOracleReport","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_exitedValidatorsCount","type":"uint256"}],"name":"checkExitedValidatorsRatePerDay","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_itemIndex","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorsCount","type":"uint256"}],"name":"checkNodeOperatorsPerExtraDataItemCount","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_postTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_postTotalShares","type":"uint256"},{"internalType":"uint256","name":"_etherLockedOnWithdrawalQueue","type":"uint256"},{"internalType":"uint256","name":"_sharesBurntDueToWithdrawals","type":"uint256"},{"internalType":"uint256","name":"_simulatedShareRate","type":"uint256"}],"name":"checkSimulatedShareRate","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_lastFinalizableRequestId","type":"uint256"},{"internalType":"uint256","name":"_reportTimestamp","type":"uint256"}],"name":"checkWithdrawalQueueOracleReport","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLidoLocator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaxPositiveTokenRebase","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOracleReportLimits","outputs":[{"components":[{"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"},{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"},{"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"},{"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"},{"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"},{"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"},{"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"internalType":"struct LimitsList","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_annualBalanceIncreaseBPLimit","type":"uint256"}],"name":"setAnnualBalanceIncreaseBPLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_churnValidatorsPerDayLimit","type":"uint256"}],"name":"setChurnValidatorsPerDayLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxAccountingExtraDataListItemsCount","type":"uint256"}],"name":"setMaxAccountingExtraDataListItemsCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxValidatorExitRequestsPerReport","type":"uint256"}],"name":"setMaxExitRequestsPerOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxNodeOperatorsPerExtraDataItemCount","type":"uint256"}],"name":"setMaxNodeOperatorsPerExtraDataItemCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxPositiveTokenRebase","type":"uint256"}],"name":"setMaxPositiveTokenRebase","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_oneOffCLBalanceDecreaseBPLimit","type":"uint256"}],"name":"setOneOffCLBalanceDecreaseBPLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"},{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"},{"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"},{"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"},{"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"},{"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"},{"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"internalType":"struct LimitsList","name":"_limitsList","type":"tuple"}],"name":"setOracleReportLimits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestTimestampMargin","type":"uint256"}],"name":"setRequestTimestampMargin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_simulatedShareRateDeviationBPLimit","type":"uint256"}],"name":"setSimulatedShareRateDeviationBPLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_preTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_preTotalShares","type":"uint256"},{"internalType":"uint256","name":"_preCLBalance","type":"uint256"},{"internalType":"uint256","name":"_postCLBalance","type":"uint256"},{"internalType":"uint256","name":"_withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_sharesRequestedToBurn","type":"uint256"},{"internalType":"uint256","name":"_etherToLockForWithdrawals","type":"uint256"},{"internalType":"uint256","name":"_newSharesToBurnForWithdrawals","type":"uint256"}],"name":"smoothenTokenRebase","outputs":[{"internalType":"uint256","name":"withdrawals","type":"uint256"},{"internalType":"uint256","name":"elRewards","type":"uint256"},{"internalType":"uint256","name":"simulatedSharesToBurn","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_lidoLocator","type":"address"},{"internalType":"address","name":"_admin","type":"address"},{"components":[{"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"},{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"},{"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"},{"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"},{"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"},{"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"},{"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"internalType":"struct LimitsList","name":"_limitsList","type":"tuple"},{"components":[{"internalType":"address[]","name":"allLimitsManagers","type":"address[]"},{"internalType":"address[]","name":"churnValidatorsPerDayLimitManagers","type":"address[]"},{"internalType":"address[]","name":"oneOffCLBalanceDecreaseLimitManagers","type":"address[]"},{"internalType":"address[]","name":"annualBalanceIncreaseLimitManagers","type":"address[]"},{"internalType":"address[]","name":"shareRateDeviationLimitManagers","type":"address[]"},{"internalType":"address[]","name":"maxValidatorExitRequestsPerReportManagers","type":"address[]"},{"internalType":"address[]","name":"maxAccountingExtraDataListItemsCountManagers","type":"address[]"},{"internalType":"address[]","name":"maxNodeOperatorsPerExtraDataItemCountManagers","type":"address[]"},{"internalType":"address[]","name":"requestTimestampMarginManagers","type":"address[]"},{"internalType":"address[]","name":"maxPositiveTokenRebaseManagers","type":"address[]"}],"internalType":"struct OracleReportSanityChecker.ManagersRoster","name":"_managersRoster","type":"tuple"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ActualShareRateIsZero","type":"error"},{"inputs":[{"internalType":"uint256","name":"limitPerDay","type":"uint256"},{"internalType":"uint256","name":"exitedPerDay","type":"uint256"}],"name":"ExitedValidatorsLimitExceeded","type":"error"},{"inputs":[{"internalType":"uint256","name":"churnLimit","type":"uint256"}],"name":"IncorrectAppearedValidators","type":"error"},{"inputs":[{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBP","type":"uint256"}],"name":"IncorrectCLBalanceDecrease","type":"error"},{"inputs":[{"internalType":"uint256","name":"annualBalanceDiff","type":"uint256"}],"name":"IncorrectCLBalanceIncrease","type":"error"},{"inputs":[{"internalType":"uint256","name":"actualELRewardsVaultBalance","type":"uint256"}],"name":"IncorrectELRewardsVaultBalance","type":"error"},{"inputs":[{"internalType":"uint256","name":"churnLimit","type":"uint256"}],"name":"IncorrectExitedValidators","type":"error"},{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"maxAllowedValue","type":"uint256"}],"name":"IncorrectLimitValue","type":"error"},{"inputs":[{"internalType":"uint256","name":"maxRequestsCount","type":"uint256"}],"name":"IncorrectNumberOfExitRequestsPerReport","type":"error"},{"inputs":[{"internalType":"uint256","name":"requestCreationBlock","type":"uint256"}],"name":"IncorrectRequestFinalization","type":"error"},{"inputs":[{"internalType":"uint256","name":"actualSharesToBurn","type":"uint256"}],"name":"IncorrectSharesRequestedToBurn","type":"error"},{"inputs":[{"internalType":"uint256","name":"actualWithdrawalVaultBalance","type":"uint256"}],"name":"IncorrectWithdrawalsVaultBalance","type":"error"},{"inputs":[{"internalType":"uint256","name":"maxItemsCount","type":"uint256"},{"internalType":"uint256","name":"receivedItemsCount","type":"uint256"}],"name":"MaxAccountingExtraDataItemsCountExceeded","type":"error"},{"inputs":[],"name":"NegativeTotalPooledEther","type":"error"},{"inputs":[{"internalType":"uint256","name":"simulatedShareRate","type":"uint256"},{"internalType":"uint256","name":"actualShareRate","type":"uint256"}],"name":"TooHighSimulatedShareRate","type":"error"},{"inputs":[],"name":"TooHighTokenRebaseLimit","type":"error"},{"inputs":[{"internalType":"uint256","name":"simulatedShareRate","type":"uint256"},{"internalType":"uint256","name":"actualShareRate","type":"uint256"}],"name":"TooLowSimulatedShareRate","type":"error"},{"inputs":[],"name":"TooLowTokenRebaseLimit","type":"error"},{"inputs":[{"internalType":"uint256","name":"itemIndex","type":"uint256"},{"internalType":"uint256","name":"nodeOpsCount","type":"uint256"}],"name":"TooManyNodeOpsPerExtraDataItem","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"}],"name":"AnnualBalanceIncreaseBPLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"}],"name":"ChurnValidatorsPerDayLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"}],"name":"MaxAccountingExtraDataListItemsCountSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"}],"name":"MaxNodeOperatorsPerExtraDataItemCountSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"name":"MaxPositiveTokenRebaseSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"}],"name":"MaxValidatorExitRequestsPerReportSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"}],"name":"OneOffCLBalanceDecreaseBPLimitSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"}],"name":"RequestTimestampMarginSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"}],"name":"SimulatedShareRateDeviationBPLimitSet","type":"event"},{"inputs":[],"name":"ALL_LIMITS_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"CHURN_VALIDATORS_PER_DAY_LIMIT_MANGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_ACCOUNTING_EXTRA_DATA_LIST_ITEMS_COUNT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_COUNT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_extraDataListItemsCount","type":"uint256"}],"name":"checkAccountingExtraDataListItemsCount","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_timeElapsed","type":"uint256"},{"internalType":"uint256","name":"_preCLBalance","type":"uint256"},{"internalType":"uint256","name":"_postCLBalance","type":"uint256"},{"internalType":"uint256","name":"_withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_sharesRequestedToBurn","type":"uint256"},{"internalType":"uint256","name":"_preCLValidators","type":"uint256"},{"internalType":"uint256","name":"_postCLValidators","type":"uint256"}],"name":"checkAccountingOracleReport","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_exitRequestsCount","type":"uint256"}],"name":"checkExitBusOracleReport","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_exitedValidatorsCount","type":"uint256"}],"name":"checkExitedValidatorsRatePerDay","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_itemIndex","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorsCount","type":"uint256"}],"name":"checkNodeOperatorsPerExtraDataItemCount","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_postTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_postTotalShares","type":"uint256"},{"internalType":"uint256","name":"_etherLockedOnWithdrawalQueue","type":"uint256"},{"internalType":"uint256","name":"_sharesBurntDueToWithdrawals","type":"uint256"},{"internalType":"uint256","name":"_simulatedShareRate","type":"uint256"}],"name":"checkSimulatedShareRate","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_lastFinalizableRequestId","type":"uint256"},{"internalType":"uint256","name":"_reportTimestamp","type":"uint256"}],"name":"checkWithdrawalQueueOracleReport","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLidoLocator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaxPositiveTokenRebase","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOracleReportLimits","outputs":[{"components":[{"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"},{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"},{"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"},{"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"},{"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"},{"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"},{"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"internalType":"struct LimitsList","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_annualBalanceIncreaseBPLimit","type":"uint256"}],"name":"setAnnualBalanceIncreaseBPLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_churnValidatorsPerDayLimit","type":"uint256"}],"name":"setChurnValidatorsPerDayLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxAccountingExtraDataListItemsCount","type":"uint256"}],"name":"setMaxAccountingExtraDataListItemsCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxValidatorExitRequestsPerReport","type":"uint256"}],"name":"setMaxExitRequestsPerOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxNodeOperatorsPerExtraDataItemCount","type":"uint256"}],"name":"setMaxNodeOperatorsPerExtraDataItemCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxPositiveTokenRebase","type":"uint256"}],"name":"setMaxPositiveTokenRebase","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_oneOffCLBalanceDecreaseBPLimit","type":"uint256"}],"name":"setOneOffCLBalanceDecreaseBPLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"churnValidatorsPerDayLimit","type":"uint256"},{"internalType":"uint256","name":"oneOffCLBalanceDecreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"annualBalanceIncreaseBPLimit","type":"uint256"},{"internalType":"uint256","name":"simulatedShareRateDeviationBPLimit","type":"uint256"},{"internalType":"uint256","name":"maxValidatorExitRequestsPerReport","type":"uint256"},{"internalType":"uint256","name":"maxAccountingExtraDataListItemsCount","type":"uint256"},{"internalType":"uint256","name":"maxNodeOperatorsPerExtraDataItemCount","type":"uint256"},{"internalType":"uint256","name":"requestTimestampMargin","type":"uint256"},{"internalType":"uint256","name":"maxPositiveTokenRebase","type":"uint256"}],"internalType":"struct LimitsList","name":"_limitsList","type":"tuple"}],"name":"setOracleReportLimits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestTimestampMargin","type":"uint256"}],"name":"setRequestTimestampMargin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_simulatedShareRateDeviationBPLimit","type":"uint256"}],"name":"setSimulatedShareRateDeviationBPLimit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_preTotalPooledEther","type":"uint256"},{"internalType":"uint256","name":"_preTotalShares","type":"uint256"},{"internalType":"uint256","name":"_preCLBalance","type":"uint256"},{"internalType":"uint256","name":"_postCLBalance","type":"uint256"},{"internalType":"uint256","name":"_withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_sharesRequestedToBurn","type":"uint256"},{"internalType":"uint256","name":"_etherToLockForWithdrawals","type":"uint256"},{"internalType":"uint256","name":"_newSharesToBurnForWithdrawals","type":"uint256"}],"name":"smoothenTokenRebase","outputs":[{"internalType":"uint256","name":"withdrawals","type":"uint256"},{"internalType":"uint256","name":"elRewards","type":"uint256"},{"internalType":"uint256","name":"simulatedSharesToBurn","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/PositiveTokenRebaseLimiter.json b/lib/abi/PositiveTokenRebaseLimiter.json index 63f6b33b3..f3c22b8f6 100644 --- a/lib/abi/PositiveTokenRebaseLimiter.json +++ b/lib/abi/PositiveTokenRebaseLimiter.json @@ -1 +1 @@ -[{"inputs":[],"name":"TooHighTokenRebaseLimit","type":"error"},{"inputs":[],"name":"TooLowTokenRebaseLimit","type":"error"},{"inputs":[],"name":"LIMITER_PRECISION_BASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"UNLIMITED_REBASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[],"name":"NegativeTotalPooledEther","type":"error"},{"inputs":[],"name":"TooHighTokenRebaseLimit","type":"error"},{"inputs":[],"name":"TooLowTokenRebaseLimit","type":"error"},{"inputs":[],"name":"LIMITER_PRECISION_BASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"UNLIMITED_REBASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/StakingRouter.json b/lib/abi/StakingRouter.json index 4d4b74251..88b6ba986 100644 --- a/lib/abi/StakingRouter.json +++ b/lib/abi/StakingRouter.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_depositContract","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AppAuthLidoFailed","type":"error"},{"inputs":[{"internalType":"uint256","name":"firstArrayLength","type":"uint256"},{"internalType":"uint256","name":"secondArrayLength","type":"uint256"}],"name":"ArraysLengthMismatch","type":"error"},{"inputs":[],"name":"DepositContractZeroAddress","type":"error"},{"inputs":[],"name":"DirectETHTransfer","type":"error"},{"inputs":[],"name":"EmptyWithdrawalsCredentials","type":"error"},{"inputs":[],"name":"ExitedValidatorsCountCannotDecrease","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[{"internalType":"uint256","name":"etherValue","type":"uint256"},{"internalType":"uint256","name":"depositsCount","type":"uint256"}],"name":"InvalidDepositsValue","type":"error"},{"inputs":[{"internalType":"uint256","name":"actual","type":"uint256"},{"internalType":"uint256","name":"expected","type":"uint256"}],"name":"InvalidPublicKeysBatchLength","type":"error"},{"inputs":[{"internalType":"uint256","name":"code","type":"uint256"}],"name":"InvalidReportData","type":"error"},{"inputs":[{"internalType":"uint256","name":"actual","type":"uint256"},{"internalType":"uint256","name":"expected","type":"uint256"}],"name":"InvalidSignaturesBatchLength","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"StakingModuleAddressExists","type":"error"},{"inputs":[],"name":"StakingModuleNotActive","type":"error"},{"inputs":[],"name":"StakingModuleNotPaused","type":"error"},{"inputs":[],"name":"StakingModuleStatusTheSame","type":"error"},{"inputs":[],"name":"StakingModuleUnregistered","type":"error"},{"inputs":[],"name":"StakingModuleWrongName","type":"error"},{"inputs":[],"name":"StakingModulesLimitExceeded","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[{"internalType":"uint256","name":"currentModuleExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOpExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOpStuckValidatorsCount","type":"uint256"}],"name":"UnexpectedCurrentValidatorsCount","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ValueOver100Percent","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ZeroAddress","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"lowLevelRevertData","type":"bytes"}],"name":"ExitedAndStuckValidatorsCountsUpdateFailed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"lowLevelRevertData","type":"bytes"}],"name":"RewardsMintedReportFailed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"address","name":"stakingModule","type":"address"},{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"address","name":"createdBy","type":"address"}],"name":"StakingModuleAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"unreportedExitedValidatorsCount","type":"uint256"}],"name":"StakingModuleExitedValidatorsIncompleteReporting","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"stakingModuleFee","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"treasuryFee","type":"uint256"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"StakingModuleFeesSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"enum StakingRouter.StakingModuleStatus","name":"status","type":"uint8"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"StakingModuleStatusSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"targetShare","type":"uint256"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"StakingModuleTargetShareSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"StakingRouterETHDeposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"withdrawalCredentials","type":"bytes32"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"WithdrawalCredentialsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"lowLevelRevertData","type":"bytes"}],"name":"WithdrawalsCredentialsChangeFailed","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEPOSIT_CONTRACT","outputs":[{"internalType":"contract IDepositContract","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FEE_PRECISION_POINTS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_WITHDRAWAL_CREDENTIALS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STAKING_MODULES_COUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STAKING_MODULE_NAME_LENGTH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REPORT_EXITED_VALIDATORS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REPORT_REWARDS_MINTED_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_MODULE_MANAGE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_MODULE_PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_MODULE_RESUME_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOTAL_BASIS_POINTS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"UNSAFE_SET_EXITED_VALIDATORS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"address","name":"_stakingModuleAddress","type":"address"},{"internalType":"uint256","name":"_targetShare","type":"uint256"},{"internalType":"uint256","name":"_stakingModuleFee","type":"uint256"},{"internalType":"uint256","name":"_treasuryFee","type":"uint256"}],"name":"addStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_depositsCount","type":"uint256"},{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"_depositCalldata","type":"bytes"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getAllNodeOperatorDigests","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.NodeOperatorDigest[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAllStakingModuleDigests","outputs":[{"components":[{"internalType":"uint256","name":"nodeOperatorsCount","type":"uint256"},{"internalType":"uint256","name":"activeNodeOperatorsCount","type":"uint256"},{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule","name":"state","type":"tuple"},{"components":[{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModuleSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.StakingModuleDigest[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_depositsCount","type":"uint256"}],"name":"getDepositsAllocation","outputs":[{"internalType":"uint256","name":"allocated","type":"uint256"},{"internalType":"uint256[]","name":"allocations","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExitedValidatorsCountAcrossAllModules","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLido","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256[]","name":"_nodeOperatorIds","type":"uint256[]"}],"name":"getNodeOperatorDigests","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.NodeOperatorDigest[]","name":"digests","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_offset","type":"uint256"},{"internalType":"uint256","name":"_limit","type":"uint256"}],"name":"getNodeOperatorDigests","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.NodeOperatorDigest[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"}],"name":"getNodeOperatorSummary","outputs":[{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingFeeAggregateDistribution","outputs":[{"internalType":"uint96","name":"modulesFee","type":"uint96"},{"internalType":"uint96","name":"treasuryFee","type":"uint96"},{"internalType":"uint256","name":"basePrecision","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingFeeAggregateDistributionE4Precision","outputs":[{"internalType":"uint16","name":"modulesFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModule","outputs":[{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleActiveValidatorsCount","outputs":[{"internalType":"uint256","name":"activeValidatorsCount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakingModuleIds","type":"uint256[]"}],"name":"getStakingModuleDigests","outputs":[{"components":[{"internalType":"uint256","name":"nodeOperatorsCount","type":"uint256"},{"internalType":"uint256","name":"activeNodeOperatorsCount","type":"uint256"},{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule","name":"state","type":"tuple"},{"components":[{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModuleSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.StakingModuleDigest[]","name":"digests","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingModuleIds","outputs":[{"internalType":"uint256[]","name":"stakingModuleIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleIsActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleIsDepositsPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleIsStopped","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleLastDepositBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_maxDepositsValue","type":"uint256"}],"name":"getStakingModuleMaxDepositsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleStatus","outputs":[{"internalType":"enum StakingRouter.StakingModuleStatus","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleSummary","outputs":[{"components":[{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModuleSummary","name":"summary","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingModules","outputs":[{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule[]","name":"res","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingModulesCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingRewardsDistribution","outputs":[{"internalType":"address[]","name":"recipients","type":"address[]"},{"internalType":"uint256[]","name":"stakingModuleIds","type":"uint256[]"},{"internalType":"uint96[]","name":"stakingModuleFees","type":"uint96[]"},{"internalType":"uint96","name":"totalFee","type":"uint96"},{"internalType":"uint256","name":"precisionPoints","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalFeeE4Precision","outputs":[{"internalType":"uint16","name":"totalFee","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getWithdrawalCredentials","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address","name":"_lido","type":"address"},{"internalType":"bytes32","name":"_withdrawalCredentials","type":"bytes32"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"onValidatorsCountsByNodeOperatorReportingFinished","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"pauseStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakingModuleIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_totalShares","type":"uint256[]"}],"name":"reportRewardsMinted","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"_nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"_exitedValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleExitedValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"_nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"_stuckValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleStuckValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"resumeStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"enum StakingRouter.StakingModuleStatus","name":"_status","type":"uint8"}],"name":"setStakingModuleStatus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_withdrawalCredentials","type":"bytes32"}],"name":"setWithdrawalCredentials","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"},{"internalType":"bool","name":"_triggerUpdateFinish","type":"bool"},{"components":[{"internalType":"uint256","name":"currentModuleExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOperatorExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOperatorStuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"newModuleExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"newNodeOperatorExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"newNodeOperatorStuckValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.ValidatorsCountsCorrection","name":"_correction","type":"tuple"}],"name":"unsafeSetExitedValidatorsCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakingModuleIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_exitedValidatorsCounts","type":"uint256[]"}],"name":"updateExitedValidatorsCountByStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"},{"internalType":"uint256","name":"_refundedValidatorsCount","type":"uint256"}],"name":"updateRefundedValidatorsCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_targetShare","type":"uint256"},{"internalType":"uint256","name":"_stakingModuleFee","type":"uint256"},{"internalType":"uint256","name":"_treasuryFee","type":"uint256"}],"name":"updateStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"},{"internalType":"bool","name":"_isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"_targetLimit","type":"uint256"}],"name":"updateTargetValidatorsLimits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_depositContract","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AppAuthLidoFailed","type":"error"},{"inputs":[{"internalType":"uint256","name":"firstArrayLength","type":"uint256"},{"internalType":"uint256","name":"secondArrayLength","type":"uint256"}],"name":"ArraysLengthMismatch","type":"error"},{"inputs":[],"name":"DepositContractZeroAddress","type":"error"},{"inputs":[],"name":"DirectETHTransfer","type":"error"},{"inputs":[],"name":"EmptyWithdrawalsCredentials","type":"error"},{"inputs":[],"name":"ExitedValidatorsCountCannotDecrease","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[{"internalType":"uint256","name":"etherValue","type":"uint256"},{"internalType":"uint256","name":"depositsCount","type":"uint256"}],"name":"InvalidDepositsValue","type":"error"},{"inputs":[{"internalType":"uint256","name":"actual","type":"uint256"},{"internalType":"uint256","name":"expected","type":"uint256"}],"name":"InvalidPublicKeysBatchLength","type":"error"},{"inputs":[{"internalType":"uint256","name":"code","type":"uint256"}],"name":"InvalidReportData","type":"error"},{"inputs":[{"internalType":"uint256","name":"actual","type":"uint256"},{"internalType":"uint256","name":"expected","type":"uint256"}],"name":"InvalidSignaturesBatchLength","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"StakingModuleAddressExists","type":"error"},{"inputs":[],"name":"StakingModuleNotActive","type":"error"},{"inputs":[],"name":"StakingModuleNotPaused","type":"error"},{"inputs":[],"name":"StakingModuleStatusTheSame","type":"error"},{"inputs":[],"name":"StakingModuleUnregistered","type":"error"},{"inputs":[],"name":"StakingModuleWrongName","type":"error"},{"inputs":[],"name":"StakingModulesLimitExceeded","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[{"internalType":"uint256","name":"currentModuleExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOpExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOpStuckValidatorsCount","type":"uint256"}],"name":"UnexpectedCurrentValidatorsCount","type":"error"},{"inputs":[],"name":"UnrecoverableModuleError","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ValueOver100Percent","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ZeroAddress","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"lowLevelRevertData","type":"bytes"}],"name":"ExitedAndStuckValidatorsCountsUpdateFailed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"lowLevelRevertData","type":"bytes"}],"name":"RewardsMintedReportFailed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"address","name":"stakingModule","type":"address"},{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"address","name":"createdBy","type":"address"}],"name":"StakingModuleAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"unreportedExitedValidatorsCount","type":"uint256"}],"name":"StakingModuleExitedValidatorsIncompleteReporting","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"stakingModuleFee","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"treasuryFee","type":"uint256"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"StakingModuleFeesSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"enum StakingRouter.StakingModuleStatus","name":"status","type":"uint8"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"StakingModuleStatusSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"targetShare","type":"uint256"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"StakingModuleTargetShareSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"StakingRouterETHDeposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"withdrawalCredentials","type":"bytes32"},{"indexed":false,"internalType":"address","name":"setBy","type":"address"}],"name":"WithdrawalCredentialsSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"lowLevelRevertData","type":"bytes"}],"name":"WithdrawalsCredentialsChangeFailed","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEPOSIT_CONTRACT","outputs":[{"internalType":"contract IDepositContract","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FEE_PRECISION_POINTS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_WITHDRAWAL_CREDENTIALS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STAKING_MODULES_COUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STAKING_MODULE_NAME_LENGTH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REPORT_EXITED_VALIDATORS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REPORT_REWARDS_MINTED_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_MODULE_MANAGE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_MODULE_PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_MODULE_RESUME_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TOTAL_BASIS_POINTS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"UNSAFE_SET_EXITED_VALIDATORS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"address","name":"_stakingModuleAddress","type":"address"},{"internalType":"uint256","name":"_targetShare","type":"uint256"},{"internalType":"uint256","name":"_stakingModuleFee","type":"uint256"},{"internalType":"uint256","name":"_treasuryFee","type":"uint256"}],"name":"addStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_depositsCount","type":"uint256"},{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"_depositCalldata","type":"bytes"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getAllNodeOperatorDigests","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.NodeOperatorDigest[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAllStakingModuleDigests","outputs":[{"components":[{"internalType":"uint256","name":"nodeOperatorsCount","type":"uint256"},{"internalType":"uint256","name":"activeNodeOperatorsCount","type":"uint256"},{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule","name":"state","type":"tuple"},{"components":[{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModuleSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.StakingModuleDigest[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_depositsCount","type":"uint256"}],"name":"getDepositsAllocation","outputs":[{"internalType":"uint256","name":"allocated","type":"uint256"},{"internalType":"uint256[]","name":"allocations","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLido","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256[]","name":"_nodeOperatorIds","type":"uint256[]"}],"name":"getNodeOperatorDigests","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.NodeOperatorDigest[]","name":"digests","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_offset","type":"uint256"},{"internalType":"uint256","name":"_limit","type":"uint256"}],"name":"getNodeOperatorDigests","outputs":[{"components":[{"internalType":"uint256","name":"id","type":"uint256"},{"internalType":"bool","name":"isActive","type":"bool"},{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.NodeOperatorDigest[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"}],"name":"getNodeOperatorSummary","outputs":[{"components":[{"internalType":"bool","name":"isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"targetValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"refundedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"stuckPenaltyEndTimestamp","type":"uint256"},{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.NodeOperatorSummary","name":"summary","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"getRoleMember","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleMemberCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingFeeAggregateDistribution","outputs":[{"internalType":"uint96","name":"modulesFee","type":"uint96"},{"internalType":"uint96","name":"treasuryFee","type":"uint96"},{"internalType":"uint256","name":"basePrecision","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingFeeAggregateDistributionE4Precision","outputs":[{"internalType":"uint16","name":"modulesFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModule","outputs":[{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleActiveValidatorsCount","outputs":[{"internalType":"uint256","name":"activeValidatorsCount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakingModuleIds","type":"uint256[]"}],"name":"getStakingModuleDigests","outputs":[{"components":[{"internalType":"uint256","name":"nodeOperatorsCount","type":"uint256"},{"internalType":"uint256","name":"activeNodeOperatorsCount","type":"uint256"},{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule","name":"state","type":"tuple"},{"components":[{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModuleSummary","name":"summary","type":"tuple"}],"internalType":"struct StakingRouter.StakingModuleDigest[]","name":"digests","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingModuleIds","outputs":[{"internalType":"uint256[]","name":"stakingModuleIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleIsActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleIsDepositsPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleIsStopped","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleLastDepositBlock","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_maxDepositsValue","type":"uint256"}],"name":"getStakingModuleMaxDepositsCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleStatus","outputs":[{"internalType":"enum StakingRouter.StakingModuleStatus","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"getStakingModuleSummary","outputs":[{"components":[{"internalType":"uint256","name":"totalExitedValidators","type":"uint256"},{"internalType":"uint256","name":"totalDepositedValidators","type":"uint256"},{"internalType":"uint256","name":"depositableValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModuleSummary","name":"summary","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingModules","outputs":[{"components":[{"internalType":"uint24","name":"id","type":"uint24"},{"internalType":"address","name":"stakingModuleAddress","type":"address"},{"internalType":"uint16","name":"stakingModuleFee","type":"uint16"},{"internalType":"uint16","name":"treasuryFee","type":"uint16"},{"internalType":"uint16","name":"targetShare","type":"uint16"},{"internalType":"uint8","name":"status","type":"uint8"},{"internalType":"string","name":"name","type":"string"},{"internalType":"uint64","name":"lastDepositAt","type":"uint64"},{"internalType":"uint256","name":"lastDepositBlock","type":"uint256"},{"internalType":"uint256","name":"exitedValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.StakingModule[]","name":"res","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingModulesCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getStakingRewardsDistribution","outputs":[{"internalType":"address[]","name":"recipients","type":"address[]"},{"internalType":"uint256[]","name":"stakingModuleIds","type":"uint256[]"},{"internalType":"uint96[]","name":"stakingModuleFees","type":"uint96[]"},{"internalType":"uint96","name":"totalFee","type":"uint96"},{"internalType":"uint256","name":"precisionPoints","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTotalFeeE4Precision","outputs":[{"internalType":"uint16","name":"totalFee","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getWithdrawalCredentials","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"hasStakingModule","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address","name":"_lido","type":"address"},{"internalType":"bytes32","name":"_withdrawalCredentials","type":"bytes32"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"onValidatorsCountsByNodeOperatorReportingFinished","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"pauseStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakingModuleIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_totalShares","type":"uint256[]"}],"name":"reportRewardsMinted","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"_nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"_exitedValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleExitedValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"bytes","name":"_nodeOperatorIds","type":"bytes"},{"internalType":"bytes","name":"_stuckValidatorsCounts","type":"bytes"}],"name":"reportStakingModuleStuckValidatorsCountByNodeOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"}],"name":"resumeStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"enum StakingRouter.StakingModuleStatus","name":"_status","type":"uint8"}],"name":"setStakingModuleStatus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_withdrawalCredentials","type":"bytes32"}],"name":"setWithdrawalCredentials","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"},{"internalType":"bool","name":"_triggerUpdateFinish","type":"bool"},{"components":[{"internalType":"uint256","name":"currentModuleExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOperatorExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"currentNodeOperatorStuckValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"newModuleExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"newNodeOperatorExitedValidatorsCount","type":"uint256"},{"internalType":"uint256","name":"newNodeOperatorStuckValidatorsCount","type":"uint256"}],"internalType":"struct StakingRouter.ValidatorsCountsCorrection","name":"_correction","type":"tuple"}],"name":"unsafeSetExitedValidatorsCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_stakingModuleIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_exitedValidatorsCounts","type":"uint256[]"}],"name":"updateExitedValidatorsCountByStakingModule","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"},{"internalType":"uint256","name":"_refundedValidatorsCount","type":"uint256"}],"name":"updateRefundedValidatorsCount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_targetShare","type":"uint256"},{"internalType":"uint256","name":"_stakingModuleFee","type":"uint256"},{"internalType":"uint256","name":"_treasuryFee","type":"uint256"}],"name":"updateStakingModule","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"_nodeOperatorId","type":"uint256"},{"internalType":"bool","name":"_isTargetLimitActive","type":"bool"},{"internalType":"uint256","name":"_targetLimit","type":"uint256"}],"name":"updateTargetValidatorsLimits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file diff --git a/package.json b/package.json index 1d389849e..9947dc151 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "lerna": "^3.22.1", "lint-staged": ">=10", "minimatch": "^6.2.0", + "mocha-param": "^2.0.1", "node-gyp": "^8.4.1", "prettier": "^2.8.4", "solhint": "^3.3.7", @@ -133,7 +134,6 @@ "@aragon/os": "^4.4.0", "@openzeppelin/contracts": "3.4.0", "@openzeppelin/contracts-v4.4": "npm:@openzeppelin/contracts@4.4.1", - "mocha-param": "^2.0.1", "openzeppelin-solidity": "2.0.0" }, "overrides": { diff --git a/test/0.4.24/legacy-oracle.test.js b/test/0.4.24/legacy-oracle.test.js index 700ae3c3a..b1dd527dd 100644 --- a/test/0.4.24/legacy-oracle.test.js +++ b/test/0.4.24/legacy-oracle.test.js @@ -23,6 +23,7 @@ const { ZERO_HASH, CONSENSUS_VERSION, computeTimestampAtEpoch, + SECONDS_PER_FRAME, } = require('../0.8.9/oracle/accounting-oracle-deploy.test') const getReportFields = (override = {}) => ({ @@ -204,6 +205,8 @@ contract('LegacyOracle', ([admin, stranger]) => { }) const { consensus, oracle } = deployedInfra await initAccountingOracle({ admin, oracle, consensus, shouldMigrateLegacyOracle: true }) + await deployedInfra.lido.setLegacyOracle(proxy.address) + await proxyAsOldImplementation.setLido(deployedInfra.lido.address) }) it('upgrade implementation', async () => { @@ -212,17 +215,23 @@ contract('LegacyOracle', ([admin, stranger]) => { await proxyAsNewImplementation.finalizeUpgrade_v4(deployedInfra.oracle.address) }) - it('submit report', async () => { - await deployedInfra.consensus.advanceTimeToNextFrameStart() + it('first report since migration', async () => { const { refSlot } = await deployedInfra.consensus.getCurrentFrame() const reportFields = getReportFields({ refSlot: +refSlot, }) + const reportItems = getAccountingReportDataItems(reportFields) const reportHash = calcAccountingReportDataHash(reportItems) await deployedInfra.consensus.addMember(admin, 1, { from: admin }) await deployedInfra.consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: admin }) const oracleVersion = +(await deployedInfra.oracle.getContractVersion()) + + // first report since migration has timeElapsed 1 slot off because lastProcessingRefSlot is calculated per legacy frame ref + // calculated as in code + const timeElapsed = (+refSlot - +(await deployedInfra.oracle.getLastProcessingRefSlot())) * SECONDS_PER_SLOT + assert.equals(timeElapsed, SECONDS_PER_FRAME - SECONDS_PER_SLOT) + const tx = await deployedInfra.oracle.submitReportData(reportItems, oracleVersion, { from: admin }) const epochId = Math.floor((+refSlot + 1) / SLOTS_PER_EPOCH) @@ -236,12 +245,29 @@ contract('LegacyOracle', ([admin, stranger]) => { }, { abi: LegacyOracleAbi } ) + assert.emits( + tx, + 'PostTotalShares', + { + postTotalPooledEther: 1, + preTotalPooledEther: 0, + timeElapsed, + totalShares: 1, + }, + { abi: LegacyOracleAbi } + ) + + const lastCompletedReportDelta = await proxyAsNewImplementation.getLastCompletedReportDelta() + assert.equals(lastCompletedReportDelta.postTotalPooledEther, 1) + assert.equals(lastCompletedReportDelta.preTotalPooledEther, 0) + assert.equals(lastCompletedReportDelta.timeElapsed, timeElapsed) const completedEpoch = await proxyAsNewImplementation.getLastCompletedEpochId() assert.equals(completedEpoch, epochId) }) it('time in sync with consensus', async () => { await deployedInfra.consensus.advanceTimeToNextFrameStart() + const { frameEpochId, frameStartTime, frameEndTime } = await proxyAsNewImplementation.getCurrentFrame() const consensusFrame = await deployedInfra.consensus.getCurrentFrame() const refSlot = consensusFrame.refSlot.toNumber() @@ -250,6 +276,38 @@ contract('LegacyOracle', ([admin, stranger]) => { assert.equals(frameEndTime, computeTimestampAtEpoch(+frameEpochId + EPOCHS_PER_FRAME) - 1) }) - it.skip('handlePostTokenRebase from lido') + it('second report', async () => { + const { refSlot } = await deployedInfra.consensus.getCurrentFrame() + const reportFields = getReportFields({ + refSlot: +refSlot, + }) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) + await deployedInfra.consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: admin }) + const oracleVersion = +(await deployedInfra.oracle.getContractVersion()) + const tx = await deployedInfra.oracle.submitReportData(reportItems, oracleVersion, { from: admin }) + const epochId = Math.floor((+refSlot + 1) / SLOTS_PER_EPOCH) + assert.emits( + tx, + 'Completed', + { + epochId, + beaconBalance: web3.utils.toWei(reportFields.clBalanceGwei, 'gwei'), + beaconValidators: reportFields.numValidators, + }, + { abi: LegacyOracleAbi } + ) + assert.emits( + tx, + 'PostTotalShares', + { + postTotalPooledEther: 1, + preTotalPooledEther: 0, + timeElapsed: SECONDS_PER_FRAME, + totalShares: 1, + }, + { abi: LegacyOracleAbi } + ) + }) }) }) diff --git a/test/0.4.24/lido-deposit-scenarios.test.js b/test/0.4.24/lido-deposit-scenarios.test.js index 3cb12ed8f..30e61c00e 100644 --- a/test/0.4.24/lido-deposit-scenarios.test.js +++ b/test/0.4.24/lido-deposit-scenarios.test.js @@ -4,22 +4,44 @@ const { EvmSnapshot, setBalance, getBalance } = require('../helpers/blockchain') const { ZERO_ADDRESS } = require('../helpers/constants') const { assert } = require('../helpers/assert') const { wei } = require('../helpers/wei') -const { StakingModuleStub } = require('../helpers/stubs/staking-module.stub') const { PUBKEY_LENGTH, FakeValidatorKeys, SIGNATURE_LENGTH } = require('../helpers/signing-keys') -const { GenericStub } = require('../helpers/stubs/generic.stub') +const { ContractStub } = require('../helpers/contract-stub') -contract('Lido deposit scenarios', ([staker, depositor]) => { +contract('Lido deposit scenarios', ([deployer, staker, depositor]) => { const STAKING_MODULE_ID = 1 const DEPOSIT_CALLDATA = '0x0' + const TOTAL_EXITED_VALIDATORS = 5 + const TOTAL_DEPOSITED_VALIDATORS = 16 + const DEPOSITABLE_VALIDATORS_COUNT = 2 + let lido, stakingRouter let stakingModuleStub, depositContractStub let snapshot + const stubObtainDepositDataReturns = (publicKeysBatch, signaturesBatch) => + ContractStub(stakingModuleStub) + .on('obtainDepositData', { + return: { + type: ['bytes', 'bytes'], + value: [publicKeysBatch, signaturesBatch], + }, + }) + .update({ from: deployer }) + before('prepare base Lido & StakingRouter setup', async () => { - stakingModuleStub = await StakingModuleStub.new() - depositContractStub = await GenericStub.new('contracts/0.6.11/deposit_contract.sol:IDepositContract') - // just accept all ether and do nothing - await GenericStub.stub(depositContractStub, 'deposit') + stakingModuleStub = await ContractStub('IStakingModule') + .on('getStakingModuleSummary', { + return: { + type: ['uint256', 'uint256', 'uint256'], + value: [TOTAL_EXITED_VALIDATORS, TOTAL_DEPOSITED_VALIDATORS, DEPOSITABLE_VALIDATORS_COUNT], + }, + }) + .create({ from: deployer }) + + depositContractStub = await ContractStub('contracts/0.6.11/deposit_contract.sol:IDepositContract') + .on('deposit') // just accept all ether and do nothing + .create({ from: deployer }) + const protocol = await deployProtocol({ stakingModulesFactory: async () => { return [ @@ -59,17 +81,8 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(lido, initialLidoETHBalance + unaccountedLidoETHBalance) assert.equal(await getBalance(lido), initialLidoETHBalance + unaccountedLidoETHBalance) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) - - const depositDataLength = depositableValidatorsCount - await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { - return: { depositDataLength }, - }) + const depositDataLength = DEPOSITABLE_VALIDATORS_COUNT + await stubObtainDepositDataReturns(...new FakeValidatorKeys(depositDataLength).slice()) const submitAmount = wei`320 ether` await lido.submit(ZERO_ADDRESS, { from: staker, value: wei.str(submitAmount) }) @@ -80,7 +93,7 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const depositedEther = wei`32 ether` * wei.min(maxDepositsCount, depositableValidatorsCount) + const depositedEther = wei`32 ether` * wei.min(maxDepositsCount, DEPOSITABLE_VALIDATORS_COUNT) assert.equals( await getBalance(lido), initialLidoETHBalance + unaccountedLidoETHBalance + submitAmount - depositedEther @@ -93,17 +106,8 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(stakingRouter, initialStakingRouterBalance) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) - - const depositDataLength = depositableValidatorsCount + 2 - await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { - return: { depositDataLength }, - }) + const depositDataLength = DEPOSITABLE_VALIDATORS_COUNT + 2 + await stubObtainDepositDataReturns(...new FakeValidatorKeys(depositDataLength).slice()) const initialLidETHBalance = await getBalance(lido) @@ -116,7 +120,7 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await assert.reverts( lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'InvalidPublicKeysBatchLength', - [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * depositableValidatorsCount] + [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * DEPOSITABLE_VALIDATORS_COUNT] ) }) @@ -125,21 +129,12 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(stakingRouter, initialStakingRouterBalance) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) - - const depositDataLength = depositableValidatorsCount + 2 + const depositDataLength = DEPOSITABLE_VALIDATORS_COUNT + 2 const depositData = new FakeValidatorKeys(depositDataLength) - await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { - return: { - publicKeysBatch: depositData.slice()[0], // two extra signatures returned - signaturesBatch: depositData.slice(0, depositableValidatorsCount)[1], - }, - }) + await stubObtainDepositDataReturns( + depositData.slice()[0], // two extra public keys returned + depositData.slice(0, DEPOSITABLE_VALIDATORS_COUNT)[1] + ) const initialLidETHBalance = await getBalance(lido) @@ -152,7 +147,7 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await assert.reverts( lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'InvalidPublicKeysBatchLength', - [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * depositableValidatorsCount] + [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * DEPOSITABLE_VALIDATORS_COUNT] ) }) @@ -161,21 +156,12 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(stakingRouter, initialStakingRouterBalance) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) - - const depositDataLength = depositableValidatorsCount + 2 + const depositDataLength = DEPOSITABLE_VALIDATORS_COUNT + 2 const depositData = new FakeValidatorKeys(depositDataLength) - await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { - return: { - publicKeysBatch: depositData.slice(0, depositableValidatorsCount)[0], - signaturesBatch: depositData.slice()[1], // two extra signatures returned - }, - }) + await stubObtainDepositDataReturns( + depositData.slice(0, DEPOSITABLE_VALIDATORS_COUNT)[0], + depositData.slice()[1] // two extra signatures returned + ) const initialLidETHBalance = await getBalance(lido) @@ -188,15 +174,17 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await assert.reverts( lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'InvalidSignaturesBatchLength', - [SIGNATURE_LENGTH * depositDataLength, SIGNATURE_LENGTH * depositableValidatorsCount] + [SIGNATURE_LENGTH * depositDataLength, SIGNATURE_LENGTH * DEPOSITABLE_VALIDATORS_COUNT] ) }) it('invalid ETH value was used for deposits in StakingRouter', async () => { // on each deposit call forward back 1 ether to the staking router - await GenericStub.stub(depositContractStub, 'deposit', { - forwardETH: { value: wei.str`1 ether`, recipient: stakingRouter.address }, - }) + await ContractStub(depositContractStub) + .on('deposit', { + ethForwards: [{ recipient: stakingRouter.address, value: wei.str`1 ether` }], + }) + .update({ from: deployer }) const submitAmount = wei`320 ether` const initialLidoETHBalance = await getBalance(lido) @@ -204,17 +192,9 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { assert.equal(await getBalance(lido), initialLidoETHBalance + submitAmount) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) + const depositDataLength = DEPOSITABLE_VALIDATORS_COUNT + await stubObtainDepositDataReturns(...new FakeValidatorKeys(depositDataLength).slice()) - const depositDataLength = depositableValidatorsCount - await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { - return: { depositDataLength }, - }) const maxDepositsCount = 10 await assert.reverts(lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor })) }) @@ -226,16 +206,11 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { assert.equal(await getBalance(lido), initialLidoETHBalance + submitAmount) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) - - await StakingModuleStub.stub(stakingModuleStub, 'obtainDepositData', { - revert: { reason: 'INVALID_ALLOCATED_KEYS_COUNT' }, - }) + await ContractStub(stakingModuleStub) + .on('obtainDepositData', { + revert: { reason: 'INVALID_ALLOCATED_KEYS_COUNT' }, + }) + .update({ from: deployer }) const maxDepositsCount = 10 await assert.reverts( @@ -248,13 +223,6 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { const submitAmount = wei`100 ether` await lido.submit(ZERO_ADDRESS, { from: staker, value: wei.str(submitAmount) }) - const depositableValidatorsCount = 2 - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { - totalExitedValidators: 5, - totalDepositedValidators: 16, - depositableValidatorsCount, - }) - const stakingModuleStateBefore = await stakingRouter.getStakingModule(STAKING_MODULE_ID) const maxDepositsCount = 0 diff --git a/test/0.4.24/node-operators-registry-happy-path.test.js b/test/0.4.24/node-operators-registry-happy-path.test.js new file mode 100644 index 000000000..380a1c9c3 --- /dev/null +++ b/test/0.4.24/node-operators-registry-happy-path.test.js @@ -0,0 +1,873 @@ +const { contract, web3 } = require('hardhat') +const { getEvents } = require('@aragon/contract-helpers-test') +const { assert } = require('../helpers/assert') + +const signingKeys = require('../helpers/signing-keys') +const { DSMAttestMessage } = require('../helpers/signatures') +const { deployProtocol } = require('../helpers/protocol') +const { setupNodeOperatorsRegistry, NodeOperatorsRegistry } = require('../helpers/staking-modules') +const { e18, e27, toBN, ETH } = require('../helpers/utils') +const { getCurrentBlockTimestamp, advanceChainTime, waitBlocks } = require('../helpers/blockchain') +const { + getAccountingReportDataItems, + encodeExtraDataItems, + packExtraDataList, + calcExtraDataListHash, + calcAccountingReportDataHash, +} = require('../0.8.9/oracle/accounting-oracle-deploy.test') + +const PENALTY_DELAY = 24 * 60 * 60 // 1 days +const E9 = toBN(10).pow(toBN(9)) + +const ADDRESS_1 = '0x0000000000000000000000000000000000000001' +const ADDRESS_2 = '0x0000000000000000000000000000000000000002' +const ADDRESS_3 = '0x0000000000000000000000000000000000000003' +const ADDRESS_4 = '0x0000000000000000000000000000000000000004' + +const NOR_ABI_GET_EV = { decodeForAbi: NodeOperatorsRegistry._json.abi } +const NOR_ABI_ASSERT_EV = { abi: NodeOperatorsRegistry._json.abi } + +const NODE_OPERATORS = [ + { + id: 0, + name: 'Node operator #1', + rewardAddressInitial: ADDRESS_1, + totalSigningKeysCount: 10, + vettedSigningKeysCount: 7, + }, + { + id: 1, + name: 'Node operator #2', + rewardAddressInitial: ADDRESS_2, + totalSigningKeysCount: 15, + vettedSigningKeysCount: 10, + }, + { + id: 2, + name: 'Node operator #3', + rewardAddressInitial: ADDRESS_3, + totalSigningKeysCount: 10, + vettedSigningKeysCount: 5, + }, + { + id: 3, + name: 'Node operator #4', + rewardAddressInitial: ADDRESS_4, + totalSigningKeysCount: 10, + vettedSigningKeysCount: 5, + }, +] + +const Operator1 = NODE_OPERATORS[0] +const Operator2 = NODE_OPERATORS[1] +const Operator3 = NODE_OPERATORS[2] +const Operator4 = NODE_OPERATORS[3] + +const forEachSync = async (arr, cb) => { + for (let i = 0; i < arr.length; ++i) { + await cb(arr[i], i) + } +} + +contract('NodeOperatorsRegistry', ([appManager, rewards1, rewards2, rewards3, rewards4, user1, nobody]) => { + let dsm + let lido + let nor + let stakingRouter + let depositContract + let depositRoot + let voting + let rewardAddresses + let guardians + let withdrawalCredentials + let consensus + let oracle + let consensusVersion + let signers + let consensusMember + let curatedId + + let stateTotalVetted = 0 + let stateTotalDepositable = 0 + + /** + * Helpers + */ + + async function addKeysToOperator(operatorId, keysToAdd) { + const keys = new signingKeys.FakeValidatorKeys(keysToAdd) + const operatorBefore = await nor.getNodeOperator(operatorId, true) + const keysCountBefore = +(await nor.getTotalSigningKeyCount(operatorId)) + const unusedKeysCountBefore = +(await nor.getUnusedSigningKeyCount(operatorId)) + + await nor.addSigningKeys(operatorId, keys.count, ...keys.slice(), { from: voting.address }) + + const operator = await nor.getNodeOperator(operatorId, true) + const keysCount = +(await nor.getTotalSigningKeyCount(operatorId)) + const unusedKeysCount = +(await nor.getUnusedSigningKeyCount(operatorId)) + + assert.equals(+operatorBefore.totalSigningKeys + keys.count, operator.totalSigningKeys) + assert.equals(keysCountBefore + keys.count, keysCount) + assert.equals(unusedKeysCountBefore + keys.count, unusedKeysCount) + + for (let i = 0; i < keys.count; ++i) { + const { key, depositSignature } = await nor.getSigningKey(operatorId, i + keysCountBefore) + const [expectedPublicKey, expectedSignature] = keys.get(i) + assert.equals(key, expectedPublicKey) + assert.equals(depositSignature, expectedSignature) + } + } + + async function setNodeOperatorStakingLimit(operatorId, stakingLimit) { + const operatorBefore = await nor.getNodeOperator(operatorId, true) + const summaryBefore = await nor.getNodeOperatorSummary(operatorId) + const stakingModuleSummaryBefore = await nor.getStakingModuleSummary() + + const tx = await nor.setNodeOperatorStakingLimit(operatorId, stakingLimit, { from: voting.address }) + + const operator = await nor.getNodeOperator(operatorId, true) + const summary = await nor.getNodeOperatorSummary(operatorId) + const stakingModuleSummary = await nor.getStakingModuleSummary() + const expectedLimit = +operatorBefore.stakingLimit + stakingLimit + const expectedDepositable = +summaryBefore.depositableValidatorsCount + stakingLimit + + assert.equals(operator.stakingLimit, expectedLimit) + assert.equals(summary.depositableValidatorsCount, expectedDepositable) + assert.equals( + +stakingModuleSummaryBefore.depositableValidatorsCount + stakingLimit, + stakingModuleSummary.depositableValidatorsCount + ) + assert.emits( + tx, + 'VettedSigningKeysCountChanged', + { + nodeOperatorId: operatorId, + approvedValidatorsCount: expectedLimit, + }, + NOR_ABI_ASSERT_EV + ) + } + + async function assertDepositCall(callIdx, operatorId, keyIdx) { + const regCall = await depositContract.calls.call(callIdx) + const { key, depositSignature } = await nor.getSigningKey(operatorId, keyIdx) + assert.equal(regCall.pubkey, key) + assert.equal(regCall.signature, depositSignature) + assert.equal(regCall.withdrawal_credentials, withdrawalCredentials) + assert.equals(regCall.value, ETH(32)) + } + + async function assertOperatorDeposits(operatorData, deposited, keysLeft) { + const operator = await nor.getNodeOperator(operatorData.id, true) + const summary = await nor.getNodeOperatorSummary(operatorData.id) + assert.equals(operator.usedSigningKeys, deposited, `${operatorData.name} usedSigningKeys asserting to ${deposited}`) + assert.equals( + summary.totalDepositedValidators, + deposited, + `${operatorData.name} totalDepositedValidators asserting to ${deposited}` + ) + assert.equals( + summary.depositableValidatorsCount, + keysLeft, + `${operatorData.name} depositableValidatorsCount asserting to ${keysLeft}` + ) + } + + async function assertTargetLimit(operatorData, isActive, limit, depositable) { + const summary = await nor.getNodeOperatorSummary(operatorData.id) + assert.equals( + summary.isTargetLimitActive, + isActive, + `${operatorData.name} isTargetLimitActive limit asserting to ${isActive}` + ) + assert.equals( + summary.targetValidatorsCount, + limit, + `${operatorData.name} targetValidatorsCount asserting to ${limit}` + ) + assert.equals( + summary.depositableValidatorsCount, + depositable, + `${operatorData.name} depositableValidatorsCount asserting to ${depositable}` + ) + } + + async function assertRewardsDistributedEvent(tx, eventIdx, rewardsAddress, amount) { + const event = getEvents(tx, 'RewardsDistributed', NOR_ABI_GET_EV)[eventIdx] + assert.addressEqual(event.args.rewardAddress, rewardsAddress) + assert.isClose(event.args.sharesAmount, amount, 10) + } + + async function assertNodeOperatorPenalizedEvent(tx, eventIdx, rewardsAddress, amount) { + const event = getEvents(tx, 'NodeOperatorPenalized', NOR_ABI_GET_EV)[eventIdx] + assert.addressEqual(event.args.recipientAddress, rewardsAddress) + assert.isClose(event.args.sharesPenalizedAmount, amount, 10) + } + + async function makeDeposit(stakesDeposited) { + const depositedValue = ETH(32 * stakesDeposited) + const depositCallCountBefore = +(await depositContract.totalCalls()) + const stakingModuleSummaryBefore = await nor.getStakingModuleSummary() + + await web3.eth.sendTransaction({ to: lido.address, from: user1, value: depositedValue }) + + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await nor.getKeysOpIndex() + + DSMAttestMessage.setMessagePrefix(await dsm.ATTEST_MESSAGE_PREFIX()) + + const attest = new DSMAttestMessage(block.number, block.hash, depositRoot, curatedId, keysOpIndex) + const signatures = [ + attest.sign(guardians.privateKeys[guardians.addresses[0]]), + attest.sign(guardians.privateKeys[guardians.addresses[1]]), + ] + + // triggers flow: + // DSM.depositBufferedEther() -> Lido.deposit() -> StakingRouter.deposit() -> Module.obtainDepositData() + await dsm.depositBufferedEther(block.number, block.hash, depositRoot, curatedId, keysOpIndex, '0x', signatures) + + const depositCallCount = +(await depositContract.totalCalls()) + const stakingModuleSummary = await nor.getStakingModuleSummary() + + assert.equals(depositCallCount - depositCallCountBefore, stakesDeposited) + assert.equals( + +stakingModuleSummaryBefore.depositableValidatorsCount - +stakingModuleSummary.depositableValidatorsCount, + stakesDeposited + ) + } + + async function deactivateNodeOperator(operatorId) { + const operatorBefore = await nor.getNodeOperator(operatorId, true) + const summaryBefore = await nor.getNodeOperatorSummary(operatorId) + const activeOperatorsBefore = await nor.getActiveNodeOperatorsCount() + const stakingModuleSummaryBefore = await nor.getStakingModuleSummary() + const keysToCut = +summaryBefore.depositableValidatorsCount + + const tx = await nor.deactivateNodeOperator(operatorId, { from: voting.address }) + + const operator = await nor.getNodeOperator(operatorId, true) + const summary = await nor.getNodeOperatorSummary(operatorId) + const activeOperatorsAfter = await nor.getActiveNodeOperatorsCount() + const stakingModuleSummary = await nor.getStakingModuleSummary() + + assert.isFalse(operator.active) + assert.isFalse(await nor.getNodeOperatorIsActive(operatorId)) + assert.equals(+activeOperatorsBefore - 1, +activeOperatorsAfter) + assert.emits(tx, 'NodeOperatorActiveSet', { nodeOperatorId: operatorId, active: false }) + if (+operatorBefore.stakingLimit - +operatorBefore.usedSigningKeys > 0) { + assert.emits(tx, 'VettedSigningKeysCountChanged', { + nodeOperatorId: operatorId, + approvedValidatorsCount: operator.usedSigningKeys, + }) + } + assert.equals(summary.depositableValidatorsCount, 0) + assert.equals(operator.stakingLimit, operator.usedSigningKeys) + assert.equals( + +stakingModuleSummaryBefore.depositableValidatorsCount - keysToCut, + stakingModuleSummary.depositableValidatorsCount + ) + } + + /** + * Deploy + */ + + before('deploy base app', async () => { + const deployed = await deployProtocol({ + stakingModulesFactory: async (protocol) => { + const curatedModule = await setupNodeOperatorsRegistry(protocol) + return [ + { + module: curatedModule, + name: 'Curated', + targetShares: 10000, + moduleFee: 500, + treasuryFee: 500, + }, + ] + }, + }) + + rewardAddresses = [rewards1, rewards2, rewards3, rewards4] + + lido = deployed.pool + nor = deployed.stakingModules[0] + stakingRouter = deployed.stakingRouter + depositContract = deployed.depositContract + depositRoot = await depositContract.get_deposit_root() + dsm = deployed.depositSecurityModule + guardians = deployed.guardians + voting = deployed.voting + consensus = deployed.consensusContract + oracle = deployed.oracle + signers = deployed.signers + consensusMember = signers[2].address + appManager = deployed.appManager + + consensusVersion = await oracle.getConsensusVersion() + await consensus.removeMember(signers[4].address, 2, { from: voting.address }) + await consensus.removeMember(signers[3].address, 1, { from: voting.address }) + + withdrawalCredentials = '0x'.padEnd(66, '1234') + await stakingRouter.setWithdrawalCredentials(withdrawalCredentials, { from: voting.address }) + + const [curated] = await stakingRouter.getStakingModules() + curatedId = curated.id + }) + + /** + * Actual flow + */ + + describe('Happy path', () => { + context('Initial setup', () => { + it('Add node operator', async () => { + await forEachSync(NODE_OPERATORS, async (operatorData, i) => { + const initialName = `operator ${i + 1}` + const tx = await nor.addNodeOperator(initialName, operatorData.rewardAddressInitial, { from: voting.address }) + const expectedStakingLimit = 0 + + assert.emits(tx, 'NodeOperatorAdded', { + nodeOperatorId: operatorData.id, + name: initialName, + rewardAddress: operatorData.rewardAddressInitial, + stakingLimit: expectedStakingLimit, + }) + + assert.isTrue(await nor.getNodeOperatorIsActive(operatorData.id)) + const operator = await nor.getNodeOperator(operatorData.id, true) + assert.isTrue(operator.active) + assert.equals(operator.name, initialName) + assert.equals(operator.rewardAddress, operatorData.rewardAddressInitial) + assert.equals(operator.stakingLimit, 0) + assert.equals(operator.stoppedValidators, 0) + assert.equals(operator.totalSigningKeys, 0) + assert.equals(operator.usedSigningKeys, 0) + }) + + assert.equals(await nor.getNodeOperatorsCount(), NODE_OPERATORS.length) + }) + + it('Deactivate node operator 4', async () => { + await deactivateNodeOperator(Operator4.id) + }) + + it('Set name', async () => { + await forEachSync(NODE_OPERATORS, async (operatorData, i) => { + await nor.setNodeOperatorName(operatorData.id, operatorData.name, { from: voting.address }) + const operator = await nor.getNodeOperator(operatorData.id, true) + assert.equals(operator.name, operatorData.name) + }) + }) + + it('Set reward address', async () => { + await forEachSync(NODE_OPERATORS, async (operatorData, i) => { + const rewardAddress = rewardAddresses[i] + await nor.setNodeOperatorRewardAddress(operatorData.id, rewardAddress, { from: voting.address }) + const operator = await nor.getNodeOperator(operatorData.id, true) + assert.equals(operator.rewardAddress, rewardAddress) + }) + }) + + it('Add signing keys', async () => { + await forEachSync(NODE_OPERATORS, async (operatorData, i) => { + await addKeysToOperator(operatorData.id, operatorData.totalSigningKeysCount) + }) + }) + + it('Set staking limit', async () => { + await forEachSync(NODE_OPERATORS, async (operatorData, i) => { + if (!(await nor.getNodeOperatorIsActive(operatorData.id))) return + stateTotalVetted += operatorData.vettedSigningKeysCount + await setNodeOperatorStakingLimit(operatorData.id, operatorData.vettedSigningKeysCount) + }) + + const stakingModuleSummary = await nor.getStakingModuleSummary() + assert.equals(stakingModuleSummary.depositableValidatorsCount, stateTotalVetted) + stateTotalDepositable = stateTotalVetted + }) + + it('Removing key with index less then stakingLimit will trim stakingLimit value to this border', async () => { + const operatorData = Operator1 + + const operatorBefore = await nor.getNodeOperator(operatorData.id, true) + const summaryBefore = await nor.getNodeOperatorSummary(operatorData.id) + const keysCountBefore = await nor.getTotalSigningKeyCount(operatorData.id) + const unusedKeysCountBefore = await nor.getUnusedSigningKeyCount(operatorData.id) + const keyIdxToRemove = 1 + const keyBefore = await nor.getSigningKey(operatorData.id, keyIdxToRemove) + assert.equals(operatorBefore.stakingLimit, operatorData.vettedSigningKeysCount) + + await nor.removeSigningKey(operatorData.id, keyIdxToRemove, { from: voting.address }) + + const operatorAfter = await nor.getNodeOperator(operatorData.id, true) + const summaryAfter = await nor.getNodeOperatorSummary(operatorData.id) + const keysCountAfter = await nor.getTotalSigningKeyCount(operatorData.id) + const unusedKeysCountAfter = await nor.getUnusedSigningKeyCount(operatorData.id) + const keyAfter = await nor.getSigningKey(operatorData.id, keyIdxToRemove) + + assert.equals(operatorAfter.stakingLimit, keyIdxToRemove) + assert.equals(+operatorBefore.totalSigningKeys - 1, +operatorAfter.totalSigningKeys) + assert.equals(+keysCountBefore - 1, +keysCountAfter) + assert.equals(+unusedKeysCountBefore - 1, +unusedKeysCountAfter) + assert.equals(summaryBefore.depositableValidatorsCount, operatorData.vettedSigningKeysCount) + assert.equals(summaryAfter.depositableValidatorsCount, keyIdxToRemove) + assert.notEqual(keyBefore.key, keyAfter.key) + assert.notEqual(keyBefore.depositSignature, keyAfter.depositSignature) + }) + + it('Set stakingLimit back after key removement', async () => { + const operatorData = Operator1 + stateTotalVetted += operatorData.vettedSigningKeysCount + await nor.setNodeOperatorStakingLimit(operatorData.id, operatorData.vettedSigningKeysCount, { + from: voting.address, + }) + const summary = await nor.getNodeOperatorSummary(operatorData.id) + const operator = await nor.getNodeOperator(operatorData.id, true) + assert.equals(operator.stakingLimit, operatorData.vettedSigningKeysCount) + assert.equals(summary.depositableValidatorsCount, operatorData.vettedSigningKeysCount) + }) + + it('Set target limit to Operator 2', async () => { + const operatorData = Operator2 + const operatorId = operatorData.id + const targetLimitCount = 1 + + await assertTargetLimit(operatorData, false, 0, operatorData.vettedSigningKeysCount) + + // StakingRouter.updateTargetValidatorsLimits() -> NOR.updateTargetValidatorsLimits() + const tx = await stakingRouter.updateTargetValidatorsLimits(curatedId, operatorId, true, targetLimitCount, { + from: voting.address, + }) + + await assertTargetLimit(operatorData, true, targetLimitCount, targetLimitCount) + + assert.emits( + tx, + 'TargetValidatorsCountChanged', + { nodeOperatorId: operatorId, targetValidatorsCount: 1 }, + NOR_ABI_ASSERT_EV + ) + + stateTotalDepositable -= operatorData.vettedSigningKeysCount - targetLimitCount + }) + + it('Initial general summary values', async () => { + const summary = await nor.getStakingModuleSummary() + assert.equals(summary.totalExitedValidators, 0) + assert.equals(summary.totalDepositedValidators, 0) + assert.equals(summary.depositableValidatorsCount, stateTotalDepositable) + }) + + it('Modify penalty delay', async () => { + const tx = await nor.setStuckPenaltyDelay(PENALTY_DELAY, { from: voting.address }) + assert.emits(tx, 'StuckPenaltyDelayChanged', { stuckPenaltyDelay: PENALTY_DELAY }, NOR_ABI_ASSERT_EV) + assert.equals(await nor.getStuckPenaltyDelay(), PENALTY_DELAY) + }) + }) + + context('Deposits distribution', () => { + it('Obtain deposit data', async () => { + /** + * Expected deposits fill 1 2 3 4 5 6 + * Operator 1 [ x x x ] + * Operator 2 (limit = 1) [ x ] + * Operator 3 [ x x ] + * Operator 4 (inactive) [ ] + */ + + await makeDeposit(6) + + await assertDepositCall(0, Operator1.id, 0) + await assertDepositCall(1, Operator1.id, 1) + await assertDepositCall(2, Operator1.id, 2) + await assertDepositCall(3, Operator2.id, 0) + await assertDepositCall(4, Operator3.id, 0) + await assertDepositCall(5, Operator3.id, 1) + + await assertOperatorDeposits(Operator1, 3, 4) + await assertOperatorDeposits(Operator2, 1, 0) + await assertOperatorDeposits(Operator3, 2, 3) + }) + + it('Rewards distribution', async () => { + const distribution = await nor.getRewardsDistribution(web3.utils.toWei('30')) + assert.equal(distribution.shares[0], web3.utils.toWei('15')) + assert.equal(distribution.shares[1], web3.utils.toWei('5')) + assert.equal(distribution.shares[2], web3.utils.toWei('10')) + assert.equal(distribution.recipients[0], rewards1) + assert.equal(distribution.recipients[1], rewards2) + assert.equal(distribution.recipients[2], rewards3) + }) + }) + + context('Validators exiting and stuck', () => { + it('Initial rewards state', async () => { + const sharesNOR = +(await lido.sharesOf(nor.address)) + const sharesRewards1 = +(await lido.sharesOf(rewards1)) + const sharesRewards2 = +(await lido.sharesOf(rewards2)) + const sharesRewards3 = +(await lido.sharesOf(rewards3)) + assert.equals(sharesNOR, 0) + assert.equals(sharesRewards1, 0) + assert.equals(sharesRewards2, 0) + assert.equals(sharesRewards3, 0) + }) + + let reportTx + + it('Consensus+oracle report', async () => { + const { refSlot } = await consensus.getCurrentFrame() + + const extraData = { + exitedKeys: [{ moduleId: 1, nodeOpIds: [0], keysCounts: [2] }], + stuckKeys: [{ moduleId: 1, nodeOpIds: [1, 2], keysCounts: [1, 1] }], + } + + const extraDataItems = encodeExtraDataItems(extraData) + const extraDataList = packExtraDataList(extraDataItems) + const extraDataHash = calcExtraDataListHash(extraDataList) + + const reportFields = { + consensusVersion, + numValidators: 6, + clBalanceGwei: toBN(ETH(32 * 6 + 1)).div(E9), + stakingModuleIdsWithNewlyExitedValidators: [curatedId], + numExitedValidatorsByStakingModule: [2], + withdrawalVaultBalance: e18(0), + elRewardsVaultBalance: e18(0), + sharesRequestedToBurn: e18(0), + withdrawalFinalizationBatches: [], + simulatedShareRate: e27(1), + isBunkerMode: false, + extraDataFormat: 1, + refSlot: +refSlot, + extraDataHash, + extraDataItemsCount: 2, + } + + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) + + await consensus.submitReport(+refSlot, reportHash, consensusVersion, { from: consensusMember }) + + // Mentionable internal calls + // AccountingOracle.submitReportData() + // -> Lido.handleOracleReport()._processRewards()._distributeFee() + // -> StakingRouter.reportRewardsMinted() -> NOR.onRewardsMinted() + // ._transferModuleRewards()._transferShares() + // -> StakingRouter.updateExitedValidatorsCountByStakingModule() -> StakingRouter.stakingModule[id].exitedValidatorsCount = exitedCount + await oracle.submitReportData(reportItems, consensusVersion, { from: consensusMember }) + + const sharesNORInMiddle = await lido.sharesOf(nor.address) + // TODO: Calculate this assert value + assert.isClose(sharesNORInMiddle, '49767921609076843', 10) + + // Mentionable internal calls + // AccountingOracle.submitReportExtraDataList() + // -> StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished() -> NOR.onExitedAndStuckValidatorsCountsUpdated()._distributeRewards() + // emits NOR.NodeOperatorPenalized + // emits NOR.RewardsDistributed + // -> stETH.transferShares() + // -> Burner.requestBurnShares() + // -> StakingRouter.reportStakingModuleExitedValidatorsCountByNodeOperator() -> NOR.updateExitedValidatorsCount() -> + // emits NOR.ExitedSigningKeysCountChanged + // ._saveSummarySigningKeysStats() + // ._updateSummaryMaxValidatorsCount() + // -> StakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator() -> NOR.updateStuckValidatorsCount() -> + // emits NOR.StuckPenaltyStateChanged + // ._saveOperatorStuckPenaltyStats() + // ._updateSummaryMaxValidatorsCount() + reportTx = await oracle.submitReportExtraDataList(extraDataList, { from: voting.address }) + }) + + // TODO: calculate those assert values + const rewardAmountForOperator1 = '12441980402269210' + const rewardAmountForOperator2 = '6220990201134605' + const rewardAmountForOperator3 = '12441980402269210' + const penaltyAmountForOperator2 = rewardAmountForOperator2 + const penaltyAmountForOperator3 = rewardAmountForOperator3 + + it('Events should be emitted after exit/stuck report', async () => { + const tx = reportTx + + assert.emits( + tx, + 'ExitedSigningKeysCountChanged', + { nodeOperatorId: Operator1.id, exitedValidatorsCount: 2 }, + NOR_ABI_ASSERT_EV + ) + + assert.emits( + tx, + 'StuckPenaltyStateChanged', + { + nodeOperatorId: Operator2.id, + stuckValidatorsCount: 1, + refundedValidatorsCount: 0, + stuckPenaltyEndTimestamp: 0, + }, + NOR_ABI_ASSERT_EV + ) + + await assertRewardsDistributedEvent(tx, 0, rewards1, rewardAmountForOperator1) + await assertRewardsDistributedEvent(tx, 1, rewards2, rewardAmountForOperator2) + await assertRewardsDistributedEvent(tx, 2, rewards3, rewardAmountForOperator3) + await assertNodeOperatorPenalizedEvent(tx, 0, rewards2, penaltyAmountForOperator2) + await assertNodeOperatorPenalizedEvent(tx, 1, rewards3, penaltyAmountForOperator3) + }) + + it('Operator summaries after exit/stuck report', async () => { + const operator1 = await nor.getNodeOperator(Operator1.id, true) + const summaryOperator1 = await nor.getNodeOperatorSummary(Operator1.id) + assert.equals(operator1.stoppedValidators, 2) + assert.equals(summaryOperator1.totalExitedValidators, 2) + assert.equals(summaryOperator1.depositableValidatorsCount, 4) + + const summaryOperator2 = await nor.getNodeOperatorSummary(Operator2.id) + assert.equals(summaryOperator2.stuckValidatorsCount, 1) + assert.equals(summaryOperator2.depositableValidatorsCount, 0) + + const summaryOperator3 = await nor.getNodeOperatorSummary(Operator3.id) + assert.equals(summaryOperator3.stuckValidatorsCount, 1) + assert.equals(summaryOperator3.depositableValidatorsCount, 0) + + const sharesNORAfter = await lido.sharesOf(nor.address) + const sharesRewards1After = await lido.sharesOf(rewards1) + const sharesRewards2After = await lido.sharesOf(rewards2) + const sharesRewards3After = await lido.sharesOf(rewards3) + + assert.isClose(sharesNORAfter, 0, 10) + assert.isClose(sharesRewards1After, rewardAmountForOperator1, 10) + assert.isClose(sharesRewards2After, rewardAmountForOperator2, 10) + assert.isClose(sharesRewards3After, rewardAmountForOperator3, 10) + }) + }) + + context('Updating state unsafely', () => { + let correctionTx + + it('unsafeSetExitedValidatorsCount', async () => { + // should be distributed after update + await lido.transfer(nor.address, ETH(10), { from: user1 }) + + const operatorData = Operator1 + const correction = { + currentModuleExitedValidatorsCount: 2, + currentNodeOperatorExitedValidatorsCount: 2, + currentNodeOperatorStuckValidatorsCount: 0, + newModuleExitedValidatorsCount: 3, + newNodeOperatorExitedValidatorsCount: 3, + newNodeOperatorStuckValidatorsCount: 0, + } + + // StakingRouter.unsafeSetExitedValidatorsCount() -> NOR.onExitedAndStuckValidatorsCountsUpdated()._distributeRewards() + // emits NOR.NodeOperatorPenalized + // emits NOR.RewardsDistributed + // -> stETH.transferShares() + // -> Burner.requestBurnShares() + correctionTx = await stakingRouter.unsafeSetExitedValidatorsCount( + curatedId, + operatorData.id, + true, + correction, + { from: voting.address } + ) + + const summaryModule = await nor.getStakingModuleSummary() + const summaryOperator1 = await nor.getNodeOperatorSummary(operatorData.id) + assert.equals(summaryModule.totalExitedValidators, correction.newModuleExitedValidatorsCount) + assert.equals(summaryOperator1.totalExitedValidators, correction.newNodeOperatorExitedValidatorsCount) + + // TODO: calculate those assert values + await assertRewardsDistributedEvent(correctionTx, 0, rewards2, '1658930720302561458') + await assertRewardsDistributedEvent(correctionTx, 1, rewards3, '3317861440605122916') + }) + + // TODO: assert stuck operators and NodeOperatorPenalized event after unsafeSetExitedValidatorsCount() + }) + + context('Keys and limits settings tweaks', () => { + it('Disable TargetLimit', async () => { + const operatorData = Operator2 + + await assertTargetLimit(operatorData, true, 1, 0) + + // StakingRouter.updateTargetValidatorsLimits() -> NOR.updateTargetValidatorsLimits() + const tx = await stakingRouter.updateTargetValidatorsLimits(curatedId, operatorData.id, false, 0, { + from: voting.address, + }) + + // keysLeft still zero because of stucked key + await assertTargetLimit(operatorData, false, 0, 0) + + assert.emits( + tx, + 'TargetValidatorsCountChanged', + { nodeOperatorId: operatorData.id, targetValidatorsCount: 0 }, + NOR_ABI_ASSERT_EV + ) + }) + + it('Remove multiple signing keys', async () => { + const operatorData = Operator1 + + const operatorBefore = await nor.getNodeOperator(operatorData.id, true) + const summaryBefore = await nor.getNodeOperatorSummary(operatorData.id) + const keysCountBefore = await nor.getTotalSigningKeyCount(operatorData.id) + const unusedKeysCountBefore = await nor.getUnusedSigningKeyCount(operatorData.id) + const keyIdxToRemove = +operatorBefore.usedSigningKeys + 1 + const keysCountToRemove = 2 + const key1Before = await nor.getSigningKey(operatorData.id, keyIdxToRemove) + const key2Before = await nor.getSigningKey(operatorData.id, keyIdxToRemove + 1) + + assert.equals(operatorBefore.stakingLimit, operatorData.vettedSigningKeysCount) + + await nor.removeSigningKeys(operatorData.id, keyIdxToRemove, keysCountToRemove, { from: voting.address }) + + const operatorAfter = await nor.getNodeOperator(operatorData.id, true) + const summaryAfter = await nor.getNodeOperatorSummary(operatorData.id) + const keysCountAfter = await nor.getTotalSigningKeyCount(operatorData.id) + const unusedKeysCountAfter = await nor.getUnusedSigningKeyCount(operatorData.id) + const key1After = await nor.getSigningKey(operatorData.id, keyIdxToRemove) + const key2After = await nor.getSigningKey(operatorData.id, keyIdxToRemove + 1) + + assert.equals(operatorAfter.stakingLimit, keyIdxToRemove) + assert.equals(+operatorBefore.totalSigningKeys - keysCountToRemove, +operatorAfter.totalSigningKeys) + assert.equals(+keysCountBefore - keysCountToRemove, +keysCountAfter) + assert.equals(+unusedKeysCountBefore - keysCountToRemove, +unusedKeysCountAfter) + assert.equals( + Math.min( + +summaryBefore.depositableValidatorsCount - keysCountToRemove, + +operatorAfter.stakingLimit - +operatorAfter.usedSigningKeys + ), + +summaryAfter.depositableValidatorsCount + ) + assert.notEqual(key1Before.key, key1After.key) + assert.notEqual(key2Before.key, key2After.key) + assert.notEqual(key1Before.depositSignature, key1After.depositSignature) + assert.notEqual(key2Before.depositSignature, key2After.depositSignature) + }) + + it('Refund stucked keys for Operator 2', async () => { + const operatorData = Operator2 + const penaltyDelay = await nor.getStuckPenaltyDelay() + + // StakingRouter.updateRefundedValidatorsCount() -> NOR.updateRefundedValidatorsCount() + const tx = await stakingRouter.updateRefundedValidatorsCount(curatedId, operatorData.id, 1, { + from: voting.address, + }) + + const timestamp = await getCurrentBlockTimestamp() + const expectedPenaltyEnd = +timestamp + +penaltyDelay + const summary = await nor.getNodeOperatorSummary(operatorData.id) + + assert.equals(summary.stuckPenaltyEndTimestamp, expectedPenaltyEnd) + assert.equals(summary.depositableValidatorsCount, 0) + assert.equals(summary.refundedValidatorsCount, 1) + + assert.emits( + tx, + 'StuckPenaltyStateChanged', + { + nodeOperatorId: operatorData.id, + stuckValidatorsCount: 1, + refundedValidatorsCount: 1, + stuckPenaltyEndTimestamp: expectedPenaltyEnd, + }, + NOR_ABI_ASSERT_EV + ) + }) + + it('Wait for half of penalty delay and check that penalty still applies', async () => { + const operatorData = Operator2 + const penaltyDelay = await nor.getStuckPenaltyDelay() + + await advanceChainTime(penaltyDelay / 2) + await assert.reverts(nor.clearNodeOperatorPenalty(operatorData.id), 'CANT_CLEAR_PENALTY') + + const summary = await nor.getNodeOperatorSummary(operatorData.id) + assert.equals(summary.depositableValidatorsCount, 0) + assert.equals(summary.refundedValidatorsCount, 1) + }) + + it('Wait for end of penalty delay and check that it is gone', async () => { + const operatorData = Operator2 + const penaltyDelay = await nor.getStuckPenaltyDelay() + + await advanceChainTime(penaltyDelay / 2 + 100) + await nor.clearNodeOperatorPenalty(operatorData.id) + + const summary = await nor.getNodeOperatorSummary(operatorData.id) + assert.equals(summary.depositableValidatorsCount, 9) + }) + }) + + context('Activation/deactivation', () => { + it('Deactivate operator 1', async () => { + await deactivateNodeOperator(Operator1.id) + }) + + it('Activate operator 4', async () => { + const operatorData = Operator4 + const activeOperatorsBefore = await nor.getActiveNodeOperatorsCount() + + const tx = await nor.activateNodeOperator(operatorData.id, { from: voting.address }) + + const operator = await nor.getNodeOperator(operatorData.id, true) + const activeOperatorsAfter = await nor.getActiveNodeOperatorsCount() + + assert.isTrue(operator.active) + assert.isTrue(await nor.getNodeOperatorIsActive(operatorData.id)) + assert.equals(+activeOperatorsBefore + 1, +activeOperatorsAfter) + assert.emits(tx, 'NodeOperatorActiveSet', { nodeOperatorId: operatorData.id, active: true }) + assert.equals(operator.stakingLimit, operator.usedSigningKeys) + }) + + it('Set operator 4 staking limit', async () => { + await setNodeOperatorStakingLimit(Operator4.id, Operator4.vettedSigningKeysCount) + }) + + it('Make another deposit', async () => { + /** + * Expected deposits fill 1 2 3 4 5 + * Operator 1 (inactive) [ ] + * Operator 2 (refunded) [ x x ] + * Operator 3 (stuck) [ ] + * Operator 4 (activated) [ x x x ] + */ + + await waitBlocks(+(await dsm.getMinDepositBlockDistance())) + await makeDeposit(5) + + // Continuing numbers from previous deposit + await assertDepositCall(6, Operator2.id, 1) + await assertDepositCall(7, Operator2.id, 2) + await assertDepositCall(8, Operator4.id, 0) + await assertDepositCall(9, Operator4.id, 1) + await assertDepositCall(10, Operator4.id, 2) + + await assertOperatorDeposits(Operator2, 3, 7) + await assertOperatorDeposits(Operator4, 3, 2) + }) + + // TODO: [optional] Make a report again if needed + }) + + context('Withdrawal credentials modifying', () => { + it('setWithdrawalCredentials', async () => { + const summaryBefore = await nor.getStakingModuleSummary() + withdrawalCredentials = '0x'.padEnd(66, '5678') + await stakingRouter.setWithdrawalCredentials(withdrawalCredentials, { from: voting.address }) + const summary = await nor.getStakingModuleSummary() + console.log(summary, summaryBefore) + + assert.equals(summaryBefore.totalExitedValidators, summary.totalExitedValidators) + assert.equals(summaryBefore.totalDepositedValidators, summary.totalDepositedValidators) + assert.notEqual(summary.depositableValidatorsCount, summaryBefore.depositableValidatorsCount) + assert.equals(summary.depositableValidatorsCount, 0) + }) + }) + + // TODO: [optional] assert NOR.getNonce() and NonceChanged event if needed + }) +}) diff --git a/test/0.4.24/node-operators-registry.test.js b/test/0.4.24/node-operators-registry.test.js index d9d3e602a..ffbb88620 100644 --- a/test/0.4.24/node-operators-registry.test.js +++ b/test/0.4.24/node-operators-registry.test.js @@ -1519,10 +1519,15 @@ contract('NodeOperatorsRegistry', (addresses) => { const targetLimit = 10 const isTargetLimitSet = true - await app.updateTargetValidatorsLimits(firstNodeOperatorId, isTargetLimitSet, targetLimit, { + const tx = await app.updateTargetValidatorsLimits(firstNodeOperatorId, isTargetLimitSet, targetLimit, { from: stakingRouter, }) + assert.emits(tx, 'TargetValidatorsCountChanged', { + nodeOperatorId: firstNodeOperatorId, + targetValidatorsCount: targetLimit, + }) + const keysStatTotal = await app.getStakingModuleSummary() const expectedExitedValidatorsCount = NODE_OPERATORS[firstNodeOperatorId].exitedSigningKeysCount + diff --git a/test/0.8.9/deposit-security-module.test.js b/test/0.8.9/deposit-security-module.test.js index 5ed520c05..1e6ccc061 100644 --- a/test/0.8.9/deposit-security-module.test.js +++ b/test/0.8.9/deposit-security-module.test.js @@ -48,7 +48,7 @@ contract('DepositSecurityModule', ([owner, stranger, guardian]) => { before('deploy mock contracts', async () => { lidoMock = await LidoMockForDepositSecurityModule.new() - stakingRouterMock = await StakingRouterMockForDepositSecurityModule.new() + stakingRouterMock = await StakingRouterMockForDepositSecurityModule.new(STAKING_MODULE) depositContractMock = await DepositContractMockForDepositSecurityModule.new() depositSecurityModule = await DepositSecurityModule.new( @@ -1042,6 +1042,33 @@ contract('DepositSecurityModule', ([owner, stranger, guardian]) => { assert.isTrue(currentBlockNumber - lastDepositBlockNumber >= minDepositBlockDistance) assert.isTrue(await depositSecurityModule.canDeposit(STAKING_MODULE)) }) + it('false if unknown staking module id', async () => { + await depositSecurityModule.addGuardian(GUARDIAN1, 1, { from: owner }) + + await assert.reverts( + stakingRouterMock.getStakingModuleIsDepositsPaused(STAKING_MODULE + 1), + `StakingModuleUnregistered()` + ) + await assert.reverts( + stakingRouterMock.getStakingModuleIsActive(STAKING_MODULE + 1), + `StakingModuleUnregistered()` + ) + await assert.reverts( + stakingRouterMock.getStakingModuleLastDepositBlock(STAKING_MODULE + 1), + `StakingModuleUnregistered()` + ) + assert.isTrue((await depositSecurityModule.getGuardianQuorum()) > 0, 'invariant failed: quorum > 0') + + const lastDepositBlockNumber = await web3.eth.getBlockNumber() + await stakingRouterMock.setStakingModuleLastDepositBlock(lastDepositBlockNumber) + await waitBlocks(2 * MIN_DEPOSIT_BLOCK_DISTANCE) + + const currentBlockNumber = await web3.eth.getBlockNumber() + const minDepositBlockDistance = await depositSecurityModule.getMinDepositBlockDistance() + + assert.isTrue(currentBlockNumber - lastDepositBlockNumber >= minDepositBlockDistance) + assert.isFalse(await depositSecurityModule.canDeposit(STAKING_MODULE + 1)) + }) it('false if paused and quorum > 0 and currentBlock - lastDepositBlock >= minDepositBlockDistance', async () => { await depositSecurityModule.addGuardians([GUARDIAN1, guardian], 1, { from: owner }) assert.isTrue((await depositSecurityModule.getGuardianQuorum()) > 0, 'invariant failed: quorum > 0') diff --git a/test/0.8.9/oracle-report-sanity-checker.test.js b/test/0.8.9/oracle-report-sanity-checker.test.js index a3ef5b8d5..efe1bea14 100644 --- a/test/0.8.9/oracle-report-sanity-checker.test.js +++ b/test/0.8.9/oracle-report-sanity-checker.test.js @@ -1,7 +1,7 @@ const { artifacts, contract, ethers } = require('hardhat') const { ETH } = require('../helpers/utils') const { assert } = require('../helpers/assert') -const { getCurrentBlockTimestamp } = require('../helpers/blockchain') +const { getCurrentBlockTimestamp, EvmSnapshot } = require('../helpers/blockchain') const mocksFilePath = 'contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol' const LidoStub = artifacts.require(`${mocksFilePath}:LidoStub`) @@ -22,7 +22,7 @@ function wei(number, units = 'wei') { } contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewardsVault, ...accounts]) => { - let oracleReportSanityChecker, lidoLocatorMock, lidoMock, withdrawalQueueMock, burnerMock + let oracleReportSanityChecker, lidoLocatorMock, lidoMock, withdrawalQueueMock, burnerMock, snapshot const managersRoster = { allLimitsManagers: accounts.slice(0, 2), churnValidatorsPerDayLimitManagers: accounts.slice(2, 4), @@ -81,6 +81,19 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa from: deployer, } ) + + snapshot = new EvmSnapshot(ethers.provider) + await snapshot.make() + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + describe('getLidoLocator()', () => { + it('retrieves correct locator address', async () => { + assert.equals(await oracleReportSanityChecker.getLidoLocator(), lidoLocatorMock.address) + }) }) describe('setOracleReportLimits()', () => { @@ -116,6 +129,13 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa assert.notEquals(limitsBefore.requestTimestampMargin, newLimitsList.requestTimestampMargin) assert.notEquals(limitsBefore.maxPositiveTokenRebase, newLimitsList.maxPositiveTokenRebase) + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setOracleReportLimits(Object.values(newLimitsList), { + from: deployer, + }), + deployer, + 'ALL_LIMITS_MANAGER_ROLE' + ) await oracleReportSanityChecker.setOracleReportLimits(Object.values(newLimitsList), { from: managersRoster.allLimitsManagers[0], }) @@ -148,7 +168,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa it('reverts with error IncorrectWithdrawalsVaultBalance() when actual withdrawal vault balance is less than passed', async () => { const currentWithdrawalVaultBalance = await ethers.provider.getBalance(withdrawalVault) - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkAccountingOracleReport( ...Object.values({ ...correctLidoOracleReport, withdrawalVaultBalance: currentWithdrawalVaultBalance.add(1) }) ), @@ -158,7 +178,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa it('reverts with error IncorrectELRewardsVaultBalance() when actual el rewards vault balance is less than passed', async () => { const currentELRewardsVaultBalance = await ethers.provider.getBalance(elRewardsVault) - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkAccountingOracleReport( ...Object.values({ ...correctLidoOracleReport, elRewardsVaultBalance: currentELRewardsVaultBalance.add(1) }) ), @@ -166,6 +186,17 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa ) }) + it('reverts with error IncorrectSharesRequestedToBurn() when actual shares to burn is less than passed', async () => { + await burnerMock.setSharesRequestedToBurn(10, 21) + + await assert.reverts( + oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ ...correctLidoOracleReport, sharesRequestedToBurn: 32 }) + ), + `IncorrectSharesRequestedToBurn(31)` + ) + }) + it('reverts with error IncorrectCLBalanceDecrease() when one off CL balance decrease more than limit', async () => { const maxBasisPoints = 10_000n const preCLBalance = wei(100_000, 'eth') @@ -173,7 +204,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa const withdrawalVaultBalance = wei(500, 'eth') const unifiedPostCLBalance = postCLBalance + withdrawalVaultBalance const oneOffCLBalanceDecreaseBP = (maxBasisPoints * (preCLBalance - unifiedPostCLBalance)) / preCLBalance - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkAccountingOracleReport( ...Object.values({ ...correctLidoOracleReport, @@ -184,6 +215,16 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa ), `IncorrectCLBalanceDecrease(${oneOffCLBalanceDecreaseBP.toString()})` ) + + const postCLBalanceCorrect = wei(99_000, 'eth') + await oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ + ...correctLidoOracleReport, + preCLBalance: preCLBalance.toString(), + postCLBalance: postCLBalanceCorrect.toString(), + withdrawalVaultBalance: withdrawalVaultBalance.toString(), + }) + ) }) it('reverts with error IncorrectCLBalanceIncrease() when reported values overcome annual CL balance limit', async () => { @@ -194,7 +235,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa const timeElapsed = BigInt(correctLidoOracleReport.timeElapsed) const annualBalanceIncrease = (secondsInOneYear * maxBasisPoints * (postCLBalance - preCLBalance)) / preCLBalance / timeElapsed - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkAccountingOracleReport( ...Object.values({ ...correctLidoOracleReport, @@ -209,36 +250,105 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa await oracleReportSanityChecker.checkAccountingOracleReport(...Object.values(correctLidoOracleReport)) }) - it('set maxAccountingExtraDataListItemsCount', async () => { - const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()) - .maxAccountingExtraDataListItemsCount - const newValue = 31 + it('set one-off CL balance decrease', async () => { + const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()).oneOffCLBalanceDecreaseBPLimit + const newValue = 3 assert.notEquals(newValue, previousValue) - await oracleReportSanityChecker.setMaxAccountingExtraDataListItemsCount(newValue, { - from: managersRoster.maxAccountingExtraDataListItemsCountManagers[0], + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setOneOffCLBalanceDecreaseBPLimit(newValue, { + from: deployer, + }), + deployer, + 'ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE' + ) + const tx = await oracleReportSanityChecker.setOneOffCLBalanceDecreaseBPLimit(newValue, { + from: managersRoster.oneOffCLBalanceDecreaseLimitManagers[0], }) - assert.equals( - (await oracleReportSanityChecker.getOracleReportLimits()).maxAccountingExtraDataListItemsCount, - newValue + assert.equals((await oracleReportSanityChecker.getOracleReportLimits()).oneOffCLBalanceDecreaseBPLimit, newValue) + assert.emits(tx, 'OneOffCLBalanceDecreaseBPLimitSet', { oneOffCLBalanceDecreaseBPLimit: newValue }) + }) + + it('set annual balance increase', async () => { + const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()).annualBalanceIncreaseBPLimit + const newValue = 9 + assert.notEquals(newValue, previousValue) + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setAnnualBalanceIncreaseBPLimit(newValue, { + from: deployer, + }), + deployer, + 'ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE' ) + const tx = await oracleReportSanityChecker.setAnnualBalanceIncreaseBPLimit(newValue, { + from: managersRoster.annualBalanceIncreaseLimitManagers[0], + }) + assert.equals((await oracleReportSanityChecker.getOracleReportLimits()).annualBalanceIncreaseBPLimit, newValue) + assert.emits(tx, 'AnnualBalanceIncreaseBPLimitSet', { annualBalanceIncreaseBPLimit: newValue }) }) - it('set maxNodeOperatorsPerExtraDataItemCount', async () => { - const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()) - .maxNodeOperatorsPerExtraDataItemCount - const newValue = 33 + it('handles zero time passed for annual balance increase', async () => { + const preCLBalance = BigInt(correctLidoOracleReport.preCLBalance) + const postCLBalance = preCLBalance + 1000n + + await oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ + ...correctLidoOracleReport, + postCLBalance: postCLBalance.toString(), + timeElapsed: 0, + }) + ) + }) + + it('handles zero pre CL balance estimating balance increase', async () => { + const preCLBalance = BigInt(0) + const postCLBalance = preCLBalance + 1000n + + await oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ + ...correctLidoOracleReport, + preCLBalance: preCLBalance.toString(), + postCLBalance: postCLBalance.toString(), + }) + ) + }) + + it('handles zero time passed for appeared validators', async () => { + const preCLValidators = BigInt(correctLidoOracleReport.preCLValidators) + const postCLValidators = preCLValidators + 2n + + await oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ + ...correctLidoOracleReport, + preCLValidators: preCLValidators.toString(), + postCLValidators: postCLValidators.toString(), + timeElapsed: 0, + }) + ) + }) + + it('set simulated share rate deviation', async () => { + const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()).simulatedShareRateDeviationBPLimit + const newValue = 7 assert.notEquals(newValue, previousValue) - await oracleReportSanityChecker.setMaxNodeOperatorsPerExtraDataItemCount(newValue, { - from: managersRoster.maxNodeOperatorsPerExtraDataItemCountManagers[0], + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setSimulatedShareRateDeviationBPLimit(newValue, { + from: deployer, + }), + deployer, + 'SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE' + ) + const tx = await oracleReportSanityChecker.setSimulatedShareRateDeviationBPLimit(newValue, { + from: managersRoster.shareRateDeviationLimitManagers[0], }) assert.equals( - (await oracleReportSanityChecker.getOracleReportLimits()).maxNodeOperatorsPerExtraDataItemCount, + (await oracleReportSanityChecker.getOracleReportLimits()).simulatedShareRateDeviationBPLimit, newValue ) + assert.emits(tx, 'SimulatedShareRateDeviationBPLimitSet', { simulatedShareRateDeviationBPLimit: newValue }) }) }) - describe('checkWithdrawalQueueOracleReport()', async () => { + describe('checkWithdrawalQueueOracleReport()', () => { const oldRequestId = 1 const newRequestId = 2 let oldRequestCreationTimestamp, newRequestCreationTimestamp @@ -258,7 +368,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa }) it('reverts with the error IncorrectRequestFinalization() when the creation timestamp of requestIdToFinalizeUpTo is too close to report timestamp', async () => { - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkWithdrawalQueueOracleReport( ...Object.values({ ...correctWithdrawalQueueOracleReport, @@ -274,9 +384,27 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa ...Object.values(correctWithdrawalQueueOracleReport) ) }) + + it('set timestamp margin for finalization', async () => { + const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()).requestTimestampMargin + const newValue = 3302 + assert.notEquals(newValue, previousValue) + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setRequestTimestampMargin(newValue, { + from: deployer, + }), + deployer, + 'REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE' + ) + const tx = await oracleReportSanityChecker.setRequestTimestampMargin(newValue, { + from: managersRoster.requestTimestampMarginManagers[0], + }) + assert.equals((await oracleReportSanityChecker.getOracleReportLimits()).requestTimestampMargin, newValue) + assert.emits(tx, 'RequestTimestampMarginSet', { requestTimestampMargin: newValue }) + }) }) - describe('checkSimulatedShareRate', async () => { + describe('checkSimulatedShareRate', () => { const correctSimulatedShareRate = { postTotalPooledEther: ETH(9), postTotalShares: ETH(4), @@ -288,7 +416,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa it('reverts with error TooHighSimulatedShareRate() when reported and onchain share rate differs', async () => { const simulatedShareRate = BigInt(ETH(2.1)) * 10n ** 9n const actualShareRate = BigInt(2) * 10n ** 27n - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkSimulatedShareRate( ...Object.values({ ...correctSimulatedShareRate, @@ -302,7 +430,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa it('reverts with error TooLowSimulatedShareRate() when reported and onchain share rate differs', async () => { const simulatedShareRate = BigInt(ETH(1.9)) * 10n ** 9n const actualShareRate = BigInt(2) * 10n ** 27n - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkSimulatedShareRate( ...Object.values({ ...correctSimulatedShareRate, @@ -314,7 +442,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa }) it('reverts with error ActualShareRateIsZero() when actual share rate is zero', async () => { - await assert.revertsWithCustomError( + await assert.reverts( oracleReportSanityChecker.checkSimulatedShareRate( ...Object.values({ ...correctSimulatedShareRate, @@ -330,4 +458,769 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa await oracleReportSanityChecker.checkSimulatedShareRate(...Object.values(correctSimulatedShareRate)) }) }) + + describe('max positive rebase', () => { + const defaultSmoothenTokenRebaseParams = { + preTotalPooledEther: ETH(100), + preTotalShares: ETH(100), + preCLBalance: ETH(100), + postCLBalance: ETH(100), + withdrawalVaultBalance: 0, + elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, + etherToLockForWithdrawals: 0, + newSharesToBurnForWithdrawals: 0, + } + + it('getMaxPositiveTokenRebase works', async () => { + assert.equals( + await oracleReportSanityChecker.getMaxPositiveTokenRebase(), + defaultLimitsList.maxPositiveTokenRebase + ) + }) + + it('setMaxPositiveTokenRebase works', async () => { + const newRebaseLimit = 1_000_000 + assert.notEquals(newRebaseLimit, defaultLimitsList.maxPositiveTokenRebase) + + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { from: deployer }), + deployer, + 'MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE' + ) + + const tx = await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + assert.equals(await oracleReportSanityChecker.getMaxPositiveTokenRebase(), newRebaseLimit) + assert.emits(tx, 'MaxPositiveTokenRebaseSet', { maxPositiveTokenRebase: newRebaseLimit }) + }) + + it('all zero data works', async () => { + const { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + preTotalPooledEther: 0, + preTotalShares: 0, + preCLBalance: 0, + postCLBalance: 0, + }) + ) + + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + }) + + it('trivial smoothen rebase works when post CL < pre CL and no withdrawals', async () => { + const newRebaseLimit = 100_000 // 0.01% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + let { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ ...defaultSmoothenTokenRebaseParams, postCLBalance: ETH(99) }) + ) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + elRewardsVaultBalance: ETH(0.1), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, ETH(0.1)) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // withdrawals + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + withdrawalVaultBalance: ETH(0.1), + }) + )) + assert.equals(withdrawals, ETH(0.1)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // shares requested to burn + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + sharesRequestedToBurn: ETH(0.1), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, ETH(0.1)) + assert.equals(sharesToBurn, ETH(0.1)) + }) + + it('trivial smoothen rebase works when post CL > pre CL and no withdrawals', async () => { + const newRebaseLimit = 100_000_000 // 10% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + let { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ ...defaultSmoothenTokenRebaseParams, postCLBalance: ETH(100.01) }) + ) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(100.01), + elRewardsVaultBalance: ETH(0.1), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, ETH(0.1)) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // withdrawals + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(100.01), + withdrawalVaultBalance: ETH(0.1), + }) + )) + assert.equals(withdrawals, ETH(0.1)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // shares requested to burn + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(100.01), + sharesRequestedToBurn: ETH(0.1), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, ETH(0.1)) + assert.equals(sharesToBurn, ETH(0.1)) + }) + + it('non-trivial smoothen rebase works when post CL < pre CL and no withdrawals', async () => { + const newRebaseLimit = 10_000_000 // 1% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + let { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ ...defaultSmoothenTokenRebaseParams, postCLBalance: ETH(99) }) + ) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, ETH(2)) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // withdrawals + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + withdrawalVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(2)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // withdrawals + el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + withdrawalVaultBalance: ETH(5), + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(2)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // shares requested to burn + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + sharesRequestedToBurn: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, '1980198019801980198') // ETH(100. - (99. / 1.01)) + assert.equals(sharesToBurn, '1980198019801980198') // the same as above since no withdrawals + }) + + it('non-trivial smoothen rebase works when post CL > pre CL and no withdrawals', async () => { + const newRebaseLimit = 20_000_000 // 2% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + let { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ ...defaultSmoothenTokenRebaseParams, postCLBalance: ETH(101) }) + ) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(101), + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, ETH(1)) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // withdrawals + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(101), + withdrawalVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(1)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // withdrawals + el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(101), + elRewardsVaultBalance: ETH(5), + withdrawalVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(1)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, 0) + // shares requested to burn + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(101), + sharesRequestedToBurn: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, '980392156862745098') // ETH(100. - (101. / 1.02)) + assert.equals(sharesToBurn, '980392156862745098') // the same as above since no withdrawals + }) + + it('non-trivial smoothen rebase works when post CL < pre CL and withdrawals', async () => { + const newRebaseLimit = 5_000_000 // 0.5% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + const defaultRebaseParams = { + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(99), + etherToLockForWithdrawals: ETH(10), + newSharesToBurnForWithdrawals: ETH(10), + } + + let { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase(...Object.values(defaultRebaseParams)) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, ETH(10)) + // el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, ETH(1.5)) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, '9950248756218905472') // 100. - 90.5 / 1.005 + // withdrawals + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + withdrawalVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(1.5)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, '9950248756218905472') // 100. - 90.5 / 1.005 + // withdrawals + el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + withdrawalVaultBalance: ETH(5), + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(1.5)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, '9950248756218905472') // 100. - 90.5 / 1.005 + // shares requested to burn + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + sharesRequestedToBurn: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, '1492537313432835820') // ETH(100. - (99. / 1.005)) + assert.equals(sharesToBurn, '11442786069651741293') // ETH(100. - (89. / 1.005)) + }) + + it('non-trivial smoothen rebase works when post CL > pre CL and withdrawals', async () => { + const newRebaseLimit = 40_000_000 // 4% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + const defaultRebaseParams = { + ...defaultSmoothenTokenRebaseParams, + postCLBalance: ETH(102), + etherToLockForWithdrawals: ETH(10), + newSharesToBurnForWithdrawals: ETH(10), + } + + let { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase(...Object.values(defaultRebaseParams)) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, ETH(10)) + // el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, ETH(2)) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, '9615384615384615384') // 100. - 94. / 1.04 + // withdrawals + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + withdrawalVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(2)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, '9615384615384615384') // 100. - 94. / 1.04 + // withdrawals + el rewards + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + withdrawalVaultBalance: ETH(5), + elRewardsVaultBalance: ETH(5), + }) + )) + assert.equals(withdrawals, ETH(2)) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, 0) + assert.equals(sharesToBurn, '9615384615384615384') // 100. - 94. / 1.04 + // shares requested to burn + ;({ withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase( + ...Object.values({ + ...defaultRebaseParams, + sharesRequestedToBurn: ETH(5), + }) + )) + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + assert.equals(simulatedSharesToBurn, '1923076923076923076') // ETH(100. - (102. / 1.04)) + assert.equals(sharesToBurn, '11538461538461538461') // ETH(100. - (92. / 1.04)) + }) + + it('share rate ~1 case with huge withdrawal', async () => { + const newRebaseLimit = 1_000_000 // 0.1% + await oracleReportSanityChecker.setMaxPositiveTokenRebase(newRebaseLimit, { + from: managersRoster.maxPositiveTokenRebaseManagers[0], + }) + + const rebaseParams = { + preTotalPooledEther: ETH('1000000'), + preTotalShares: ETH('1000000'), + preCLBalance: ETH('1000000'), + postCLBalance: ETH('1000000'), + elRewardsVaultBalance: ETH(500), + withdrawalVaultBalance: ETH(500), + sharesRequestedToBurn: ETH(0), + etherToLockForWithdrawals: ETH(40000), + newSharesToBurnForWithdrawals: ETH(40000), + } + + const { withdrawals, elRewards, simulatedSharesToBurn, sharesToBurn } = + await oracleReportSanityChecker.smoothenTokenRebase(...Object.values(rebaseParams)) + + assert.equals(withdrawals, ETH(500)) + assert.equals(elRewards, ETH(500)) + assert.equals(simulatedSharesToBurn, ETH(0)) + assert.equals(sharesToBurn, '39960039960039960039960') // ETH(1000000 - 961000. / 1.001) + }) + }) + + describe('churn limit', () => { + it('setChurnValidatorsPerDayLimit works', async () => { + const oldChurnLimit = defaultLimitsList.churnValidatorsPerDayLimit + await oracleReportSanityChecker.checkExitedValidatorsRatePerDay(oldChurnLimit) + await assert.reverts( + oracleReportSanityChecker.checkExitedValidatorsRatePerDay(oldChurnLimit + 1), + `ExitedValidatorsLimitExceeded(${oldChurnLimit}, ${oldChurnLimit + 1})` + ) + assert.equals((await oracleReportSanityChecker.getOracleReportLimits()).churnValidatorsPerDayLimit, oldChurnLimit) + + const newChurnLimit = 30 + assert.notEquals(newChurnLimit, oldChurnLimit) + + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setChurnValidatorsPerDayLimit(newChurnLimit, { from: deployer }), + deployer, + 'CHURN_VALIDATORS_PER_DAY_LIMIT_MANGER_ROLE' + ) + + const tx = await oracleReportSanityChecker.setChurnValidatorsPerDayLimit(newChurnLimit, { + from: managersRoster.churnValidatorsPerDayLimitManagers[0], + }) + + assert.emits(tx, 'ChurnValidatorsPerDayLimitSet', { churnValidatorsPerDayLimit: newChurnLimit }) + assert.equals((await oracleReportSanityChecker.getOracleReportLimits()).churnValidatorsPerDayLimit, newChurnLimit) + + await oracleReportSanityChecker.checkExitedValidatorsRatePerDay(newChurnLimit) + await assert.reverts( + oracleReportSanityChecker.checkExitedValidatorsRatePerDay(newChurnLimit + 1), + `ExitedValidatorsLimitExceeded(${newChurnLimit}, ${newChurnLimit + 1})` + ) + }) + + it('checkAccountingOracleReport: churnLimit works', async () => { + const churnLimit = defaultLimitsList.churnValidatorsPerDayLimit + assert.equals((await oracleReportSanityChecker.getOracleReportLimits()).churnValidatorsPerDayLimit, churnLimit) + + await oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ ...correctLidoOracleReport, postCLValidators: churnLimit }) + ) + await assert.reverts( + oracleReportSanityChecker.checkAccountingOracleReport( + ...Object.values({ + ...correctLidoOracleReport, + postCLValidators: churnLimit + 1, + }) + ), + `IncorrectAppearedValidators(${churnLimit + 1})` + ) + }) + }) + + describe('checkExitBusOracleReport', () => { + beforeEach(async () => { + await oracleReportSanityChecker.setOracleReportLimits(Object.values(defaultLimitsList), { + from: managersRoster.allLimitsManagers[0], + }) + }) + + it('checkExitBusOracleReport works', async () => { + const maxRequests = defaultLimitsList.maxValidatorExitRequestsPerReport + assert.equals( + (await oracleReportSanityChecker.getOracleReportLimits()).maxValidatorExitRequestsPerReport, + maxRequests + ) + + await oracleReportSanityChecker.checkExitBusOracleReport(maxRequests) + await assert.reverts( + oracleReportSanityChecker.checkExitBusOracleReport(maxRequests + 1), + `IncorrectNumberOfExitRequestsPerReport(${maxRequests})` + ) + }) + + it('setMaxExitRequestsPerOracleReport', async () => { + const oldMaxRequests = defaultLimitsList.maxValidatorExitRequestsPerReport + await oracleReportSanityChecker.checkExitBusOracleReport(oldMaxRequests) + await assert.reverts( + oracleReportSanityChecker.checkExitBusOracleReport(oldMaxRequests + 1), + `IncorrectNumberOfExitRequestsPerReport(${oldMaxRequests})` + ) + assert.equals( + (await oracleReportSanityChecker.getOracleReportLimits()).maxValidatorExitRequestsPerReport, + oldMaxRequests + ) + + const newMaxRequests = 306 + assert.notEquals(newMaxRequests, oldMaxRequests) + + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setMaxExitRequestsPerOracleReport(newMaxRequests, { from: deployer }), + deployer, + 'MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE' + ) + + const tx = await oracleReportSanityChecker.setMaxExitRequestsPerOracleReport(newMaxRequests, { + from: managersRoster.maxValidatorExitRequestsPerReportManagers[0], + }) + + assert.emits(tx, 'MaxValidatorExitRequestsPerReportSet', { maxValidatorExitRequestsPerReport: newMaxRequests }) + assert.equals( + (await oracleReportSanityChecker.getOracleReportLimits()).maxValidatorExitRequestsPerReport, + newMaxRequests + ) + + await oracleReportSanityChecker.checkExitBusOracleReport(newMaxRequests) + await assert.reverts( + oracleReportSanityChecker.checkExitBusOracleReport(newMaxRequests + 1), + `IncorrectNumberOfExitRequestsPerReport(${newMaxRequests})` + ) + }) + }) + + describe('extra data reporting', () => { + beforeEach(async () => { + await oracleReportSanityChecker.setOracleReportLimits(Object.values(defaultLimitsList), { + from: managersRoster.allLimitsManagers[0], + }) + }) + + it('set maxNodeOperatorsPerExtraDataItemCount', async () => { + const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()) + .maxNodeOperatorsPerExtraDataItemCount + const newValue = 33 + assert.notEquals(newValue, previousValue) + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setMaxNodeOperatorsPerExtraDataItemCount(newValue, { + from: deployer, + }), + deployer, + 'MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_COUNT_ROLE' + ) + const tx = await oracleReportSanityChecker.setMaxNodeOperatorsPerExtraDataItemCount(newValue, { + from: managersRoster.maxNodeOperatorsPerExtraDataItemCountManagers[0], + }) + assert.equals( + (await oracleReportSanityChecker.getOracleReportLimits()).maxNodeOperatorsPerExtraDataItemCount, + newValue + ) + assert.emits(tx, 'MaxNodeOperatorsPerExtraDataItemCountSet', { maxNodeOperatorsPerExtraDataItemCount: newValue }) + }) + + it('set maxAccountingExtraDataListItemsCount', async () => { + const previousValue = (await oracleReportSanityChecker.getOracleReportLimits()) + .maxAccountingExtraDataListItemsCount + const newValue = 31 + assert.notEquals(newValue, previousValue) + await assert.revertsOZAccessControl( + oracleReportSanityChecker.setMaxAccountingExtraDataListItemsCount(newValue, { + from: deployer, + }), + deployer, + 'MAX_ACCOUNTING_EXTRA_DATA_LIST_ITEMS_COUNT_ROLE' + ) + const tx = await oracleReportSanityChecker.setMaxAccountingExtraDataListItemsCount(newValue, { + from: managersRoster.maxAccountingExtraDataListItemsCountManagers[0], + }) + assert.equals( + (await oracleReportSanityChecker.getOracleReportLimits()).maxAccountingExtraDataListItemsCount, + newValue + ) + assert.emits(tx, 'MaxAccountingExtraDataListItemsCountSet', { maxAccountingExtraDataListItemsCount: newValue }) + }) + + it('checkNodeOperatorsPerExtraDataItemCount', async () => { + const maxCount = (await oracleReportSanityChecker.getOracleReportLimits()).maxNodeOperatorsPerExtraDataItemCount + + await oracleReportSanityChecker.checkNodeOperatorsPerExtraDataItemCount(12, maxCount) + + await assert.reverts( + oracleReportSanityChecker.checkNodeOperatorsPerExtraDataItemCount(12, +maxCount + 1), + `TooManyNodeOpsPerExtraDataItem(12, ${+maxCount + 1})` + ) + }) + + it('checkAccountingExtraDataListItemsCount', async () => { + const maxCount = (await oracleReportSanityChecker.getOracleReportLimits()).maxAccountingExtraDataListItemsCount + + await oracleReportSanityChecker.checkAccountingExtraDataListItemsCount(maxCount) + + await assert.reverts( + oracleReportSanityChecker.checkAccountingExtraDataListItemsCount(maxCount + 1), + `MaxAccountingExtraDataItemsCountExceeded(${maxCount}, ${maxCount + 1})` + ) + }) + }) + + describe('check limit boundaries', () => { + it('values must be less or equal to MAX_BASIS_POINTS', async () => { + const MAX_BASIS_POINTS = 10000 + const INVALID_BASIS_POINTS = MAX_BASIS_POINTS + 1 + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, oneOffCLBalanceDecreaseBPLimit: INVALID_BASIS_POINTS }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_BASIS_POINTS}, ${MAX_BASIS_POINTS})` + ) + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, annualBalanceIncreaseBPLimit: 10001 }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_BASIS_POINTS}, ${MAX_BASIS_POINTS})` + ) + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, simulatedShareRateDeviationBPLimit: 10001 }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_BASIS_POINTS}, ${MAX_BASIS_POINTS})` + ) + }) + + it('values must be less or equal to type(uint16).max', async () => { + const MAX_UINT_16 = 65535 + const INVALID_VALUE = MAX_UINT_16 + 1 + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, churnValidatorsPerDayLimit: INVALID_VALUE }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_VALUE}, ${MAX_UINT_16})` + ) + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, maxValidatorExitRequestsPerReport: INVALID_VALUE }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_VALUE}, ${MAX_UINT_16})` + ) + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, maxAccountingExtraDataListItemsCount: INVALID_VALUE }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_VALUE}, ${MAX_UINT_16})` + ) + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, maxNodeOperatorsPerExtraDataItemCount: INVALID_VALUE }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_VALUE}, ${MAX_UINT_16})` + ) + }) + + it('values must be less or equals to type(uint64).max', async () => { + const MAX_UINT_64 = BigInt(2) ** 64n - 1n + const INVALID_VALUE = MAX_UINT_64 + 1n + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, requestTimestampMargin: INVALID_VALUE.toString() }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_VALUE.toString()}, ${MAX_UINT_64.toString()})` + ) + + await assert.reverts( + oracleReportSanityChecker.setOracleReportLimits( + Object.values({ ...defaultLimitsList, maxPositiveTokenRebase: INVALID_VALUE.toString() }), + { + from: managersRoster.allLimitsManagers[0], + } + ), + `IncorrectLimitValue(${INVALID_VALUE.toString()}, ${MAX_UINT_64.toString()})` + ) + }) + }) }) diff --git a/test/0.8.9/oracle/accounting-oracle-deploy.test.js b/test/0.8.9/oracle/accounting-oracle-deploy.test.js index 1815151c9..4e603cf3b 100644 --- a/test/0.8.9/oracle/accounting-oracle-deploy.test.js +++ b/test/0.8.9/oracle/accounting-oracle-deploy.test.js @@ -104,6 +104,7 @@ async function deployAccountingOracleSetup( getLegacyOracle = deployMockLegacyOracle, lidoLocatorAddr: lidoLocatorAddrArg, legacyOracleAddr: legacyOracleAddrArg, + lidoAddr: lidoAddrArg, } = {} ) { const locatorAddr = (await deployLocatorWithDummyAddressesImplementation(admin)).address @@ -118,7 +119,7 @@ async function deployAccountingOracleSetup( const oracle = await AccountingOracle.new( lidoLocatorAddrArg || locatorAddr, - lido.address, + lidoAddrArg || lido.address, legacyOracleAddrArg || legacyOracle.address, secondsPerSlot, genesisTime, @@ -134,7 +135,7 @@ async function deployAccountingOracleSetup( initialEpoch, }) await updateLocatorImplementation(locatorAddr, admin, { - lido: lido.address, + lido: lidoAddrArg || lido.address, stakingRouter: stakingRouter.address, withdrawalQueue: withdrawalQueue.address, oracleReportSanityChecker: oracleReportSanityChecker.address, @@ -449,6 +450,10 @@ contract('AccountingOracle', ([admin, member1]) => { ) }) + it('constructor reverts if lido address is zero', async () => { + await assert.reverts(deployAccountingOracleSetup(admin, { lidoAddr: ZERO_ADDRESS }), 'LidoCannotBeZero()') + }) + it('initialize reverts if admin address is zero', async () => { const deployed = await deployAccountingOracleSetup(admin) await updateInitialEpoch(deployed.consensus) diff --git a/test/0.8.9/oracle/accounting-oracle-submit-report-data.test.js b/test/0.8.9/oracle/accounting-oracle-submit-report-data.test.js index 7778696fb..fb91dc5fa 100644 --- a/test/0.8.9/oracle/accounting-oracle-submit-report-data.test.js +++ b/test/0.8.9/oracle/accounting-oracle-submit-report-data.test.js @@ -379,27 +379,6 @@ contract('AccountingOracle', ([admin, member1]) => { ) }) - it('reverts with NumExitedValidatorsCannotDecrease if total count of exited validators less then previous exited number', async () => { - const totalExitedValidators = reportFields.numExitedValidatorsByStakingModule.reduce( - (sum, curr) => sum + curr, - 0 - ) - await mockStakingRouter.setExitedKeysCountAcrossAllModules(totalExitedValidators + 1) - await assert.reverts( - oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), - 'NumExitedValidatorsCannotDecrease()' - ) - }) - - it('does not reverts with NumExitedValidatorsCannotDecrease if total count of exited validators equals to previous exited number', async () => { - const totalExitedValidators = reportFields.numExitedValidatorsByStakingModule.reduce( - (sum, curr) => sum + curr, - 0 - ) - await mockStakingRouter.setExitedKeysCountAcrossAllModules(totalExitedValidators) - await oracle.submitReportData(reportItems, oracleVersion, { from: member1 }) - }) - it('reverts with ExitedValidatorsLimitExceeded if exited validators rate limit will be reached', async () => { // Really simple test here for now // TODO: Come up with more tests for better coverage of edge-case scenarios that can be accrued diff --git a/test/0.8.9/ossifiable-proxy.test.js b/test/0.8.9/ossifiable-proxy.test.js new file mode 100644 index 000000000..68925cdce --- /dev/null +++ b/test/0.8.9/ossifiable-proxy.test.js @@ -0,0 +1,233 @@ +const { assert } = require('../helpers/assert') +const { contract, artifacts, network } = require('hardhat') + +const TruffleContract = require('@truffle/contract') +const { ContractStub } = require('../helpers/contract-stub') +const { ZERO_ADDRESS } = require('../helpers/constants') +const { EvmSnapshot } = require('../helpers/blockchain') + +const InitializableABI = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'version', + type: 'uint256', + }, + ], + name: 'Initialized', + type: 'event', + }, + { + anonymous: false, + inputs: [], + name: 'ReceiveCalled', + type: 'event', + }, + { + inputs: [ + { + internalType: 'uint8', + name: 'version_', + type: 'uint8', + }, + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'version', + outputs: [ + { + internalType: 'uint8', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] + +const OssifiableProxy = artifacts.require('OssifiableProxy') +const InitializableContract = TruffleContract({ abi: InitializableABI }) + +contract('OssifiableProxy', ([deployer, admin, stranger]) => { + let currentImpl, newImpl, proxy, proxiedImpl + const snapshot = new EvmSnapshot(network.provider) + + before(async () => { + InitializableContract.setProvider(network.provider) + currentImpl = await ContractStub(InitializableContract).create({ from: deployer }) + newImpl = await ContractStub(InitializableContract) + .frame(0) + .on('receive', { emits: [{ name: 'ReceiveCalled' }] }) + .on('version', { return: { type: ['uint8'], value: [0] } }) + .on('initialize', { emits: [{ name: 'Initialized', args: { type: ['uint256'], value: [1] } }], nextFrame: 1 }) + + .frame(1) + .on('version', { return: { type: ['uint8'], value: [1] } }) + .create({ from: deployer }) + + proxy = await OssifiableProxy.new(currentImpl.address, admin, '0x', { from: deployer }) + proxiedImpl = await InitializableContract.at(proxy.address) + await snapshot.make() + }) + + afterEach(async () => snapshot.rollback()) + + describe('getters', () => { + it('proxy__getAdmin()', async () => { + assert.equal(await proxy.proxy__getAdmin(), admin) + }) + + it('proxy__getImplementation()', async () => { + assert.equal(await proxy.proxy__getImplementation(), currentImpl.address) + }) + + it('proxy__getIsOssified()', async () => { + assert.isFalse(await proxy.proxy__getIsOssified()) + }) + }) + + describe('proxy__ossify()', () => { + it('reverts with error "NotAdmin" when called by stranger', async () => { + await assert.reverts(proxy.proxy__ossify({ from: stranger }), 'NotAdmin()') + }) + + it('reverts with error "ProxyIsOssified" when called on ossified proxy', async () => { + // ossify proxy + await proxy.proxy__ossify({ from: admin }) + + // validate proxy is ossified + assert.isTrue(await proxy.proxy__getIsOssified()) + + await assert.reverts(proxy.proxy__ossify({ from: admin }), 'ProxyIsOssified()') + }) + + it('ossifies proxy', async () => { + const tx = await proxy.proxy__ossify({ from: admin }) + + // validate AdminChanged event was emitted + assert.emits(tx, 'AdminChanged', { previousAdmin: admin, newAdmin: ZERO_ADDRESS }) + + // validate ProxyOssified event was emitted + assert.emits(tx, 'ProxyOssified') + + // validate proxy is ossified + assert.isTrue(await proxy.proxy__getIsOssified()) + }) + }) + + describe('proxy__changeAdmin()', () => { + it('reverts with error "NotAdmin" when called by stranger', async () => { + await assert.reverts(proxy.proxy__changeAdmin(stranger, { from: stranger }), 'NotAdmin()') + }) + + it('reverts with error "ProxyIsOssified" when called on ossified proxy', async () => { + // ossify proxy + await proxy.proxy__ossify({ from: admin }) + + // validate proxy is ossified + assert.isTrue(await proxy.proxy__getIsOssified()) + + await assert.reverts(proxy.proxy__changeAdmin(stranger, { from: admin }), 'ProxyIsOssified()') + }) + + it('changes admin', async () => { + const tx = await proxy.proxy__changeAdmin(stranger, { from: admin }) + + // validate AdminChanged event was emitted + assert.emits(tx, 'AdminChanged', { previousAdmin: admin, newAdmin: stranger }) + + // validate admin was changed + assert.equal(await proxy.proxy__getAdmin(), stranger) + }) + }) + + describe('proxy__upgradeTo()', () => { + it('reverts with error "NotAdmin" called by stranger', async () => { + await assert.reverts(proxy.proxy__upgradeTo(newImpl.address, { from: stranger }), 'NotAdmin()') + }) + + it('reverts with error "ProxyIsOssified()" when called on ossified proxy', async () => { + // ossify proxy + await proxy.proxy__ossify({ from: admin }) + + // validate proxy is ossified + assert.isTrue(await proxy.proxy__getIsOssified()) + + await assert.reverts(proxy.proxy__upgradeTo(newImpl.address, { from: admin }), 'ProxyIsOssified()') + }) + + it('upgrades proxy to new implementation', async () => { + const tx = await proxy.proxy__upgradeTo(newImpl.address, { from: admin }) + + // validate Upgraded event was emitted + assert.emits(tx, 'Upgraded', { implementation: newImpl.address }) + + // validate implementation address was updated + assert.equal(await proxy.proxy__getImplementation(), newImpl.address) + }) + }) + + describe('proxy__upgradeToAndCall()', () => { + let initPayload + it('reverts with error "NotAdmin()" when called by stranger', async () => { + initPayload = newImpl.contract.methods.initialize(1).encodeABI() + await assert.reverts( + proxy.proxy__upgradeToAndCall(newImpl.address, initPayload, false, { + from: stranger, + }), + 'NotAdmin()' + ) + }) + + it('reverts with error "ProxyIsOssified()" whe called on ossified proxy', async () => { + // ossify proxy + await proxy.proxy__ossify({ from: admin }) + + // validate proxy is ossified + assert.isTrue(await proxy.proxy__getIsOssified()) + + await assert.reverts(proxy.proxy__upgradeToAndCall(newImpl.address, initPayload, false), 'ProxyIsOssified()') + }) + + it('upgrades proxy to new implementation when forceCall is false', async () => { + const tx = await proxy.proxy__upgradeToAndCall(newImpl.address, initPayload, false, { from: admin }) + + // validate Upgraded event was emitted + assert.emits(tx, 'Upgraded', { implementation: newImpl.address }) + + // validate Initialized event was emitted + assert.emits(tx, 'Initialized', { version: 1 }, { abi: InitializableABI }) + + // validate implementation address was updated + assert.equal(await proxy.proxy__getImplementation(), newImpl.address) + + // validate version was set + assert.equal(await proxiedImpl.version(), 1) + }) + + it('upgrades proxy to new implementation when forceCall is false', async () => { + const tx = await proxy.proxy__upgradeToAndCall(newImpl.address, '0x', true, { from: admin }) + + // validate Upgraded event was emitted + assert.emits(tx, 'Upgraded', { implementation: newImpl.address }) + + // validate ReceiveCalled event was emitted + assert.emits(tx, 'ReceiveCalled', {}, { abi: InitializableABI }) + + // validate implementation address was updated + assert.equal(await proxy.proxy__getImplementation(), newImpl.address) + + // validate version wasn't set + assert.equal(await proxiedImpl.version(), 0) + }) + }) +}) diff --git a/test/0.8.9/positive-token-rebase-limiter.test.js b/test/0.8.9/positive-token-rebase-limiter.test.js index a7bebe038..f30dd9bfe 100644 --- a/test/0.8.9/positive-token-rebase-limiter.test.js +++ b/test/0.8.9/positive-token-rebase-limiter.test.js @@ -2,11 +2,12 @@ const { artifacts, contract, ethers } = require('hardhat') const { bn, MAX_UINT64 } = require('@aragon/contract-helpers-test') const { EvmSnapshot } = require('../helpers/blockchain') -const { ETH } = require('../helpers/utils') +const { ETH, addSendWithResult } = require('../helpers/utils') const { assert } = require('../helpers/assert') const PositiveTokenRebaseLimiter = artifacts.require('PositiveTokenRebaseLimiterMock.sol') const UNLIMITED_REBASE = bn(MAX_UINT64) +const e9 = bn('10').pow(bn('9')) contract('PositiveTokenRebaseLimiter', () => { let limiter, snapshot @@ -16,6 +17,8 @@ contract('PositiveTokenRebaseLimiter', () => { snapshot = new EvmSnapshot(ethers.provider) await snapshot.make() + + addSendWithResult(limiter.increaseEther) }) afterEach(async () => { @@ -25,169 +28,201 @@ contract('PositiveTokenRebaseLimiter', () => { it('check uninitialized state', async () => { const limiterValues = await limiter.getLimiterValues() - assert.equals(limiterValues.totalPooledEther, 0) - assert.equals(limiterValues.totalShares, 0) - assert.equals(limiterValues.rebaseLimit, 0) - assert.equals(limiterValues.accumulatedRebase, 0) + assert.equals(limiterValues.preTotalPooledEther, 0) + assert.equals(limiterValues.preTotalShares, 0) + assert.equals(limiterValues.currentTotalPooledEther, 0) + assert.equals(limiterValues.positiveRebaseLimit, 0) assert.isTrue(await limiter.isLimitReached()) }) it('initialization check', async () => { const rebaseLimit = UNLIMITED_REBASE.div(bn(10)) - const totalPooledEther = ETH(101) - const totalShares = ETH(75) - await limiter.initLimiterState(rebaseLimit, totalPooledEther, totalShares) + const preTotalPooledEther = ETH(101) + const preTotalShares = ETH(75) + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) const limiterValues = await limiter.getLimiterValues() - assert.equals(limiterValues.totalPooledEther, totalPooledEther) - assert.equals(limiterValues.totalShares, totalShares) - assert.equals(limiterValues.rebaseLimit, rebaseLimit) - assert.equals(limiterValues.accumulatedRebase, bn(0)) + assert.equals(limiterValues.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.preTotalShares, preTotalShares) + assert.equals(limiterValues.currentTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.positiveRebaseLimit, rebaseLimit) assert.isFalse(await limiter.isLimitReached()) await assert.revertsWithCustomError( - limiter.initLimiterState(ETH(0), totalPooledEther, totalShares), + limiter.initLimiterState(ETH(0), preTotalPooledEther, preTotalShares), 'TooLowTokenRebaseLimit()' ) await assert.revertsWithCustomError( - limiter.initLimiterState(UNLIMITED_REBASE.add(bn(1)), totalPooledEther, totalShares), + limiter.initLimiterState(UNLIMITED_REBASE.add(bn(1)), preTotalPooledEther, preTotalShares), 'TooHighTokenRebaseLimit()' ) - await limiter.initLimiterState(UNLIMITED_REBASE, totalPooledEther, totalShares) + await limiter.initLimiterState(UNLIMITED_REBASE, preTotalPooledEther, preTotalShares) }) it('raise limit', async () => { const rebaseLimit = bn('7500') - const totalPooledEther = ETH(101) - const totalShares = ETH(75) - await limiter.initLimiterState(rebaseLimit, totalPooledEther, totalShares) + const preTotalPooledEther = ETH(101) + const preTotalShares = ETH(75) + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) - await limiter.raiseLimit(ETH(0)) + await limiter.decreaseEther(ETH(0)) const limiterValues0 = await limiter.getLimiterValues() - assert.equals(limiterValues0.totalPooledEther, totalPooledEther) - assert.equals(limiterValues0.totalShares, totalShares) - assert.equals(limiterValues0.rebaseLimit, rebaseLimit) - assert.equals(limiterValues0.accumulatedRebase, bn(0)) + assert.equals(limiterValues0.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues0.preTotalShares, preTotalShares) + assert.equals(limiterValues0.currentTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues0.positiveRebaseLimit, rebaseLimit) assert.isFalse(await limiter.isLimitReached()) - await limiter.raiseLimit(ETH(1)) + await limiter.decreaseEther(ETH(1)) const limiterValuesNeg = await limiter.getLimiterValues() - assert.equals(limiterValuesNeg.totalPooledEther, totalPooledEther) - assert.equals(limiterValuesNeg.totalShares, totalShares) - assert.equals(limiterValuesNeg.rebaseLimit, bn(9908490)) - assert.equals(limiterValuesNeg.accumulatedRebase, bn(0)) + assert.equals(limiterValuesNeg.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValuesNeg.preTotalShares, preTotalShares) + assert.equals(limiterValuesNeg.positiveRebaseLimit, rebaseLimit) + assert.equals(limiterValuesNeg.currentTotalPooledEther, bn(preTotalPooledEther).sub(bn(ETH(1)))) assert.isFalse(await limiter.isLimitReached()) }) it('consume limit', async () => { const rebaseLimit = bn('7500') - const totalPooledEther = ETH(1000000) - const totalShares = ETH(750) - await limiter.initLimiterState(rebaseLimit, totalPooledEther, totalShares) + const preTotalPooledEther = ETH(1000000) + const preTotalShares = ETH(750) + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) - await limiter.consumeLimit(ETH(1)) + await limiter.increaseEther(ETH(1)) assert.isFalse(await limiter.isLimitReached()) const limiterValues = await limiter.getLimiterValues() - assert.equals(limiterValues.totalPooledEther, totalPooledEther) - assert.equals(limiterValues.totalShares, totalShares) - assert.equals(limiterValues.rebaseLimit, rebaseLimit) - assert.equals(limiterValues.accumulatedRebase, bn(1000)) + assert.equals(limiterValues.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.preTotalShares, preTotalShares) + assert.equals(limiterValues.currentTotalPooledEther, bn(preTotalPooledEther).add(bn(ETH(1)))) + assert.equals(limiterValues.positiveRebaseLimit, rebaseLimit) assert.isFalse(await limiter.isLimitReached()) - const tx = await limiter.consumeLimit(ETH(2)) - assert.emits(tx, 'ReturnValue', { retValue: ETH(2) }) + assert.equals(await limiter.increaseEther.sendWithResult(ETH(2)), ETH(2)) assert.isFalse(await limiter.isLimitReached()) - const tx2 = await limiter.consumeLimit(ETH(4)) - assert.emits(tx2, 'ReturnValue', { retValue: ETH(4) }) + assert.equals(await limiter.increaseEther.sendWithResult(ETH(4)), ETH(4)) assert.isFalse(await limiter.isLimitReached()) - const tx3 = await limiter.consumeLimit(ETH(1)) - assert.emits(tx3, 'ReturnValue', { retValue: ETH(0.5) }) + assert.equals(await limiter.increaseEther.sendWithResult(ETH(1)), ETH(0.5)) assert.isTrue(await limiter.isLimitReached()) assert.equals(await limiter.getSharesToBurnLimit(), 0) }) it('raise and consume', async () => { const rebaseLimit = bn('5000') - const totalPooledEther = ETH(2000000) - const totalShares = ETH(1000000) - await limiter.initLimiterState(rebaseLimit, totalPooledEther, totalShares) + const preTotalPooledEther = ETH(2000000) + const preTotalShares = ETH(1000000) + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) - await limiter.raiseLimit(ETH(1)) + await limiter.decreaseEther(ETH(1)) assert.isFalse(await limiter.isLimitReached()) - const tx = await limiter.consumeLimit(ETH(2)) - assert.emits(tx, 'ReturnValue', { retValue: ETH(2) }) + assert.equals(await limiter.increaseEther.sendWithResult(ETH(2)), ETH(2)) assert.isFalse(await limiter.isLimitReached()) const limiterValues = await limiter.getLimiterValues() - assert.equals(limiterValues.totalPooledEther, totalPooledEther) - assert.equals(limiterValues.totalShares, totalShares) - assert.equals(limiterValues.rebaseLimit, rebaseLimit.add(bn(500))) - assert.equals(limiterValues.accumulatedRebase, bn(1000)) + assert.equals(limiterValues.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.preTotalShares, preTotalShares) + assert.equals(limiterValues.currentTotalPooledEther, bn(preTotalPooledEther).add(bn(ETH(2 - 1)))) + assert.equals(limiterValues.positiveRebaseLimit, rebaseLimit) + + assert.equals(await limiter.getSharesToBurnLimit(), bn('4499977500112499437')) - assert.equals(await limiter.getSharesToBurnLimit(), bn('4499979750091124589')) + const preShareRate = bn(preTotalPooledEther).mul(e9).div(bn(preTotalShares)) + const postShareRate = bn(limiterValues.currentTotalPooledEther) + .mul(e9) + .div(bn(preTotalShares).sub(await limiter.getSharesToBurnLimit())) + + const rebase = e9.mul(postShareRate).div(preShareRate).sub(e9) + assert.almostEqual(rebase, rebaseLimit, 1) }) it('raise, consume, and raise again', async () => { const rebaseLimit = bn('5000') - const totalPooledEther = ETH(2000000) - const totalShares = ETH(1000000) - await limiter.initLimiterState(rebaseLimit, totalPooledEther, totalShares) + const preTotalPooledEther = ETH(2000000) + const preTotalShares = ETH(1000000) + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) - await limiter.raiseLimit(ETH(1)) + await limiter.decreaseEther(ETH(1)) assert.isFalse(await limiter.isLimitReached()) - const tx = await limiter.consumeLimit(ETH(2)) - assert.emits(tx, 'ReturnValue', { retValue: ETH(2) }) + assert.equals(await limiter.increaseEther.sendWithResult(ETH(2)), ETH(2)) assert.isFalse(await limiter.isLimitReached()) - const limiterValues = await limiter.getLimiterValues() + let limiterValues = await limiter.getLimiterValues() - assert.equals(limiterValues.totalPooledEther, totalPooledEther) - assert.equals(limiterValues.totalShares, totalShares) - assert.equals(limiterValues.rebaseLimit, rebaseLimit.add(bn(500))) - assert.equals(limiterValues.accumulatedRebase, bn(1000)) + assert.equals(limiterValues.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.preTotalShares, preTotalShares) + assert.equals(limiterValues.currentTotalPooledEther, bn(preTotalPooledEther).add(bn(ETH(2 - 1)))) + assert.equals(limiterValues.positiveRebaseLimit, rebaseLimit) - await limiter.raiseLimit(ETH(1)) + await limiter.decreaseEther(ETH(1)) assert.isFalse(await limiter.isLimitReached()) + limiterValues = await limiter.getLimiterValues() - assert.equals(limiterValues.totalPooledEther, totalPooledEther) - assert.equals(limiterValues.totalShares, totalShares) - assert.equals(limiterValues.rebaseLimit, rebaseLimit.add(bn(500))) - assert.equals(limiterValues.accumulatedRebase, bn(1000)) + assert.equals(limiterValues.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.preTotalShares, preTotalShares) + assert.equals(limiterValues.currentTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.positiveRebaseLimit, rebaseLimit) assert.equals(await limiter.getSharesToBurnLimit(), bn('4999975000124999375')) }) it('zero tvl no reverts (means unlimited)', async () => { const rebaseLimit = bn('5000') - const totalPooledEther = ETH(0) - const totalShares = ETH(0) + const preTotalPooledEther = ETH(0) + const preTotalShares = ETH(0) - await limiter.initLimiterState(rebaseLimit, totalPooledEther, totalShares) + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) + assert.equals(await limiter.getSharesToBurnLimit(), 0) - await limiter.raiseLimit(ETH(0)) + await limiter.decreaseEther(ETH(0)) assert.isFalse(await limiter.isLimitReached()) - await limiter.consumeLimit(ETH(0)) + await limiter.increaseEther(ETH(0)) assert.isFalse(await limiter.isLimitReached()) - await limiter.raiseLimit(ETH(1)) + await limiter.decreaseEther(ETH(1)) assert.isFalse(await limiter.isLimitReached()) - await limiter.consumeLimit(ETH(1)) + await limiter.increaseEther(ETH(1)) assert.isFalse(await limiter.isLimitReached()) - const ethTx = await limiter.consumeLimit(ETH(2)) + assert.equals(await limiter.increaseEther.sendWithResult(ETH(2)), ETH(2)) assert.isFalse(await limiter.isLimitReached()) - assert.emits(ethTx, 'ReturnValue', { retValue: ETH(2) }) const maxSharesToBurn = await limiter.getSharesToBurnLimit() assert.equals(maxSharesToBurn, 0) }) + + it('share rate ~1 case with huge withdrawal', async () => { + const rebaseLimit = bn('1000000') // 0.1% + const preTotalPooledEther = ETH('1000000') + const preTotalShares = ETH('1000000') + + await limiter.initLimiterState(rebaseLimit, preTotalPooledEther, preTotalShares) + await limiter.increaseEther(ETH(1000)) + await limiter.decreaseEther(ETH(40000)) // withdrawal fulfillment + + assert.isFalse(await limiter.isLimitReached()) + const limiterValues = await limiter.getLimiterValues() + + assert.equals(limiterValues.preTotalPooledEther, preTotalPooledEther) + assert.equals(limiterValues.preTotalShares, preTotalShares) + assert.equals(limiterValues.currentTotalPooledEther, bn(preTotalPooledEther).sub(bn(ETH(39000)))) + assert.equals(limiterValues.positiveRebaseLimit, rebaseLimit) + + assert.equals(await limiter.getSharesToBurnLimit(), bn('39960039960039960039960')) + + const preShareRate = bn(preTotalPooledEther).mul(e9).div(bn(preTotalShares)) + const postShareRate = bn(limiterValues.currentTotalPooledEther) + .mul(e9) + .div(bn(preTotalShares).sub(await limiter.getSharesToBurnLimit())) + + const rebase = e9.mul(postShareRate).div(preShareRate).sub(e9) + assert.almostEqual(rebase, rebaseLimit, 1) + }) }) diff --git a/test/0.8.9/staking-router/digest.test.js b/test/0.8.9/staking-router/digest.test.js index 877eed769..9da68189d 100644 --- a/test/0.8.9/staking-router/digest.test.js +++ b/test/0.8.9/staking-router/digest.test.js @@ -38,6 +38,7 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { after(revert) let module1Id, module2Id + let module1AddedBlock, module2AddedBlock const nodeOperator1 = 0 let StakingModuleDigest, StakingModuleDigest2 @@ -55,6 +56,7 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { 5_000, // 50 % _treasuryFee { from: admin } ) + module1AddedBlock = await ethers.provider.getBlock() module1Id = +(await router.getStakingModuleIds())[0] }) @@ -67,6 +69,7 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { 3_000, // 50 % _treasuryFee { from: admin } ) + module2AddedBlock = await ethers.provider.getBlock() module2Id = +(await router.getStakingModuleIds())[1] }) @@ -186,8 +189,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { targetShare: '10000', status: '0', name: 'module 1', - lastDepositAt: '0', - lastDepositBlock: '0', + lastDepositAt: module1AddedBlock.timestamp.toString(), + lastDepositBlock: module1AddedBlock.number.toString(), exitedValidatorsCount: '0', }), summary: Object.values({ @@ -210,8 +213,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { targetShare: '9000', status: '0', name: 'module 2', - lastDepositAt: '0', - lastDepositBlock: '0', + lastDepositAt: module2AddedBlock.timestamp.toString(), + lastDepositBlock: module2AddedBlock.number.toString(), exitedValidatorsCount: '0', }), summary: Object.values({ diff --git a/test/0.8.9/staking-router/incomplete-exited-keys-reporting.test.js b/test/0.8.9/staking-router/incomplete-exited-keys-reporting.test.js new file mode 100644 index 000000000..792ee65c3 --- /dev/null +++ b/test/0.8.9/staking-router/incomplete-exited-keys-reporting.test.js @@ -0,0 +1,208 @@ +const { assert } = require('../../helpers/assert') +const { contract, artifacts } = require('hardhat') +const { ContractStub } = require('../../helpers/contract-stub') +const { hex, hexConcat } = require('../../helpers/utils') + +const StakingRouter = artifacts.require('StakingRouterMock') + +const sum = (...items) => items.reduce((s, v) => s + v, 0) +const packNodeOperatorIds = (nodeOperatorIds) => hexConcat(...nodeOperatorIds.map((i) => hex(i, 8))) +const packExitedValidatorCounts = (exitedValidatorsCount) => hexConcat(...exitedValidatorsCount.map((c) => hex(c, 16))) + +// Test covers the following scenario: +// 1. round i: oracle reports exited validators by staking module +// 2. round i: oracle reports exited validators by the node operator, but report is incomplete. +// Simulate a situation when all extra data can't be reported in one transaction +// 3. round i + 1: oracle reports exited validators by staking module +// 4. round i + 1: oracle reports exited validators missing in the first extra data report +contract('StakingRouter :: incomplete exited keys reporting', ([deployer, admin, lidoEOA]) => { + const deployStakingModuleStub = (firstGetStakingModuleSummary, secondGetStakingModuleSummary) => + ContractStub('IStakingModule') + .frame(0) + .on('getStakingModuleSummary', { + return: { + type: ['uint256', 'uint256', 'uint256'], + value: Object.values(firstGetStakingModuleSummary), + }, + }) + .on('updateExitedValidatorsCount', { nextFrame: 1 }) + + .frame(1) + .on('getStakingModuleSummary', { + return: { + type: ['uint256', 'uint256', 'uint256'], + value: Object.values(secondGetStakingModuleSummary), + }, + }) + .on('updateExitedValidatorsCount') + .create({ from: deployer }) + + const firstStakingModuleId = 1 + const secondStakingModuleId = 2 + + const defaultStakingModuleSummaries = { + [firstStakingModuleId]: { + totalExitedValidators: 0, + totalDepositedValidators: 30, + depositableValidatorsCount: 10, + }, + [secondStakingModuleId]: { + totalExitedValidators: 0, + totalDepositedValidators: 13, + depositableValidatorsCount: 20, + }, + } + + // oracle report data for round i + const firstOracleReport = { + byStakingModule: { [firstStakingModuleId]: 16, [secondStakingModuleId]: 11 }, + byNodeOperator: { + [firstStakingModuleId]: { nodeOperatorIds: [1, 2, 3, 4], exitedValidatorsCount: [2, 3, 4, 7] }, // full report + [secondStakingModuleId]: { nodeOperatorIds: [2, 4], exitedValidatorsCount: [1, 3] }, // partial report + }, + } + + // oracle report data for round i + 1 + const secondOracleReport = { + byStakingModule: { [secondStakingModuleId]: 11 }, + byNodeOperator: { + // deliver missing node operators data + [secondStakingModuleId]: { nodeOperatorIds: [3, 5, 6, 9], exitedValidatorsCount: [1, 2, 1, 3] }, + }, + } + + let depositContractStub, router, firstStakingModuleStub, secondStakingModuleStub + + before(async () => { + depositContractStub = await ContractStub('contracts/0.6.11/deposit_contract.sol:IDepositContract').create({ + from: deployer, + }) + router = await StakingRouter.new(depositContractStub.address, { from: deployer }) + + firstStakingModuleStub = await deployStakingModuleStub( + // return default staking module summary before first oracle report + defaultStakingModuleSummaries[firstStakingModuleId], + // after the first report, totalExitedValidators increased by sum of all exited validators + { + ...defaultStakingModuleSummaries[firstStakingModuleId], + totalExitedValidators: sum( + defaultStakingModuleSummaries[firstStakingModuleId].totalExitedValidators, + ...firstOracleReport.byNodeOperator[firstStakingModuleId].exitedValidatorsCount + ), + } + ) + + secondStakingModuleStub = await deployStakingModuleStub( + // return default staking module summary before first oracle report + defaultStakingModuleSummaries[secondStakingModuleId], + // after the first report, totalExitedValidators increased by sum of all exited validators + { + ...defaultStakingModuleSummaries[secondStakingModuleId], + totalExitedValidators: sum( + defaultStakingModuleSummaries[secondStakingModuleId].totalExitedValidators, + ...firstOracleReport.byNodeOperator[secondStakingModuleId].exitedValidatorsCount + ), + } + ) + + const wc = '0xff' + await router.initialize(admin, lidoEOA, wc, { from: deployer }) + + await router.grantRole(await router.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), admin, { from: admin }) + await router.grantRole(await router.STAKING_MODULE_PAUSE_ROLE(), admin, { from: admin }) + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.grantRole(await router.REPORT_EXITED_VALIDATORS_ROLE(), admin, { from: admin }) + + const addFirstStakingModuleTx = await router.addStakingModule( + 'module stub 1', + firstStakingModuleStub.address, + 100_00, // target share 100% + 10_00, // module fee 10% + 50_00, // treasury fee 50% from module fee + { from: admin } + ) + + // validate that actual staking module id equal to expected one + assert.isTrue( + addFirstStakingModuleTx.logs.some( + (e) => e.event === 'StakingModuleAdded' && e.args.stakingModuleId.toString() === firstStakingModuleId.toString() + ) + ) + + const addSecondStakingModuleTx = await router.addStakingModule( + 'module stub 2', + secondStakingModuleStub.address, + 10_00, // target share 10% + 10_00, // module fee 10% + 0, // treasury fee 0% from module fee + { from: admin } + ) + + // validate that actual staking module id equal to expected one + assert.isTrue( + addSecondStakingModuleTx.logs.some( + (e) => + e.event === 'StakingModuleAdded' && e.args.stakingModuleId.toString() === secondStakingModuleId.toString() + ) + ) + }) + + describe('test', () => { + it('round i: oracle reports exited validators by staking module', async () => { + await router.updateExitedValidatorsCountByStakingModule( + [firstStakingModuleId, secondStakingModuleId], + [ + firstOracleReport.byStakingModule[firstStakingModuleId], + firstOracleReport.byStakingModule[secondStakingModuleId], + ], + { from: admin } + ) + const [firstStakingModule, secondStakingModule] = await Promise.all([ + router.getStakingModule(firstStakingModuleId), + router.getStakingModule(secondStakingModuleId), + ]) + assert.equals(firstStakingModule.exitedValidatorsCount, firstOracleReport.byStakingModule[firstStakingModuleId]) + assert.equals(secondStakingModule.exitedValidatorsCount, firstOracleReport.byStakingModule[secondStakingModuleId]) + }) + + it('round i: oracle reports incompletely exited validators by node operator', async () => { + await router.reportStakingModuleExitedValidatorsCountByNodeOperator( + firstStakingModuleId, + packNodeOperatorIds(firstOracleReport.byNodeOperator[firstStakingModuleId].nodeOperatorIds), + packExitedValidatorCounts(firstOracleReport.byNodeOperator[firstStakingModuleId].exitedValidatorsCount), + { from: admin } + ) + + await router.reportStakingModuleExitedValidatorsCountByNodeOperator( + secondStakingModuleId, + packNodeOperatorIds(firstOracleReport.byNodeOperator[secondStakingModuleId].nodeOperatorIds), + packExitedValidatorCounts(firstOracleReport.byNodeOperator[secondStakingModuleId].exitedValidatorsCount), + { from: admin } + ) + }) + + it('round i + 1: oracle reports exited validators by staking module', async () => { + const tx = await router.updateExitedValidatorsCountByStakingModule( + [secondStakingModuleId], + [secondOracleReport.byStakingModule[secondStakingModuleId]], + { from: admin } + ) + + assert.emits(tx, 'StakingModuleExitedValidatorsIncompleteReporting', { + stakingModuleId: secondStakingModuleId, + unreportedExitedValidatorsCount: sum( + ...secondOracleReport.byNodeOperator[secondStakingModuleId].exitedValidatorsCount + ), + }) + }) + + it('round i + 1: oracle reports exited validators by node operators completely', async () => { + await router.reportStakingModuleExitedValidatorsCountByNodeOperator( + secondStakingModuleId, + packNodeOperatorIds(secondOracleReport.byNodeOperator[secondStakingModuleId].nodeOperatorIds), + packExitedValidatorCounts(secondOracleReport.byNodeOperator[secondStakingModuleId].exitedValidatorsCount), + { from: admin } + ) + }) + }) +}) diff --git a/test/0.8.9/staking-router/report-exited-keys.test.js b/test/0.8.9/staking-router/report-exited-keys.test.js index ff34a0db7..d05781853 100644 --- a/test/0.8.9/staking-router/report-exited-keys.test.js +++ b/test/0.8.9/staking-router/report-exited-keys.test.js @@ -1,7 +1,7 @@ const { contract, ethers } = require('hardhat') const { assert } = require('../../helpers/assert') const { EvmSnapshot } = require('../../helpers/blockchain') -const { hexConcat, hex, ETH } = require('../../helpers/utils') +const { hexConcat, hex, ETH, addSendWithResult } = require('../../helpers/utils') const { deployProtocol } = require('../../helpers/protocol') const { setupNodeOperatorsRegistry } = require('../../helpers/staking-modules') @@ -26,6 +26,7 @@ contract('StakingRouter', ([admin, depositor]) => { }) router = deployed.stakingRouter + addSendWithResult(router.updateExitedValidatorsCountByStakingModule) voting = deployed.voting.address operators = await setupNodeOperatorsRegistry(deployed, true) module2 = await setupNodeOperatorsRegistry(deployed, true) @@ -154,9 +155,16 @@ contract('StakingRouter', ([admin, depositor]) => { assert.equal(+distribution.shares[0], op1shareBefore * sharesDistribute) assert.equal(+distribution.shares[1], op2shareBefore * sharesDistribute) - // //update exited validators + // update exited validators const exitValidatorsCount = 20 - await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + const newlyExitedValidatorsCount = await router.updateExitedValidatorsCountByStakingModule.sendWithResult( + [module1Id], + [exitValidatorsCount], + { + from: admin, + } + ) + assert.equals(newlyExitedValidatorsCount, exitValidatorsCount) const nodeOpIds = [0] const exitedValidatorsCounts = [exitValidatorsCount] @@ -219,7 +227,12 @@ contract('StakingRouter', ([admin, depositor]) => { // //update exited validators const exitValidatorsCount = 20 - await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + const newlyExitedCount = await router.updateExitedValidatorsCountByStakingModule.sendWithResult( + [module1Id], + [exitValidatorsCount], + { from: admin } + ) + assert.equals(newlyExitedCount, exitValidatorsCount) const nodeOpIds = [0] const exitedValidatorsCounts = [exitValidatorsCount] @@ -289,9 +302,13 @@ contract('StakingRouter', ([admin, depositor]) => { assert.deepEqual([15, 5], await maxDepositsPerModule()) // update exited validators - let exitValidatorsCount = 1 - await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) - + const exitValidatorsCount = 1 + const exitedCount = await router.updateExitedValidatorsCountByStakingModule.sendWithResult( + [module1Id], + [exitValidatorsCount], + { from: admin } + ) + assert.equals(exitValidatorsCount, exitedCount) const nodeOpIds = [0] let exitedValidatorsCounts = [exitValidatorsCount] @@ -314,11 +331,16 @@ contract('StakingRouter', ([admin, depositor]) => { assert.deepEqual([16, 5], maxDepositsPerModuleAfterAlloc) // update next exited validators - exitValidatorsCount = 30 - exitedValidatorsCounts = [exitValidatorsCount] + const nextExitValidatorsCount = 30 + exitedValidatorsCounts = [nextExitValidatorsCount] keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) - await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + const newlyExitedCount = await router.updateExitedValidatorsCountByStakingModule.sendWithResult( + [module1Id], + [nextExitValidatorsCount], + { from: admin } + ) + assert.equals(newlyExitedCount, nextExitValidatorsCount - exitValidatorsCount) // report exited by module and node operator await router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { from: admin, diff --git a/test/0.8.9/staking-router/staking-router-keys-reporting.test.js b/test/0.8.9/staking-router/staking-router-keys-reporting.test.js index 7f7a825f4..eaded7a53 100644 --- a/test/0.8.9/staking-router/staking-router-keys-reporting.test.js +++ b/test/0.8.9/staking-router/staking-router-keys-reporting.test.js @@ -1,8 +1,8 @@ const { artifacts, contract, ethers } = require('hardhat') const { EvmSnapshot } = require('../../helpers/blockchain') const { assert } = require('../../helpers/assert') -const { hex, hexConcat, toNum } = require('../../helpers/utils') -const { StakingModuleStub } = require('../../helpers/stubs/staking-module.stub') +const { hex, hexConcat, toNum, addSendWithResult } = require('../../helpers/utils') +const { ContractStub } = require('../../helpers/contract-stub') const StakingRouter = artifacts.require('StakingRouterMock.sol') const StakingModuleMock = artifacts.require('StakingModuleMock.sol') @@ -17,6 +17,7 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { before(async () => { depositContract = await DepositContractMock.new({ from: deployer }) router = await StakingRouter.new(depositContract.address, { from: deployer }) + addSendWithResult(router.updateExitedValidatorsCountByStakingModule) ;[module1, module2] = await Promise.all([ StakingModuleMock.new({ from: deployer }), StakingModuleMock.new({ from: deployer }), @@ -78,9 +79,6 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { it('initially, router assumes no staking modules have exited validators', async () => { const info = await router.getStakingModule(module1Id) assert.equals(info.exitedValidatorsCount, 0) - - const totalExited = await router.getExitedValidatorsCountAcrossAllModules() - assert.equals(totalExited, 0) }) it('reverts total exited validators without REPORT_EXITED_VALIDATORS_ROLE', async () => { @@ -113,7 +111,14 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { }) it('reporting module 1 to have total 3 exited validators', async () => { - await router.updateExitedValidatorsCountByStakingModule([module1Id], [3], { from: admin }) + const newlyExitedCount = await router.updateExitedValidatorsCountByStakingModule.sendWithResult( + [module1Id], + [3], + { + from: admin, + } + ) + assert.equals(newlyExitedCount, 3) }) it('staking module info gets updated', async () => { @@ -121,11 +126,6 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { assert.equals(info.exitedValidatorsCount, 3) }) - it('exited validators count accross all modules gets updated', async () => { - const totalExited = await router.getExitedValidatorsCountAcrossAllModules() - assert.equals(totalExited, 3) - }) - it('no functions were called on the module', async () => { const callInfo = await getCallInfo(module1) assert.equal(callInfo.updateStuckValidatorsCount.callCount, 0) @@ -405,9 +405,9 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { await module1.setTotalExitedValidatorsCount(2) }) - it(`router's view on exited validators count accross all modules stays the same`, async () => { - const totalExited = await router.getExitedValidatorsCountAcrossAllModules() - assert.equals(totalExited, 3) + it(`router's view on exited validators count stays the same`, async () => { + const info = await router.getStakingModule(module1Id) + assert.equals(info.exitedValidatorsCount, 3) }) it(`calling onValidatorsCountsByNodeOperatorReportingFinished still doesn't call anything on the module`, async () => { @@ -480,17 +480,20 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { ) it("doesn't revert when onExitedAndStuckValidatorsCountsUpdated reverted", async () => { - const stakingModuleWithBug = await StakingModuleStub.new() // staking module will revert with panic exit code - await StakingModuleStub.stub(stakingModuleWithBug, 'onExitedAndStuckValidatorsCountsUpdated', { - revert: { error: 'Panic', args: { type: ['uint256'], value: [0x01] } }, - }) - await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleWithBug, { - totalExitedValidators: 0, - totalDepositedValidators: 0, - depositableValidatorsCount: 0, - }) - await router.addStakingModule('Staking Module With Bug', stakingModuleWithBug.address, 100, 1000, 2000, { + const buggedStakingModule = await ContractStub('IStakingModule') + .on('onExitedAndStuckValidatorsCountsUpdated', { + revert: { error: { name: 'Panic', args: { type: ['uint256'], value: [0x01] } } }, + }) + .on('getStakingModuleSummary', { + return: { + type: ['uint256', 'uint256', 'uint256'], + value: [0, 0, 0], + }, + }) + .create({ from: deployer }) + + await router.addStakingModule('Staking Module With Bug', buggedStakingModule.address, 100, 1000, 2000, { from: admin, }) const stakingModuleId = await router.getStakingModulesCount() @@ -501,6 +504,16 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { stakingModuleId, lowLevelRevertData: '0x4e487b710000000000000000000000000000000000000000000000000000000000000001', }) + + // staking module will revert with out of gas error (revert data is empty bytes) + await ContractStub(buggedStakingModule) + .on('onExitedAndStuckValidatorsCountsUpdated', { revert: { reason: 'outOfGas' } }) + .update({ from: deployer }) + + await assert.reverts( + router.onValidatorsCountsByNodeOperatorReportingFinished({ from: admin }), + 'UnrecoverableModuleError()' + ) }) }) @@ -536,13 +549,13 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { const info2 = await router.getStakingModule(moduleIds[1]) assert.equals(info2.exitedValidatorsCount, 0) - - const totalExited = await router.getExitedValidatorsCountAcrossAllModules() - assert.equals(totalExited, 0) }) it('reporting 3 exited keys total for module 1 and 2 exited keys total for module 2', async () => { - await router.updateExitedValidatorsCountByStakingModule(moduleIds, [3, 2], { from: admin }) + const newlyExited = await router.updateExitedValidatorsCountByStakingModule.sendWithResult(moduleIds, [3, 2], { + from: admin, + }) + assert.equals(newlyExited, 5) }) it('staking modules info gets updated', async () => { @@ -553,11 +566,6 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { assert.equals(info2.exitedValidatorsCount, 2) }) - it('exited validators count accross all modules gets updated', async () => { - const totalExited = await router.getExitedValidatorsCountAcrossAllModules() - assert.equals(totalExited, 5) - }) - it('revert on decreased exited keys for modules', async () => { await assert.reverts( router.updateExitedValidatorsCountByStakingModule(moduleIds, [2, 1], { from: admin }), @@ -579,7 +587,11 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { const { totalExitedValidators: totalExitedValidators1 } = await module1.getStakingModuleSummary() const { totalExitedValidators: totalExitedValidators2 } = await module2.getStakingModuleSummary() - const tx = await router.updateExitedValidatorsCountByStakingModule(moduleIds, [3, 2], { from: admin }) + const args = [moduleIds, [3, 2], { from: admin }] + const newlyExited = await router.updateExitedValidatorsCountByStakingModule.call(...args) + assert.equals(newlyExited, 0) + const tx = await router.updateExitedValidatorsCountByStakingModule(...args) + assert.emits(tx, 'StakingModuleExitedValidatorsIncompleteReporting', { stakingModuleId: moduleIds[0], unreportedExitedValidatorsCount: prevReportedExitedValidatorsCount1 - totalExitedValidators1, @@ -901,7 +913,10 @@ contract('StakingRouter', ([deployer, lido, admin, stranger]) => { } // first correction - await router.updateExitedValidatorsCountByStakingModule([module1Id], [10], { from: admin }) + const newlyExited = await router.updateExitedValidatorsCountByStakingModule.sendWithResult([module1Id], [10], { + from: admin, + }) + assert.equals(newlyExited, 10) await assert.reverts( router.unsafeSetExitedValidatorsCount(module1Id, nodeOperatorId, false, ValidatorsCountsCorrection, { from: admin, diff --git a/test/0.8.9/staking-router/staking-router.test.js b/test/0.8.9/staking-router/staking-router.test.js index 6fed47211..68d808476 100644 --- a/test/0.8.9/staking-router/staking-router.test.js +++ b/test/0.8.9/staking-router/staking-router.test.js @@ -5,7 +5,7 @@ const { BN } = require('bn.js') const { assert } = require('../../helpers/assert') const { EvmSnapshot } = require('../../helpers/blockchain') const { ETH, toBN } = require('../../helpers/utils') -const { StakingModuleStub } = require('../../helpers/stubs/staking-module.stub') +const { ContractStub } = require('../../helpers/contract-stub') const OssifiableProxy = artifacts.require('OssifiableProxy.sol') const DepositContractMock = artifacts.require('DepositContractMock') @@ -63,6 +63,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { assert.equals(await router.getWithdrawalCredentials(), wc) assert.equals(await router.getLido(), lido) assert.equals(await router.getStakingModulesCount(), 0) + assert.equals(await router.hasStakingModule(0), false) + assert.equals(await router.hasStakingModule(1), false) assert.equals(await router.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 1) assert.equals(await router.hasRole(DEFAULT_ADMIN_ROLE, admin), true) @@ -210,9 +212,11 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { stakingModule = await StakingModuleMock.new({ from: deployer }) + assert.equals(await router.hasStakingModule(1), false) await router.addStakingModule('Test module', stakingModule.address, 100, 1000, 2000, { from: appManager, }) + assert.equals(await router.hasStakingModule(1), true) await stakingModule.setAvailableKeysCount(100, { from: deployer }) @@ -223,6 +227,15 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { await revert() }) + it('reverts if module is unregistered', async () => { + await assert.reverts(router.getStakingModuleIsActive(123), `StakingModuleUnregistered()`) + await assert.reverts(router.getStakingModuleLastDepositBlock(123), `StakingModuleUnregistered()`) + await assert.reverts(router.getStakingModuleIsDepositsPaused(123), `StakingModuleUnregistered()`) + await assert.reverts(router.getStakingModuleNonce(123), `StakingModuleUnregistered()`) + await assert.reverts(router.getStakingModuleIsStopped(123), `StakingModuleUnregistered()`) + await assert.reverts(router.getStakingModuleStatus(123), `StakingModuleUnregistered()`) + }) + it('reverts if module address exists', async () => { await assert.revertsWithCustomError( router.addStakingModule('Test', stakingModule.address, 100, 1000, 2000, { from: appManager }), @@ -305,12 +318,14 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { }) it('set withdrawal credentials works when staking module reverts', async () => { - const stakingModuleWithBug = await StakingModuleStub.new() // staking module will revert with panic exit code - await StakingModuleStub.stub(stakingModuleWithBug, 'onWithdrawalCredentialsChanged', { - revert: { error: 'Panic', args: { type: ['uint256'], value: [0x01] } }, - }) - await router.addStakingModule('Staking Module With Bug', stakingModuleWithBug.address, 100, 1000, 2000, { + const buggedStakingModule = await ContractStub('IStakingModule') + .on('onWithdrawalCredentialsChanged', { + revert: { error: { name: 'Panic', args: { type: ['uint256'], value: [0x01] } } }, + }) + .create({ from: deployer }) + + await router.addStakingModule('Staking Module With Bug', buggedStakingModule.address, 100, 1000, 2000, { from: appManager, }) const stakingModuleId = await router.getStakingModulesCount() @@ -336,6 +351,13 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { ) assert.isTrue(await router.getStakingModuleIsDepositsPaused(stakingModuleId)) + + // staking module will revert with out of gas error (revert data is empty bytes) + await ContractStub(buggedStakingModule) + .on('onWithdrawalCredentialsChanged', { revert: { reason: 'outOfGas' } }) + .update({ from: deployer }) + + await assert.reverts(router.setWithdrawalCredentials(newWC, { from: appManager }), 'UnrecoverableModuleError()') }) }) @@ -346,7 +368,9 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { it('staking modules limit is 32', async () => { for (let i = 0; i < 32; i++) { const stakingModule = await StakingModuleMock.new({ from: deployer }) + assert.equals(await router.hasStakingModule(i + 1), false) await router.addStakingModule('Test module', stakingModule.address, 100, 100, 100, { from: appManager }) + assert.equals(await router.hasStakingModule(i + 1), true) } const oneMoreStakingModule = await StakingModuleMock.new({ from: deployer }) @@ -368,6 +392,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { treasuryFee: 200, expectedModuleId: 1, address: null, + lastDepositAt: null, + lastDepositBlock: null, }, { name: 'Test module 1', @@ -376,6 +402,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { treasuryFee: 200, expectedModuleId: 2, address: null, + lastDepositAt: null, + lastDepositBlock: null, }, ] @@ -494,7 +522,11 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { from: appManager, } ) - assert.equals(tx.logs.length, 3) + const latestBlock = await ethers.provider.getBlock() + stakingModulesParams[0].lastDepositAt = latestBlock.timestamp + stakingModulesParams[0].lastDepositBlock = latestBlock.number + + assert.equals(tx.logs.length, 4) await assert.emits(tx, 'StakingModuleAdded', { stakingModuleId: stakingModulesParams[0].expectedModuleId, stakingModule: stakingModule1.address, @@ -512,6 +544,10 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { treasuryFee: stakingModulesParams[0].treasuryFee, setBy: appManager, }) + await assert.emits(tx, 'StakingRouterETHDeposited', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + amount: 0, + }) assert.equals(await router.getStakingModulesCount(), 1) assert.equals( @@ -530,8 +566,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { assert.equals(module.treasuryFee, stakingModulesParams[0].treasuryFee) assert.equals(module.targetShare, stakingModulesParams[0].targetShare) assert.equals(module.status, StakingModuleStatus.Active) - assert.equals(module.lastDepositAt, 0) - assert.equals(module.lastDepositBlock, 0) + assert.equals(module.lastDepositAt, stakingModulesParams[0].lastDepositAt) + assert.equals(module.lastDepositBlock, stakingModulesParams[0].lastDepositBlock) }) it('add another staking module', async () => { @@ -545,8 +581,11 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { from: appManager, } ) + const latestBlock = await ethers.provider.getBlock() + stakingModulesParams[1].lastDepositAt = latestBlock.timestamp + stakingModulesParams[1].lastDepositBlock = latestBlock.number - assert.equals(tx.logs.length, 3) + assert.equals(tx.logs.length, 4) await assert.emits(tx, 'StakingModuleAdded', { stakingModuleId: stakingModulesParams[1].expectedModuleId, stakingModule: stakingModule2.address, @@ -564,6 +603,10 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { treasuryFee: stakingModulesParams[1].treasuryFee, setBy: appManager, }) + await assert.emits(tx, 'StakingRouterETHDeposited', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + amount: 0, + }) assert.equals(await router.getStakingModulesCount(), 2) assert.equals( @@ -582,8 +625,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { assert.equals(module.treasuryFee, stakingModulesParams[1].treasuryFee) assert.equals(module.targetShare, stakingModulesParams[1].targetShare) assert.equals(module.status, StakingModuleStatus.Active) - assert.equals(module.lastDepositAt, 0) - assert.equals(module.lastDepositBlock, 0) + assert.equals(module.lastDepositAt, stakingModulesParams[1].lastDepositAt) + assert.equals(module.lastDepositBlock, stakingModulesParams[1].lastDepositBlock) }) it('get staking modules list', async () => { @@ -596,8 +639,8 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { assert.equals(stakingModules[i].treasuryFee, stakingModulesParams[i].treasuryFee) assert.equals(stakingModules[i].targetShare, stakingModulesParams[i].targetShare) assert.equals(stakingModules[i].status, StakingModuleStatus.Active) - assert.equals(stakingModules[i].lastDepositAt, 0) - assert.equals(stakingModules[i].lastDepositBlock, 0) + assert.equals(stakingModules[i].lastDepositAt, stakingModulesParams[i].lastDepositAt) + assert.equals(stakingModules[i].lastDepositBlock, stakingModulesParams[i].lastDepositBlock) } }) @@ -928,12 +971,12 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { }) it('handles reverted staking modules correctly', async () => { - const stakingModuleWithBug = await StakingModuleStub.new() // staking module will revert with message "UNHANDLED_ERROR" - await StakingModuleStub.stub(stakingModuleWithBug, 'onRewardsMinted', { - revert: { reason: 'UNHANDLED_ERROR' }, - }) - await router.addStakingModule('Staking Module With Bug', stakingModuleWithBug.address, 100, 1000, 2000, { + const buggedStakingModule = await ContractStub('IStakingModule') + .on('onRewardsMinted', { revert: { reason: 'UNHANDLED_ERROR' } }) + .create({ from: deployer }) + + await router.addStakingModule('Staking Module With Bug', buggedStakingModule.address, 100, 1000, 2000, { from: admin, }) const stakingModuleWithBugId = await router.getStakingModulesCount() @@ -954,6 +997,16 @@ contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { stakingModuleId: stakingModuleWithBugId, lowLevelRevertData: [errorMethodId, ...errorMessageEncoded].join(''), }) + + // staking module will revert with out of gas error (revert data is empty bytes) + await ContractStub(buggedStakingModule) + .on('onRewardsMinted', { revert: { reason: 'outOfGas' } }) + .update({ from: deployer }) + + await assert.reverts( + router.reportRewardsMinted(stakingModuleIds, totalShares, { from: admin }), + 'UnrecoverableModuleError()' + ) }) }) diff --git a/test/0.8.9/withdrawal-queue-gas.test.js b/test/0.8.9/withdrawal-queue-gas.test.js new file mode 100644 index 000000000..9700e153d --- /dev/null +++ b/test/0.8.9/withdrawal-queue-gas.test.js @@ -0,0 +1,194 @@ +/* eslint-disable no-template-curly-in-string */ +const { contract, ethers } = require('hardhat') +const { bn } = require('@aragon/contract-helpers-test') +const { itParam } = require('mocha-param') + +const { ETH, StETH, shares } = require('../helpers/utils') +const { setBalance, EvmSnapshot } = require('../helpers/blockchain') +const { deployWithdrawalQueue } = require('./withdrawal-queue-deploy.test') + +contract('WithdrawalQueue', ([owner, user]) => { + let wq, steth, defaultShareRate, belowShareRate, aboveShareRate, snapshot + let gasPrice = 1 + const currentRate = async () => + bn(await steth.getTotalPooledEther()) + .mul(bn(10).pow(bn(27))) + .div(await steth.getTotalShares()) + + const MAX_BATCH_SIZE = 280 + const batchIncrement = (i) => i * 2 + const REQ_AMOUNT = ETH(0.00001) + const batchSizes = [] + for (let batch_size = 1; batch_size <= MAX_BATCH_SIZE; batch_size = batchIncrement(batch_size)) { + batchSizes.push(batch_size) + } + + before('Deploy', async function () { + if (!process.env.REPORT_GAS) { + this.skip() + } + snapshot = new EvmSnapshot(ethers.provider) + + const deployed = await deployWithdrawalQueue({ + stethOwner: owner, + queueAdmin: owner, + queuePauser: owner, + queueResumer: owner, + queueFinalizer: owner, + }) + + steth = deployed.steth + wq = deployed.withdrawalQueue + + await steth.setTotalPooledEther(ETH(600)) + await setBalance(steth.address, ETH(600)) + await steth.mintShares(user, shares(300)) + await steth.approve(wq.address, StETH(300), { from: user }) + defaultShareRate = await currentRate() + belowShareRate = defaultShareRate.divn(2) + aboveShareRate = defaultShareRate.muln(2) + await snapshot.make() + }) + + after('clean up', async function () { + // only rollback if not skipped + if (process.env.REPORT_GAS) { + await snapshot.rollback() + } + }) + + context('requestWithdrawal', () => { + let results + + before(async () => { + results = [] + }) + + after(async () => { + console.log('requestWithdrawals') + console.table(results) + }) + + itParam('batch size ${value}', batchSizes, async (batch_size) => { + const args = [ + Array(batch_size).fill(REQ_AMOUNT), + user, + { + from: user, + // smap of large transactions causes local network baseFee rise in next blocks + // increase gasPrice a bit on every tx to make sure execute + gasPrice: gasPrice++, + gasLimit: 1000000000, + }, + ] + const estimated = await wq.requestWithdrawals.estimateGas(...args) + args[args.length - 1].gasLimit = estimated + const tx = await wq.requestWithdrawals(...args) + results.push({ + 'batch size': batch_size, + estimated, + used: tx.receipt.gasUsed, + 'diff%': parseFloat((((estimated - tx.receipt.gasUsed) / estimated) * 100).toFixed(3)), + 'gas/req': Math.ceil(tx.receipt.gasUsed / batch_size), + }) + }) + }) + + context('pre/finalize', () => { + let prefinalize_results, finalization_results, slash + + before(async () => { + prefinalize_results = [] + finalization_results = [] + slash = false + }) + + after(async () => { + console.log('Prefinalize') + console.table(prefinalize_results) + console.log('Finalize') + console.table(finalization_results) + }) + + itParam('batch size ${value}', batchSizes, async (batch_size) => { + const batchStart = await wq.getLastFinalizedRequestId() + const batchEnd = batchStart.addn(batch_size) + const prefinalize_args = [[batchEnd], slash ? aboveShareRate : belowShareRate] + const [prefinalize_gas, prefinalize_res] = await Promise.all([ + wq.prefinalize.estimateGas(...prefinalize_args), + wq.prefinalize.call(...prefinalize_args), + ]) + prefinalize_results.push({ + 'batch size': batch_size, + gas: prefinalize_gas, + slash, + 'gas/req': Math.ceil(prefinalize_gas / batch_size), + }) + + const finalization_args = [ + [batchEnd], + slash ? aboveShareRate : belowShareRate, + { from: owner, value: prefinalize_res.ethToLock, gasPrice: gasPrice++ }, + ] + const estimated = await wq.finalize.estimateGas(...finalization_args) + finalization_args[finalization_args.length - 1].gasLimit = estimated + const tx = await wq.finalize(...finalization_args) + finalization_results.push({ + 'batch size': batch_size, + estimated, + used: tx.receipt.gasUsed, + 'diff%': parseFloat((((estimated - tx.receipt.gasUsed) / estimated) * 100).toFixed(3)), + 'gas/req': Math.ceil(tx.receipt.gasUsed / batch_size), + slash, + }) + slash = !slash + }) + }) + + context('findHints/claim', () => { + let findHints_results, claim_results, lastCheckpointIndex, batchStart + + before(async () => { + findHints_results = [] + claim_results = [] + lastCheckpointIndex = await wq.getLastCheckpointIndex() + batchStart = 1 + }) + + after(async () => { + console.log('FindCheckpointsHints') + console.table(findHints_results) + console.log('claimWithdrawals') + console.table(claim_results) + }) + itParam('batch size ${value}', batchSizes, async (batch_size) => { + const requestIds = Array(batch_size) + .fill(0) + .map((_, i) => batchStart + i) + const findHintsArgs = [requestIds, 1, lastCheckpointIndex] + const [findHints_gas, findHints_res] = await Promise.all([ + wq.findCheckpointHints.estimateGas(...findHintsArgs), + wq.findCheckpointHints.call(...findHintsArgs), + ]) + findHints_results.push({ + 'batch size': batch_size, + gas: findHints_gas, + 'gas/req': Math.ceil(findHints_gas / batch_size), + }) + + /// Claiming + const claiming_args = [requestIds, findHints_res, { from: user, gasPrice: gasPrice++ }] + const estimated = await wq.claimWithdrawals.estimateGas(...claiming_args) + claiming_args[claiming_args.length - 1].gasLimit = estimated + const tx = await wq.claimWithdrawals(...claiming_args) + claim_results.push({ + 'batch size': batch_size, + estimated, + used: tx.receipt.gasUsed, + 'diff%': parseFloat((((estimated - tx.receipt.gasUsed) / estimated) * 100).toFixed(3)), + 'gas/req': Math.ceil(tx.receipt.gasUsed / batch_size), + }) + batchStart += batch_size + }) + }) +}) diff --git a/test/0.8.9/withdrawal-queue-nft.test.js b/test/0.8.9/withdrawal-queue-nft.test.js index 552a23c21..c9a2f627a 100644 --- a/test/0.8.9/withdrawal-queue-nft.test.js +++ b/test/0.8.9/withdrawal-queue-nft.test.js @@ -35,7 +35,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, tokenUriManager, erc721ReceiverMock = await ERC721ReceiverMock.new({ from: owner }) await steth.setTotalPooledEther(ETH(600)) - // we need 1 ETH additionally to pay gas on finalization because coverage ingnores gasPrice=0 + // we need 1 ETH additionally to pay gas on finalization because coverage ignores gasPrice=0 await setBalance(steth.address, ETH(600 + 1)) await steth.mintShares(user, shares(1)) await steth.approve(withdrawalQueue.address, StETH(300), { from: user }) diff --git a/test/0.8.9/withdrawal-queue-requests-finalization.test.js b/test/0.8.9/withdrawal-queue-requests-finalization.test.js index a350d1085..88f523a87 100644 --- a/test/0.8.9/withdrawal-queue-requests-finalization.test.js +++ b/test/0.8.9/withdrawal-queue-requests-finalization.test.js @@ -1,7 +1,7 @@ const { contract, ethers } = require('hardhat') const { itParam } = require('mocha-param') -const { StETH, shareRate, e18, e27, toBN } = require('../helpers/utils') +const { StETH, shareRate, e18, e27, toBN, ETH, addSendWithResult } = require('../helpers/utils') const { assert } = require('../helpers/assert') const { MAX_UINT256 } = require('../helpers/constants') const { EvmSnapshot } = require('../helpers/blockchain') @@ -57,6 +57,7 @@ contract('WithdrawalQueue', ([owner, daoAgent, user, anotherUser]) => { steth = deployed.steth withdrawalQueue = deployed.withdrawalQueue + addSendWithResult(withdrawalQueue.requestWithdrawals) await steth.mintShares(user, e18(10)) await steth.approve(withdrawalQueue.address, StETH(10), { from: user }) @@ -71,6 +72,66 @@ contract('WithdrawalQueue', ([owner, daoAgent, user, anotherUser]) => { await snapshot.rollback() }) + context('calculateFinalizationBatches', () => { + it('reverts on invalid state', async () => { + await assert.reverts( + withdrawalQueue.calculateFinalizationBatches(shareRate(300), 100000, 1000, [ + ETH(10), + true, + Array(36).fill(0), + 0, + ]), + 'InvalidState()' + ) + await assert.reverts( + withdrawalQueue.calculateFinalizationBatches(shareRate(300), 100000, 1000, [0, false, Array(36).fill(0), 0]), + 'InvalidState()' + ) + }) + + it('works correctly on multiple calls', async () => { + const [requestId1, requestId2] = await withdrawalQueue.requestWithdrawals.sendWithResult([ETH(1), ETH(1)], user, { + from: user, + }) + const calculatedBatches1 = await withdrawalQueue.calculateFinalizationBatches(shareRate(1), 10000000000, 1, [ + ETH(2), + false, + Array(36).fill(0), + 0, + ]) + + assert.equals(calculatedBatches1.remainingEthBudget, ETH(1)) + assert.equals(calculatedBatches1.finished, false) + assert.equals(calculatedBatches1.batchesLength, 1) + assert.equals(calculatedBatches1.batches[0], requestId1) + const calculatedBatches2 = await withdrawalQueue.calculateFinalizationBatches( + shareRate(1), + 10000000000, + 1, + calculatedBatches1 + ) + assert.equals(calculatedBatches2.remainingEthBudget, 0) + assert.equals(calculatedBatches2.finished, true) + assert.equals(calculatedBatches2.batchesLength, 1) + assert.equals(calculatedBatches2.batches[0], requestId2) + }) + + it('stops on maxTimestamp', async () => { + const [requestId1] = await withdrawalQueue.requestWithdrawals.sendWithResult([ETH(1)], user, { + from: user, + }) + const [status] = await withdrawalQueue.getWithdrawalStatus([requestId1]) + const calculatedBatches1 = await withdrawalQueue.calculateFinalizationBatches( + shareRate(1), + +status.timestamp - 1, + 10, + [ETH(2), false, Array(36).fill(0), 0] + ) + assert.equals(calculatedBatches1.finished, true) + assert.equals(calculatedBatches1.batchesLength, 0) + }) + }) + context('1 request', () => { itParam('same rate ', [0.25, 0.5, 1], async (postFinalizationRate) => { const finalizationShareRate = shareRate(1) diff --git a/test/0.8.9/withdrawal-queue.test.js b/test/0.8.9/withdrawal-queue.test.js index 62fd8dd9b..199a52963 100644 --- a/test/0.8.9/withdrawal-queue.test.js +++ b/test/0.8.9/withdrawal-queue.test.js @@ -1,4 +1,4 @@ -const { contract, ethers, web3 } = require('hardhat') +const { contract, ethers, web3, artifacts } = require('hardhat') const { bn, getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test') const { ETH, StETH, shareRate, shares } = require('../helpers/utils') @@ -7,11 +7,13 @@ const { MAX_UINT256, ACCOUNTS_AND_KEYS } = require('../helpers/constants') const { signPermit, makeDomainSeparator } = require('../0.6.12/helpers/permit_helpers') const { impersonate, EvmSnapshot, getCurrentBlockTimestamp, setBalance } = require('../helpers/blockchain') +const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock') + const { deployWithdrawalQueue } = require('./withdrawal-queue-deploy.test') contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, oracle]) => { - let withdrawalQueue, steth, wsteth - + let withdrawalQueue, steth, wsteth, defaultShareRate + const ALLOWED_ERROR_WEI = 100 const snapshot = new EvmSnapshot(ethers.provider) const currentRate = async () => @@ -32,11 +34,13 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, withdrawalQueue = deployed.withdrawalQueue await steth.setTotalPooledEther(ETH(600)) - // we need 1 ETH additionally to pay gas on finalization because coverage ignores gasPrice=0 + // we need 1 ETH additionally to pay gas on finalization because solidity-coverage ignores gasPrice=0 await setBalance(steth.address, ETH(600 + 1)) await steth.mintShares(user, shares(1)) await steth.approve(withdrawalQueue.address, StETH(300), { from: user }) + defaultShareRate = (await currentRate()).toString(10) + await impersonate(ethers.provider, steth.address) await snapshot.make() }) @@ -55,7 +59,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(0)) }) - context('Pause/Resume', async () => { + context('Pause/Resume', () => { it('only correct roles can alter pause state', async () => { const [PAUSE_ROLE, RESUME_ROLE] = await Promise.all([withdrawalQueue.PAUSE_ROLE(), withdrawalQueue.RESUME_ROLE()]) await withdrawalQueue.grantRole(PAUSE_ROLE, pauser, { from: daoAgent }) @@ -145,7 +149,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) }) - context('BunkerMode', async () => { + context('BunkerMode', () => { it('init config', async () => { assert(!(await withdrawalQueue.isBunkerModeActive())) assert.equals(ethers.constants.MaxUint256, await withdrawalQueue.bunkerModeSinceTimestamp()) @@ -171,6 +175,10 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, withdrawalQueue.onOracleReport(true, +timestamp + 1000000, +timestamp + 1100000, { from: steth.address }), 'InvalidReportTimestamp()' ) + await assert.reverts( + withdrawalQueue.onOracleReport(true, +timestamp - 100, +timestamp + 1100000, { from: steth.address }), + 'InvalidReportTimestamp()' + ) // enable timestamp = await getCurrentBlockTimestamp() const tx1 = await withdrawalQueue.onOracleReport(true, timestamp, timestamp, { from: steth.address }) @@ -186,7 +194,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) }) - context('Request', async () => { + context('Request', () => { it('One can request a withdrawal', async () => { const receipt = await withdrawalQueue.requestWithdrawals([StETH(300)], owner, { from: user }) const requestId = getEventArgument(receipt, 'WithdrawalRequested', 'requestId') @@ -353,7 +361,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) }) - context('Finalization', async () => { + context('Finalization', () => { const amount = bn(ETH(300)) beforeEach('Enqueue a request', async () => { @@ -391,8 +399,8 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, await steth.approve(withdrawalQueue.address, StETH(300), { from: user }) await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - const batch = await withdrawalQueue.prefinalize.call([2], shareRate(300)) - await withdrawalQueue.finalize([2], shareRate(300), { from: steth.address, value: batch.ethToLock }) + const batch = await withdrawalQueue.prefinalize.call([2], defaultShareRate) + await withdrawalQueue.finalize([2], defaultShareRate, { from: steth.address, value: batch.ethToLock }) assert.equals(batch.sharesToBurn, shares(2)) assert.equals(await withdrawalQueue.getLastRequestId(), 2) @@ -411,7 +419,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) assert.equals(await withdrawalQueue.getLastRequestId(), 2) assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), 1) @@ -421,7 +429,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, await ethers.provider.getBalance(withdrawalQueue.address) ) - await withdrawalQueue.finalize([2], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([2], defaultShareRate, { from: steth.address, value: amount }) assert.equals(await withdrawalQueue.getLastRequestId(), 2) assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), 2) @@ -436,27 +444,44 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, await assert.reverts(withdrawalQueue.prefinalize([1], shareRate(0)), 'ZeroShareRate()') }) + it('batch reverts if share rate is zero', async () => { + await steth.setTotalPooledEther(ETH(900)) + await steth.mintShares(user, shares(1)) + await steth.approve(withdrawalQueue.address, StETH(600), { from: user }) + + await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) + await assert.reverts(withdrawalQueue.prefinalize([2, 1], shareRate(1)), 'BatchesAreNotSorted()') + }) + + it('reverts if batches are empty', async () => { + await assert.reverts(withdrawalQueue.prefinalize([], shareRate(1.5)), 'EmptyBatches()') + await assert.reverts( + withdrawalQueue.finalize([], defaultShareRate, { from: steth.address, value: amount }), + 'EmptyBatches()' + ) + }) + it('reverts if request with given id did not even created', async () => { const idAhead = +(await withdrawalQueue.getLastRequestId()) + 1 await assert.reverts( - withdrawalQueue.finalize([idAhead], shareRate(300), { from: steth.address, value: amount }), + withdrawalQueue.finalize([idAhead], defaultShareRate, { from: steth.address, value: amount }), `InvalidRequestId(${idAhead})` ) - await assert.reverts(withdrawalQueue.prefinalize([idAhead], shareRate(300)), `InvalidRequestId(${idAhead})`) + await assert.reverts(withdrawalQueue.prefinalize([idAhead], defaultShareRate), `InvalidRequestId(${idAhead})`) }) it('reverts if request with given id was finalized already', async () => { const id = +(await withdrawalQueue.getLastRequestId()) - await withdrawalQueue.finalize([id], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([id], defaultShareRate, { from: steth.address, value: amount }) await assert.reverts( - withdrawalQueue.finalize([id], shareRate(300), { from: steth.address, value: amount }), + withdrawalQueue.finalize([id], defaultShareRate, { from: steth.address, value: amount }), `InvalidRequestId(${id})` ) - await assert.reverts(withdrawalQueue.prefinalize([id], shareRate(300)), `InvalidRequestId(${id})`) + await assert.reverts(withdrawalQueue.prefinalize([id], defaultShareRate), `InvalidRequestId(${id})`) }) it('reverts if given amount to finalize exceeds requested', async () => { @@ -464,7 +489,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, const amountExceeded = bn(ETH(400)) await assert.reverts( - withdrawalQueue.finalize([id], shareRate(300), { from: steth.address, value: amountExceeded }), + withdrawalQueue.finalize([id], defaultShareRate, { from: steth.address, value: amountExceeded }), `TooMuchEtherToFinalize(${+amountExceeded}, ${+amount})` ) }) @@ -476,8 +501,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) it('works', async () => { - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: ETH(1) }) - + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: ETH(1) }) assert.almostEqual(await withdrawalQueue.getClaimableEther([1], [1]), ETH(1), 100) }) @@ -520,16 +544,37 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, await assert.reverts(withdrawalQueue.getClaimableEther([3], [1]), 'InvalidHint(1)') }) + + it('works on multiple checkpoints, no discount', async () => { + const requestCount = 5 + const shareRate = await currentRate() + await withdrawalQueue.finalize([1], shareRate, { from: steth.address, value: ETH(1) }) + for (let index = 0; index < requestCount; index++) { + await withdrawalQueue.requestWithdrawals([ETH(1)], owner, { from: user }) + await withdrawalQueue.finalize([index + 2], shareRate, { from: steth.address, value: ETH(1) }) + } + const requestIds = Array(requestCount + 1) + .fill(0) + .map((_, i) => i + 1) + + const hints = await withdrawalQueue.findCheckpointHints( + requestIds, + 1, + await withdrawalQueue.getLastCheckpointIndex() + ) + const claimableEth = await withdrawalQueue.getClaimableEther(requestIds, hints) + claimableEth.forEach((eth) => assert.almostEqual(eth, ETH(1), 100)) + }) }) - context('claimWithdrawal()', async () => { + context('claimWithdrawal()', () => { const amount = ETH(300) beforeEach('Enqueue a request', async () => { await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) }) it('Owner can claim a finalized request to recipient address', async () => { - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) const balanceBefore = bn(await ethers.provider.getBalance(user)) @@ -551,7 +596,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) it('reverts if sender is not owner', async () => { - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) await assert.reverts( withdrawalQueue.claimWithdrawalsTo([1], [1], owner, { from: stranger }), `NotOwner("${stranger}", "${owner}")` @@ -559,21 +604,30 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) it('reverts if there is not enough balance', async () => { - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) await setBalance(withdrawalQueue.address, ETH(200)) await assert.reverts(withdrawalQueue.claimWithdrawalsTo([1], [1], owner, { from: owner }), 'NotEnoughEther()') }) + + it('reverts if receiver declines', async () => { + const receiver = await ERC721ReceiverMock.new({ from: owner }) + await receiver.setDoesAcceptTokens(false, { from: owner }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) + await assert.reverts( + withdrawalQueue.claimWithdrawalsTo([1], [1], receiver.address, { from: owner }), + 'CantSendValueRecipientMayHaveReverted()' + ) + }) }) it('Owner can claim a finalized request without hint', async () => { - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) const balanceBefore = bn(await ethers.provider.getBalance(owner)) - const tx = await withdrawalQueue.claimWithdrawal(1, { from: owner }) + await withdrawalQueue.claimWithdrawal(1, { from: owner, gasPrice: 0 }) - // tx.receipt.gasUsed is a workaround for coverage, because it ignores gasPrice=0 - assert.almostEqual(await ethers.provider.getBalance(owner), balanceBefore.add(bn(amount)), tx.receipt.gasUsed) + assert.equals(await ethers.provider.getBalance(owner), balanceBefore.add(bn(amount))) }) it('One cant claim not finalized or not existed request', async () => { @@ -594,29 +648,30 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - await withdrawalQueue.finalize([2], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([2], defaultShareRate, { from: steth.address, value: amount }) await assert.reverts(withdrawalQueue.claimWithdrawals([1], [0], { from: owner }), 'InvalidHint(0)') await assert.reverts(withdrawalQueue.claimWithdrawals([1], [2], { from: owner }), 'InvalidHint(2)') }) it('Cant withdraw token two times', async () => { - await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], defaultShareRate, { from: steth.address, value: amount }) await withdrawalQueue.claimWithdrawal(1, { from: owner }) await assert.reverts(withdrawalQueue.claimWithdrawal(1, { from: owner }), 'RequestAlreadyClaimed(1)') }) it('Discounted withdrawals produce less eth', async () => { - await withdrawalQueue.finalize([1], shareRate(150), { from: steth.address, value: ETH(150) }) + const batch = await withdrawalQueue.prefinalize([1], shareRate(150)) + await withdrawalQueue.finalize([1], shareRate(150), { from: steth.address, value: batch.ethToLock }) const balanceBefore = bn(await ethers.provider.getBalance(owner)) - assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(150)) + assert.equals(await withdrawalQueue.getLockedEtherAmount(), batch.ethToLock) + + await withdrawalQueue.claimWithdrawal(1, { from: owner, gasPrice: 0 }) - const tx = await withdrawalQueue.claimWithdrawal(1, { from: owner }) assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(0)) - // tx.receipt.gasUsed is a workaround for coverage, because it ignores gasPrice=0 - assert.almostEqual(bn(await ethers.provider.getBalance(owner)).sub(balanceBefore), ETH(150), tx.receipt.gasUsed) + assert.almostEqual(bn(await ethers.provider.getBalance(owner)).sub(balanceBefore), ETH(150), ALLOWED_ERROR_WEI) }) it('One can claim a lot of withdrawals with different discounts', async () => { @@ -648,63 +703,81 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) }) - context('claim scenarios', async () => { + context('claimWithdrawals()', () => { + const amount = ETH(20) + + beforeEach('Enqueue a request', async () => { + await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) + }) + + it('claims correct requests', async () => { + await steth.mintShares(owner, shares(300)) // 1 share to user and 299 shares to owner total = 300 ETH + await steth.approve(withdrawalQueue.address, StETH(300), { from: owner }) + + const secondRequestAmount = ETH(10) + await withdrawalQueue.requestWithdrawals([secondRequestAmount], owner, { from: owner }) + const secondRequestId = await withdrawalQueue.getLastRequestId() + await withdrawalQueue.finalize([secondRequestId], defaultShareRate, { from: steth.address, value: ETH(30) }) + + const balanceBefore = bn(await ethers.provider.getBalance(owner)) + await withdrawalQueue.claimWithdrawals([1, 2], [1, 1], { from: owner, gasPrice: 0 }) + assert.almostEqual(await ethers.provider.getBalance(owner), balanceBefore.add(bn(ETH(30))), ALLOWED_ERROR_WEI * 2) + }) + }) + + context('claim scenarios', () => { const requestCount = 5 const requestsAmounts = Array(requestCount).fill(StETH(1)) - const total = StETH(requestCount) - const normalizedShareRate = shareRate(+total / +(await steth.getSharesByPooledEth(total))) + let requestIds beforeEach(async () => { - await snapshot.rollback() await withdrawalQueue.requestWithdrawals(requestsAmounts, user, { from: user }) requestIds = await withdrawalQueue.getWithdrawalRequests(user, { from: user }) }) it('direct', async () => { + const normalizedShareRate = await currentRate() const balanceBefore = bn(await ethers.provider.getBalance(user)) const id = await withdrawalQueue.getLastRequestId() const batch = await withdrawalQueue.prefinalize([id], normalizedShareRate) - assert.equals(total, batch.ethToLock) + withdrawalQueue.finalize([id], normalizedShareRate, { from: steth.address, value: batch.ethToLock }) for (let index = 0; index < requestIds.length; index++) { const requestId = requestIds[index] - const tx = await withdrawalQueue.claimWithdrawal(requestId, { from: user }) - assert.emits(tx, 'WithdrawalClaimed', { requestId, owner: user, receiver: user, amountOfETH: ETH(1) }) + await withdrawalQueue.claimWithdrawal(requestId, { from: user, gasPrice: 0 }) } const balanceAfter = bn(await ethers.provider.getBalance(user)) - assert.equals(balanceAfter, balanceBefore.add(bn(total))) + assert.equals(balanceAfter, balanceBefore.add(bn(batch.ethToLock))) }) it('reverse', async () => { + const normalizedShareRate = await currentRate() const balanceBefore = bn(await ethers.provider.getBalance(user)) const id = await withdrawalQueue.getLastRequestId() const batch = await withdrawalQueue.prefinalize([id], normalizedShareRate) - assert.equals(total, batch.ethToLock) withdrawalQueue.finalize([id], normalizedShareRate, { from: steth.address, value: batch.ethToLock }) for (let index = requestIds.length - 1; index >= 0; index--) { const requestId = requestIds[index] - const tx = await withdrawalQueue.claimWithdrawal(requestId, { from: user }) - assert.emits(tx, 'WithdrawalClaimed', { requestId, owner: user, receiver: user, amountOfETH: ETH(1) }) + await withdrawalQueue.claimWithdrawal(requestId, { from: user, gasPrice: 0 }) } const balanceAfter = bn(await ethers.provider.getBalance(user)) - assert.equals(balanceAfter, balanceBefore.add(bn(total))) + assert.equals(balanceAfter, balanceBefore.add(bn(batch.ethToLock))) }) it('random', async () => { + const normalizedShareRate = await currentRate() const randomIds = [...requestIds].sort(() => 0.5 - Math.random()) const balanceBefore = bn(await ethers.provider.getBalance(user)) const id = await withdrawalQueue.getLastRequestId() const batch = await withdrawalQueue.prefinalize([id], normalizedShareRate) - assert.equals(total, batch.ethToLock) withdrawalQueue.finalize([id], normalizedShareRate, { from: steth.address, value: batch.ethToLock }) for (let index = 0; index < randomIds.length; index++) { const requestId = randomIds[index] - const tx = await withdrawalQueue.claimWithdrawal(requestId, { from: user }) - assert.emits(tx, 'WithdrawalClaimed', { requestId, owner: user, receiver: user, amountOfETH: ETH(1) }) + await withdrawalQueue.claimWithdrawal(requestId, { from: user, gasPrice: 0 }) } const balanceAfter = bn(await ethers.provider.getBalance(user)) - assert.equals(balanceAfter, balanceBefore.add(bn(total))) + assert.equals(balanceAfter, balanceBefore.add(bn(batch.ethToLock))) }) it('different rates', async () => { @@ -719,77 +792,40 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) totalDistributedEth.iadd(bn(batch.ethToLock)) } - const id = await withdrawalQueue.getLastRequestId() - await withdrawalQueue.finalize([id], await currentRate(), { from: steth.address, value: total }) for (let index = 0; index < requestIds.length; index++) { const requestId = requestIds[index] - await withdrawalQueue.claimWithdrawal(requestId, { from: user }) + await withdrawalQueue.claimWithdrawal(requestId, { from: user, gasPrice: 0 }) } const balanceAfter = bn(await ethers.provider.getBalance(user)) assert.equals(balanceAfter, balanceBefore.add(totalDistributedEth)) }) - }) - - context.skip('claim fuzzing', () => { - const fuzzClaim = async (perRequestWEI, requestCount, finalizedWEI) => { - await withdrawalQueue.requestWithdrawals(Array(requestCount).fill(perRequestWEI), user, { from: user }) - const requestIds = await withdrawalQueue.getWithdrawalRequests(user, { from: user }) - - const id = await withdrawalQueue.getLastRequestId() - await withdrawalQueue.finalize([id], shareRate(1), { from: steth.address, value: finalizedWEI }) - - const hints = await withdrawalQueue.findCheckpointHints( - requestIds, - 1, - await withdrawalQueue.getLastCheckpointIndex() - ) - - // this causes division by zero - const claimableEth = await withdrawalQueue.getClaimableEther(requestIds, hints).catch((e) => { - throw new Error( - // hack to fix error objects with bigInit causing `can't serialise bigInt` with wrong trace - JSON.parse(JSON.stringify(e, (_, value) => (typeof value === 'bigint' ? value.toString() : value))) - ) - }) - - const totalClaimable = claimableEth.reduce((s, i) => s.iadd(i) && s, bn(0)) - assert.equals(totalClaimable, finalizedWEI, `Total Claimable doesn't add up to finalized amount`) + it('100% discount', async () => { const balanceBefore = bn(await ethers.provider.getBalance(user)) - await withdrawalQueue.claimWithdrawals(requestIds, hints, { from: user }) + const id = await withdrawalQueue.getLastRequestId() + const batches = await withdrawalQueue.prefinalize([id], 1) + assert.equals(batches.ethToLock, 0) + withdrawalQueue.finalize([id], 1, { from: steth.address, value: batches.ethToLock }) + for (let index = 0; index < requestIds.length; index++) { + const requestId = requestIds[index] + const tx = await withdrawalQueue.claimWithdrawal(requestId, { from: user, gasPrice: 0 }) + assert.emits(tx, 'WithdrawalClaimed', { requestId, owner: user, receiver: user, amountOfETH: 0 }) + } const balanceAfter = bn(await ethers.provider.getBalance(user)) - assert.equals(balanceBefore.addn(finalizedWEI), balanceAfter, `Total Claimed doesn't add up to finalized amount`) - } - - it('distribute&claim 10wei per 100*100WEI requests ', async () => { - await fuzzClaim(100, 100, 1000) - }) - - it('distribute&claim 1wei per 100*100WEI requests', async () => { - await fuzzClaim(100, 100, 100) - }) - - it('distribute&claim 1 wei per 10*MAX_STETH_WITHDRAWAL_AMOUNT requests', async () => { - const MAX_STETH_WITHDRAWAL_AMOUNT = await withdrawalQueue.MAX_STETH_WITHDRAWAL_AMOUNT() - // account for stEth~ { - const numOfRequests = 10 - const requests = Array(numOfRequests).fill(ETH(20)) - const discountedPrices = Array(numOfRequests) - .fill() - .map((_, i) => ETH(i)) - const sharesPerRequest = await steth.getSharesByPooledEth(ETH(20)) - const discountShareRates = discountedPrices.map((p) => shareRate(+p / +sharesPerRequest)) - + context('findCheckpointHints()', () => { beforeEach(async () => { + const numOfRequests = 10 + const requests = Array(numOfRequests).fill(ETH(20)) + const discountedPrices = Array(numOfRequests) + .fill() + .map((_, i) => ETH(i)) + const sharesPerRequest = await steth.getSharesByPooledEth(ETH(20)) + const discountShareRates = discountedPrices.map((p) => shareRate(+p / +sharesPerRequest)) + await withdrawalQueue.requestWithdrawals(requests, owner, { from: user }) for (let i = 1; i <= numOfRequests; i++) { await withdrawalQueue.finalize([i], discountShareRates[i - 1], { @@ -885,8 +921,8 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) it('returns not found when indexes have negative overlap', async () => { - const batch = await withdrawalQueue.prefinalize.call([requestId], shareRate(300)) - await withdrawalQueue.finalize([requestId], shareRate(300), { from: steth.address, value: batch.ethToLock }) + const batch = await withdrawalQueue.prefinalize.call([requestId], defaultShareRate) + await withdrawalQueue.finalize([requestId], defaultShareRate, { from: steth.address, value: batch.ethToLock }) const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() const hints = await withdrawalQueue.findCheckpointHints( [requestId], @@ -898,8 +934,8 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) it('returns hints array with one item for list from single request id', async () => { - const batch = await withdrawalQueue.prefinalize.call([requestId], shareRate(300)) - await withdrawalQueue.finalize([requestId], shareRate(300), { from: steth.address, value: batch.ethToLock }) + const batch = await withdrawalQueue.prefinalize.call([requestId], defaultShareRate) + await withdrawalQueue.finalize([requestId], defaultShareRate, { from: steth.address, value: batch.ethToLock }) const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() const hints = await withdrawalQueue.findCheckpointHints([requestId], 1, lastCheckpointIndex) assert.equal(hints.length, 1) @@ -958,29 +994,6 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) }) - context('claimWithdrawals()', () => { - const amount = ETH(20) - - beforeEach('Enqueue a request', async () => { - await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - }) - - it('claims correct requests', async () => { - await steth.mintShares(owner, shares(300)) // 1 share to user and 299 shares to owner total = 300 ETH - await steth.approve(withdrawalQueue.address, StETH(300), { from: owner }) - - const secondRequestAmount = ETH(10) - await withdrawalQueue.requestWithdrawals([secondRequestAmount], owner, { from: owner }) - const secondRequestId = await withdrawalQueue.getLastRequestId() - await withdrawalQueue.finalize([secondRequestId], shareRate(300), { from: steth.address, value: ETH(30) }) - - const balanceBefore = bn(await ethers.provider.getBalance(owner)) - const tx = await withdrawalQueue.claimWithdrawals([1, 2], [1, 1], { from: owner, gasPrice: 0 }) - // tx.receipt.gasUsed is a workaround for coverage, because it ignores gasPrice=0 - assert.almostEqual(await ethers.provider.getBalance(owner), balanceBefore.add(bn(ETH(30))), tx.receipt.gasUsed) - }) - }) - context('requestWithdrawals()', () => { it('works correctly with non empty payload and different tokens', async () => { await steth.mintShares(user, shares(10)) @@ -1118,7 +1131,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) }) - context('Transfer request', async () => { + context('Transfer request', () => { const amount = ETH(300) let requestId @@ -1162,7 +1175,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, }) it("One can't change claimed request", async () => { - await withdrawalQueue.finalize([requestId], shareRate(300), { from: steth.address, value: amount }) + await withdrawalQueue.finalize([requestId], defaultShareRate, { from: steth.address, value: amount }) await withdrawalQueue.claimWithdrawal(requestId, { from: user }) await assert.reverts( diff --git a/test/common/lib/mem-utils.test.sol b/test/common/lib/mem-utils.test.sol index cb4e462f4..c1600f2d4 100644 --- a/test/common/lib/mem-utils.test.sol +++ b/test/common/lib/mem-utils.test.sol @@ -457,78 +457,4 @@ contract MemUtilsTest is Test { bytes32(0x2222222222222222222222222222222222222222222222222222222222222222) )); } - - /// - /// keccakUint256Array - /// - - function test_keccakUint256Array_calcs_keccak_over_a_uint_array() external pure { - uint256[] memory array = new uint256[](5); - array[0] = uint256(0x1111111111111111111111111111111111111111111111111111111111111111); - array[1] = uint256(0x2222222222222222222222222222222222222222222222222222222222222222); - array[2] = uint256(0x3333333333333333333333333333333333333333333333333333333333333333); - array[3] = uint256(0x4444444444444444444444444444444444444444444444444444444444444444); - array[4] = uint256(0x5555555555555555555555555555555555555555555555555555555555555555); - - bytes32 expected = keccak256(abi.encodePacked(array)); - bytes32 actual = MemUtils.keccakUint256Array(array); - - Assert.equal(actual, expected); - } - - function test_keccakUint256Array_calcs_keccak_over_an_empty_array() external pure { - uint256[] memory array = new uint256[](0); - - bytes32 expected = keccak256(abi.encodePacked(array)); - bytes32 actual = MemUtils.keccakUint256Array(array); - - Assert.equal(actual, expected); - } - - /// - /// trimUint256Array - /// - - function test_trimUint256Array_decreases_length_of_a_uint_array() external pure { - uint256[] memory array = new uint256[](5); - array[0] = uint256(0x1111111111111111111111111111111111111111111111111111111111111111); - array[1] = uint256(0x2222222222222222222222222222222222222222222222222222222222222222); - array[2] = uint256(0x3333333333333333333333333333333333333333333333333333333333333333); - array[3] = uint256(0x4444444444444444444444444444444444444444444444444444444444444444); - array[4] = uint256(0x5555555555555555555555555555555555555555555555555555555555555555); - - MemUtils.trimUint256Array(array, 2); - - Assert.equal(array.length, 3); - Assert.equal(array[0], uint256(0x1111111111111111111111111111111111111111111111111111111111111111)); - Assert.equal(array[1], uint256(0x2222222222222222222222222222222222222222222222222222222222222222)); - Assert.equal(array[2], uint256(0x3333333333333333333333333333333333333333333333333333333333333333)); - - Assert.equal(abi.encodePacked(array), abi.encodePacked( - bytes32(0x1111111111111111111111111111111111111111111111111111111111111111), - bytes32(0x2222222222222222222222222222222222222222222222222222222222222222), - bytes32(0x3333333333333333333333333333333333333333333333333333333333333333) - )); - } - - function test_trimUint256Array_allows_trimming_to_zero_length() external pure { - uint256[] memory array = new uint256[](3); - array[0] = uint256(0x1111111111111111111111111111111111111111111111111111111111111111); - array[1] = uint256(0x2222222222222222222222222222222222222222222222222222222222222222); - array[2] = uint256(0x3333333333333333333333333333333333333333333333333333333333333333); - - MemUtils.trimUint256Array(array, 3); - - Assert.empty(array); - } - - function test_trimUint256Array_reverts_on_trying_to_trim_by_more_than_length() external { - uint256[] memory array = new uint256[](3); - array[0] = uint256(0x1111111111111111111111111111111111111111111111111111111111111111); - array[1] = uint256(0x2222222222222222222222222222222222222222222222222222222222222222); - array[2] = uint256(0x3333333333333333333333333333333333333333333333333333333333333333); - - vm.expectRevert(); - MemUtils.trimUint256Array(array, 4); - } } diff --git a/test/helpers/contract-stub.js b/test/helpers/contract-stub.js new file mode 100644 index 000000000..9ce10a777 --- /dev/null +++ b/test/helpers/contract-stub.js @@ -0,0 +1,308 @@ +const hre = require('hardhat') + +const MAX_UINT256 = BigInt(2n ** 256n - 1n).toString() +const EMPTY_FRAME_ID = MAX_UINT256 +const GET_STORAGE_ADDRESS_METHOD_ID = '0x00000001' + +const ContractStubStorage = hre.artifacts.require('ContractStubStorage') + +function ContractStub(artifact) { + return new ContractStubBuilder(artifact) +} + +function isTruffleContractName(maybeContractName) { + return typeof maybeContractName === 'string' +} + +function isTruffleContractFactory(maybeContractFactory) { + return typeof maybeContractFactory.new === 'function' +} + +function isTruffleContractInstance(maybeContractInstance) { + return ( + typeof maybeContractInstance.constructor === 'function' && + isTruffleContractFactory(maybeContractInstance.constructor) + ) +} + +function prepareTruffleArtifact(artifact) { + if (isTruffleContractName(artifact)) return { factory: hre.artifacts.require(artifact), instance: undefined } + if (isTruffleContractFactory(artifact)) return { factory: artifact, instance: undefined } + if (isTruffleContractInstance(artifact)) return { factory: artifact.constructor, instance: artifact } + throw new Error(`Unexpected artifact value "${artifact}"`) +} + +class ContractStubBuilder { + constructor(truffleArtifact) { + this._artifact = prepareTruffleArtifact(truffleArtifact) + this._methodNames = [] + this._stubBuildSteps = [] + this._currentFrame = EMPTY_FRAME_ID + } + + async create(txDetails) { + if (!this._artifact.instance) { + const stub = await hre.artifacts.require('ContractStub').new(GET_STORAGE_ADDRESS_METHOD_ID, txDetails) + this._artifact.instance = await this._artifact.factory.at(stub.address) + } + return this._stub(txDetails) + } + + async update(txDetails) { + await this._stub(txDetails) + } + + /** + * Stubs the method call + * + * @typedef {number | string | BN | BigInt } Numberable + * + * @typedef {Object} TypedTupleConfig - stores typed tuple value + * @property {string[]} type - types of the tuple elements + * @property {unknown[]} value - values of the tuple elements + * + * @typedef {Object} CustomErrorConfig - describes the custom error to revert with + * @property {string} name - the name of the error + * @property {TypedTupleConfig} args - the arguments of the error + * + * @typedef {Object} EventArgsConfig + * @property {string[]} type - types of the event arguments + * @property {unknown[]} value - values of the event arguments + * @property {boolean[]} indexed - array with flag whether the arg is indexed or not + * + * @typedef {Object} RevertConfig - describes the value to revert with + * @property {string=} reason - the error message to revert with + * @property {CustomErrorConfig=} error - the custom error to revert with + * + * @typedef {Object} ForwardETHConfig - the info about unconditional (even if recipient is not payable) + * ETH forwarding from the stub contract + * @property {string} recipient - address to forward ETH from the stub contract + * @property {Numberable} value - the amount of ETH to forward + * + * @typedef {Object} CallConfig - low level call method params + * @property {string} callee - address of the account to call + * @property {string=} data - msg.data to pass on call. By default no data passed + * @property {Numberable=} value - the amount of ETH to send with call. By default is 0.p + * @property {Numberable=} gas - the gas limit for the call. By default uses all gas + * + * @typedef {Object} EventConfig - the config of the event + * @property {string} name - name of the event + * @property {EventArgsConfig} args - arguments of the event + * + * @typedef {Object} MethodStubConfig + * @property {TypedTupleConfig=} input - when passed, stub will be triggered only when method is called + * with data matched input. When omitted, stub will be triggered for any call to method + * @property {TypedTupleConfig=} return - the value to return when stub is called + * @property {RevertConfig=} revert - the error to revert with when stub is called + * @property {ForwardETHConfig[]=} ethForwards - the info about unconditional (even if recipient + * is not payable) ETH forwarding from the stub contract + * @property {boolean=} traceable - whether to emit event on stub call. The default value is false + * @property {number=} nextFrame - the frame to set as active when the stub will be called. + * If not passed ContractStub stays in the same frame. + * @property {CallConfig[]=} calls - the list of external calls to make from method stub when it's triggered + * @property {EventConfig[]=} emits - the list of events to emit from method stub when it's triggered + * + * @param {string} methodName - name of the method to stub + * @param {MethodStubConfig} config - config of the method stub + */ + on(methodName, config = {}) { + this._methodNames.push(methodName) + this._stubBuildSteps.push({ currentFrame: this._currentFrame, ...config }) + return this + } + + /** + * Sets the active frame of the contract stub + * + * @param {number} frame - the number of the frame to set as active + */ + frame(frame) { + this._currentFrame = frame + return this + } + + async _stub(txDetails) { + for (let i = 0; i < this._methodNames.length; ++i) { + await this._stubMethod( + await this._getContractStubStorage(this._artifact.instance), + this._artifact.instance.abi, + this._methodNames[i], + this._stubBuildSteps[i], + txDetails + ) + } + + return this._artifact.instance + } + + async _stubMethod(contractStubStorage, abi, methodName, config, txDetails) { + const configParser = new ContractStubConfigParser() + const { currentFrame, stub } = configParser.parse(this._getMethodSignature(abi, methodName), config) + await contractStubStorage.addMethodStub(currentFrame, stub, txDetails) + } + + _getMethodSignature(abi, methodName) { + if (methodName === 'receive') return '0x' + const methodAbi = abi.filter((abi) => abi.type === 'function' && abi.name === methodName) + + if (methodAbi.length > 1) { + throw new Error('Support of methods overloading has not implemented yet') + } + return methodAbi[0].signature + } + + async _getContractStubStorage(stubInstance) { + const storageAddress = await hre.web3.eth.call({ + to: stubInstance.address, + data: GET_STORAGE_ADDRESS_METHOD_ID, + }) + return ContractStubStorage.at(storageAddress) + } +} + +class TypedTuple { + constructor(type, value) { + this.type = type + this.value = value + } + + static empty() { + return new TypedTuple([], []) + } + + static seed(type, value) { + return new TypedTuple([type], [value]) + } + + static create(type, value) { + return new TypedTuple(type, value) + } + + append(type, value) { + this.type.push(type) + this.value.push(value) + return this + } +} + +const EMPTY_TYPED_TUPLE = Object.freeze(TypedTuple.empty()) + +class ContractStubConfigParser { + parse(methodSignature, config) { + return { + currentFrame: this._parseCurrentFrame(config), + stub: [ + this._parseInput(methodSignature, config), + this._parseOutput(config), + this._parseIsRevert(config), + [ + this._parseTraceable(config), + this._parseNextFrame(config), + this._parseLogs(config), + this._parseCalls(config), + this._parseETHForwards(config), + ], + ], + } + } + + _parseInput(methodSignature, config) { + return methodSignature + this._encode(config.input || EMPTY_TYPED_TUPLE).slice(2) + } + + _parseOutput(config) { + if (config.return) { + return this._encode(config.return) + } else if (config.revert && config.revert.reason === 'outOfGas') { + return this._encode(EMPTY_TYPED_TUPLE) + } else if (config.revert && config.revert.reason !== undefined) { + return this._encodeError({ name: 'Error', args: TypedTuple.create(['string'], [config.revert.reason]) }) + } else if (config.revert && config.revert.error) { + return this._encodeError(config.revert.error) + } + return this._encode(EMPTY_TYPED_TUPLE) + } + + _parseLogs(config) { + if (!config.emits || config.emits.length === 0) return [] + return config.emits.map((emitConfig) => { + // required field so just read it + const name = emitConfig.name + // if not passed event considered as without arguments + const args = emitConfig.args + ? { type: emitConfig.args.type, value: emitConfig.args.value } + : { type: [], value: [] } + // when indexed is passed take its values or consider all fields as non-indexed in other cases + const indexed = emitConfig.args && emitConfig.args.indexed ? emitConfig.args.indexed : args.value.map(() => false) + // filter all indexed args indices to pass them as topics + const indexedIndices = indexed.map((indexed, index) => (indexed ? index : -1)).filter((i) => i >= 0) + // filter all non-indexed args indices to pass them as data + const nonIndexedIndices = indexed.map((indexed, index) => (indexed ? -1 : index)).filter((i) => i >= 0) + + // signature of the event always goes as topic1 + const signature = this._eventSignature(name, args.type) + // collect argument into topics via ABI encoding + const topics = indexedIndices.map((i) => this._encode(TypedTuple.seed(args.type[i], args.value[i]))) + // collect non-indexed args to encode them via ABI encoder and use it as data + const nonIndexedArgs = nonIndexedIndices + .map((i) => [args.type[i], args.value[i]]) + .reduce((args, [type, value]) => args.append(type, value), TypedTuple.empty()) + + const logType = topics.length + 1 // first topic is event signature + return [ + logType, + this._encode(nonIndexedArgs), + signature, + logType >= 2 ? topics[0] : '0x0', + logType >= 3 ? topics[1] : '0x0', + logType === 4 ? topics[2] : '0x0', + ] + }) + } + + _parseETHForwards(config) { + return (config.ethForwards || []).map((forward) => [forward.recipient, forward.value.toString()]) + } + + _parseIsRevert(config) { + return !!config.revert + } + + _parseNextFrame(config) { + return config.nextFrame ?? EMPTY_FRAME_ID + } + + _parseCurrentFrame(config) { + return config.currentFrame ?? EMPTY_FRAME_ID + } + + _parseTraceable(config) { + return config.traceable ?? false + } + + _parseCalls(config) { + return (config.calls ?? []).map((call) => [call.callee, call.data ?? '0x', call.value ?? 0, call.gas ?? 0]) + } + + _encode(args) { + return hre.ethers.utils.defaultAbiCoder.encode(args.type, args.value) + } + + _encodeError(error) { + const args = error.args ?? EMPTY_TYPED_TUPLE + const signature = this._errorSignature(error.name, args.type) + return signature + this._encode(args).slice(2) + } + + _errorSignature(name, argTypes) { + const fullName = `${name}(${argTypes.join(',')})` + return hre.web3.utils.soliditySha3(fullName).slice(0, 10) + } + + _eventSignature(name, argTypes) { + const fullName = `${name}(${argTypes.join(',')})` + return hre.web3.utils.soliditySha3(fullName) + } +} + +module.exports = { ContractStub, ContractStubBuilder } diff --git a/test/helpers/factories.js b/test/helpers/factories.js index c080ddb22..a87939571 100644 --- a/test/helpers/factories.js +++ b/test/helpers/factories.js @@ -369,6 +369,8 @@ async function postSetup({ oracle, legacyOracle, consensusContract, + stakingModules, + burner, }) { await pool.initialize(lidoLocator.address, eip712StETH.address, { value: ETH(1) }) @@ -376,6 +378,9 @@ async function postSetup({ await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), voting.address, { from: voting.address }) await oracle.grantRole(await oracle.MANAGE_CONSENSUS_VERSION_ROLE(), voting.address, { from: voting.address }) await oracle.grantRole(await oracle.SUBMIT_DATA_ROLE(), voting.address, { from: voting.address }) + for (const stakingModule of stakingModules) { + await burner.grantRole(await burner.REQUEST_BURN_SHARES_ROLE(), stakingModule.address, { from: appManager.address }) + } await legacyOracle.initialize(lidoLocator.address, consensusContract.address) diff --git a/test/helpers/staking-modules.js b/test/helpers/staking-modules.js index edc4ecce9..d0df8e6b8 100644 --- a/test/helpers/staking-modules.js +++ b/test/helpers/staking-modules.js @@ -98,5 +98,7 @@ async function setupNodeOperatorsRegistry({ dao, acl, lidoLocator, stakingRouter } module.exports = { + NodeOperatorsRegistry, + NodeOperatorsRegistryMock, setupNodeOperatorsRegistry, } diff --git a/test/helpers/stubs/generic.stub.js b/test/helpers/stubs/generic.stub.js deleted file mode 100644 index df48b8d40..000000000 --- a/test/helpers/stubs/generic.stub.js +++ /dev/null @@ -1,190 +0,0 @@ -const hre = require('hardhat') -const { ZERO_ADDRESS } = require('../constants') - -class GenericStub { - static LOG_TYPE = Object.freeze({ - LOG0: 0, - LOG1: 1, - LOG2: 2, - LOG3: 3, - LOG4: 4, - }) - - static GenericStubContract = hre.artifacts.require('GenericStub') - - static async new(contractName) { - const stubInstance = await GenericStub.GenericStubContract.new() - const StubbedContractFactory = hre.artifacts.require(contractName) - return StubbedContractFactory.at(stubInstance.address) - } - - static async addState(stubbedContract) { - const stubInstance = await GenericStub.GenericStubContract.at(stubbedContract.address) - await stubInstance.GenericStub__addState() - } - - static async setState(stubbedContract, stateIndex) { - const stubInstance = await GenericStub.GenericStubContract.at(stubbedContract.address) - await stubInstance.GenericStub__setState(stateIndex) - } - - /** - * @typedef {object} TypedTuple - stores a info about tuple type & value - * @property {string[]} - tuple with type names - * @property {any[]} - tuple with values for types - * - * @param {object} stubbedContract instance of the GenericStub contract to add stub - * - * @param {string} methodName name of the method to stub - * - * @param {object} config stubbed method params - * @param {object} state describes the state were stub declared and next transition - * @param {number} state.current index of the state where stub will be added - * @param {number} state.next index of the state which will be activated after the stub called - * @param {TypedTuple} [config.input] the input value to trigger the stub - * @param {TypedTuple} [config.return] the output value to return or revert from stub - * @param {object} [config.revert] the revert info when stub must finish with error - * @param {string} [config.revert.reason] the revert reason. Used when method reverts with string message - * @param {string} [config.revert.error] the custom error name when method must revert with custom error - * @param {TypedTuple} [config.revert.args] the arguments info for custom error - * @param {object} [config.forwardETH] amount and recipient where to send ETH - * @param {string} config.forwardETH.recipient recipient address of the ETH - * @param {object} config.forwardETH.value amount of ETH to send - * @param {object[]} [config.emit] events to emit when stub called - * @param {string} config.emit.name name of the event to emit - * @param {object} [config.emit.args] arguments of the event - * @param {string[]} [config.emit.args.type] tuple with type names - * @param {any[]} [config.emit.args.value] tuple with values for types - * @param {bool[]} [config.emit.args.indexed] is value indexed or not - */ - static async stub(stubbedContract, methodName, config = {}) { - const stubInstance = await GenericStub.GenericStubContract.at(stubbedContract.address) - - const { abi: abis } = stubbedContract - const methodAbis = abis.filter((abi) => abi.type === 'function' && abi.name === methodName) - - if (methodAbis.length > 1) { - throw new Error('Support of methods overloading has not implemented yet') - } - const [methodAbi] = methodAbis - - const configParser = new GenericStubConfigParser() - const { currentState, ...parsedConfig } = configParser.parse(methodAbi.signature, config) - - if (currentState === undefined) { - await stubInstance.GenericStub__addStub(Object.values(parsedConfig)) - } else { - await stubInstance.GenericStub__addStub(currentState, Object.values(parsedConfig)) - } - } -} - -module.exports = { - GenericStub, -} - -class GenericStubConfigParser { - parse(methodAbi, config) { - return { - input: this._parseInput(methodAbi, config), - output: this._parseOutput(config), - logs: this._parseLogs(config), - forwardETH: this._parseForwardETH(config), - isRevert: this._parseIsRevert(config), - currentState: this._parseCurrentState(config), - nextState: this._parseNextState(config), - } - } - - _parseInput(methodSignature, config) { - return methodSignature + this._encode(config.input || { type: [], value: [] }).slice(2) - } - - _parseOutput(config) { - if (config.return) { - return this._encode(config.return) - } - if (config.revert) { - return config.revert.error - ? this._encodeError(config.revert) - : this._encodeError({ error: 'Error', args: { type: ['string'], value: [config.revert.reason || ''] } }) - } - return this._encode({ type: [], value: [] }) - } - - _parseLogs(config) { - if (!config.emit || config.emit.length === 0) return [] - return config.emit.map((event) => { - // required field so just read it - const name = event.name - // if not passed event considered as without arguments - const args = event.args ? { type: event.args.type, value: event.args.value } : { type: [], value: [] } - // when indexed is passed take its values or consider all fields as non-indexed in other cases - const indexed = event.args && event.args.indexed ? event.args.indexed : args.value.map(() => false) - // filter all indexed args indices to pass them as topics - const indexedIndices = indexed.map((indexed, index) => (indexed ? index : -1)).filter((i) => i >= 0) - // filter all non-indexed args indices to pass them as data - const nonIndexedIndices = indexed.map((indexed, index) => (indexed ? -1 : index)).filter((i) => i >= 0) - - // signature of the event always goes as topic1 - const signature = this._eventSignature(name, args.type) - // collect argument into topics via ABI encoding - const topics = indexedIndices.map((i) => this._encode({ type: [args.type[i]], value: [args.value[i]] })) - // collect non-indexed args to encode them via ABI encoder and use it as data - const nonIndexedArgs = nonIndexedIndices - .map((i) => [args.type[i], args.value[i]]) - .reduce((args, [type, value]) => ({ type: [...args.type, type], value: [...args.value, value] }), { - type: [], - value: [], - }) - - const logType = topics.length + 1 // first topic is event signature - return [ - logType, - this._encode(nonIndexedArgs), - signature, - logType >= 2 ? topics[0] : '0x0', - logType >= 3 ? topics[1] : '0x0', - logType === 4 ? topics[2] : '0x0', - ] - }) - } - - _parseForwardETH(config) { - const { forwardETH = { recipient: ZERO_ADDRESS, value: 0 } } = config - return [forwardETH.recipient, forwardETH.value] - } - - _parseIsRevert(config) { - return !!config.revert - } - - _parseNextState(config) { - if (!config.state || !config.state.next) return 0 - return config.state.next + 1 - } - - _parseCurrentState(config) { - if (!config.state || !config.state.current) return undefined - return config.state.current - } - - _encode({ type, value }) { - return hre.ethers.utils.defaultAbiCoder.encode(type, value) - } - - _encodeError({ error, args }) { - const signature = this._errorSignature(error, args.type) - return signature + this._encode(args).slice(2) - } - - _errorSignature(name, argTypes) { - const fullName = `${name}(${argTypes.join(',')})` - return hre.ethers.utils.keccak256(hre.ethers.utils.toUtf8Bytes(fullName)).slice(0, 10) - } - - _eventSignature(name, argTypes) { - const fullName = `${name}(${argTypes.join(',')})` - return hre.ethers.utils.keccak256(hre.ethers.utils.toUtf8Bytes(fullName)) - } -} diff --git a/test/helpers/stubs/staking-module.stub.js b/test/helpers/stubs/staking-module.stub.js deleted file mode 100644 index 994a33d10..000000000 --- a/test/helpers/stubs/staking-module.stub.js +++ /dev/null @@ -1,61 +0,0 @@ -const { GenericStub } = require('./generic.stub') -const { FakeValidatorKeys } = require('../../helpers/signing-keys') - -class StakingModuleStub extends GenericStub { - static new() { - return GenericStub.new('IStakingModule') - } - - static async stubGetStakingModuleSummary( - stakingModuleStub, - { totalExitedValidators, totalDepositedValidators, depositableValidatorsCount }, - configOverrides = {} - ) { - await GenericStub.stub(stakingModuleStub, 'getStakingModuleSummary', { - return: { - type: ['uint256', 'uint256', 'uint256'], - value: [totalExitedValidators, totalDepositedValidators, depositableValidatorsCount], - }, - ...configOverrides, - }) - } - - /** - * @param {object} stakingModuleStub instance of GenericStub contract - * @param {object} config config for the method stub - * @param {object} config.input the input stub must return value for. When not set - * config.return value will be returned for any input - * @param {number} config.input.depositsCount the input value of the _depositsCount to trigger stub - * @param {string} config.input.calldata the input value of the _calldata to trigger stub - * @param {object} config.return the config for the return value - * @param {object} config.return.depositData the instance of the FakeValidatorKeys to return from the stub. - * If not set will be used FakeValidatorKeys instance of default length - * @param {number} config.return.depositDataLength the length of the FakeValidatorKeys instance - * to use for return value - * @param {string} config.return.publicKeysBatch the bytes batch of the public keys - * @param {string} config.return.signaturesBatch the bytes batch of the signatures - */ - static async stubObtainDepositData(stakingModuleStub, config) { - const input = config.input - ? { type: ['uint256', 'bytes'], value: [config.input.depositsCount, config.input.calldata] } - : undefined - const depositData = config.return.depositData - ? config.return.depositData - : new FakeValidatorKeys(config.return.depositDataLength) - const [defaultPublicKeysBatch, defaultSignaturesBatch] = depositData.slice() - await GenericStub.stub(stakingModuleStub, 'obtainDepositData', { - input, - return: { - type: ['bytes', 'bytes'], - value: [ - config.return.publicKeysBatch || defaultPublicKeysBatch, - config.return.signaturesBatch || defaultSignaturesBatch, - ], - }, - }) - } -} - -module.exports = { - StakingModuleStub, -} diff --git a/test/helpers/utils.js b/test/helpers/utils.js index ee296ec8b..c4906fd20 100644 --- a/test/helpers/utils.js +++ b/test/helpers/utils.js @@ -132,24 +132,23 @@ const calcSharesMintedAsFees = (rewards, fee, feePoints, prevTotalShares, newTot const limitRebase = (limitE9, preTotalPooledEther, preTotalShares, clBalanceUpdate, elBalanceUpdate, sharesToBurn) => { const bnE9 = toBN(e9(1)) - let accumulatedRebase = toBN(0) - const clRebase = toBN(clBalanceUpdate).mul(bnE9).div(toBN(preTotalPooledEther)) - accumulatedRebase = accumulatedRebase.add(clRebase) - if (limitE9.lte(accumulatedRebase)) { + const etherLimit = limitE9.mul(toBN(preTotalPooledEther)).div(bnE9).add(toBN(preTotalPooledEther)) + + const clRebase = toBN(preTotalPooledEther).add(toBN(clBalanceUpdate)) + if (etherLimit.lte(clRebase)) { return { elBalanceUpdate: 0, sharesToBurn: 0 } } - let remainLimit = limitE9.sub(accumulatedRebase) - const remainEther = remainLimit.mul(toBN(preTotalPooledEther)).div(bnE9) + const remainEther = etherLimit.sub(clRebase) if (remainEther.lte(toBN(elBalanceUpdate))) { return { elBalanceUpdate: remainEther, sharesToBurn: 0 } } - const elRebase = toBN(elBalanceUpdate).mul(bnE9).div(toBN(preTotalPooledEther)) - accumulatedRebase = accumulatedRebase.add(elRebase) - remainLimit = toBN(limitE9).sub(accumulatedRebase) + const postTotalPooledEther = clRebase.add(toBN(elBalanceUpdate)) + const rebaseLimitPlus1 = toBN(limitE9).add(bnE9) + const tvlRate = toBN(postTotalPooledEther).mul(bnE9).div(toBN(preTotalPooledEther)) - const remainShares = remainLimit.mul(toBN(preTotalShares)).div(bnE9.add(remainLimit)) + const remainShares = toBN(preTotalShares).mul(rebaseLimitPlus1.sub(tvlRate)).div(rebaseLimitPlus1) if (remainShares.lte(toBN(sharesToBurn))) { return { elBalanceUpdate, sharesToBurn: remainShares } @@ -170,6 +169,14 @@ function getFirstEventArgs(receipt, eventName, abi = undefined) { return events[0].args } +function addSendWithResult(method) { + method.sendWithResult = async (...args) => { + const result = await method.call(...args) + await method(...args) + return result + } +} + module.exports = { ZERO_ADDRESS, ZERO_HASH, @@ -201,5 +208,6 @@ module.exports = { calcSharesMintedAsFees, getFirstEventArgs, calcShareRateDeltaE27, + addSendWithResult, limitRebase, } diff --git a/test/helpers/wei.js b/test/helpers/wei.js index 7cb93aac2..776f672d7 100644 --- a/test/helpers/wei.js +++ b/test/helpers/wei.js @@ -1,98 +1,90 @@ const hre = require('hardhat') -function wei(...args) { - return parseWeiExpression(weiExpressionTag(...args)) +const ETHER_UNITS = [ + 'wei', + 'kwei', + 'mwei', + 'gwei', + 'nano', + 'nanoether', + 'micro', + 'microether', + 'milli', + 'milliether', + 'ether', + 'kether', + 'grand', + 'mether', + 'gether', + 'tether', +] + +const weiToString = (templateOrStringifiable, ...values) => processWeiTagInput(templateOrStringifiable, values) + +const weiToBigInt = (templateOrStringifiable, ...values) => BigInt(processWeiTagInput(templateOrStringifiable, values)) + +const wei = Object.assign(weiToBigInt, { + int: weiToBigInt, + str: weiToString, + min: (...values) => { + if (values.length === 0) { + throw new Error(`No arguments provided to wei.min() call`) + } + return values.reduce((min, value) => (wei.int(value) < min ? wei.int(value) : min), wei.int(values[0])) + }, + max: (...values) => { + if (values.length === 0) { + throw new Error(`No arguments provided to wei.min() call`) + } + return values.reduce((max, value) => (wei.int(value) > max ? wei.int(value) : max), wei.int(values[0])) + }, +}) + +function processWeiTagInput(templateOrStringifiable, values) { + return parseWeiExpression( + isTemplateStringArray(templateOrStringifiable) + ? templateToString(templateOrStringifiable, values) + : stringifiableToString(templateOrStringifiable) + ) } -wei.int = (...args) => { - if (args.length === 0) { - throw new Error('No arguments provided to wei.int() call') - } - - // when str is used as JS tag it first argument will be array of strings - if (Array.isArray(args[0]) && args[0].every((e) => typeof e === 'string')) { - return wei(...args) - } - - // when first argument is string, consider it as wei expression - if (typeof args[0] === 'string') { - return wei(...args) - } - - // in all other cases just cast first item to string and convert it to BigInt - return BigInt(args[0].toString()) -} - -wei.str = (...args) => { - if (args.length === 0) { - throw new Error('No arguments provided to wei.str() call') - } +function parseWeiExpression(expression) { + const [amount, unit = 'wei'] = expression + .replaceAll('_', '') // remove all _ from numbers written like '100_00' + .trim() // remove all leading and trailing spaces + .split(' ') // split amount and unit parts + .filter((v) => !!v) // remove all empty strings if value had redundant spaces between amount and unit parts + .map((v) => v.toLowerCase()) // needed for units - // when str is used as JS tag it first argument will be array of strings - if (Array.isArray(args[0]) && args[0].every((e) => typeof e === 'string')) { - return wei(...args).toString() + if (!Number.isFinite(+amount)) { + throw new Error(`Wei Parse Error: Amount "${amount}" is not a valid number`) } - // when first argument is string, consider it as wei expression - if (typeof args[0] === 'string') { - return wei(...args).toString() + if (!isValidEtherUnit(unit)) { + throw new Error(`Wei Parse Error: unsupported unit value: ${unit}`) } - // in all other cases just cast first item to string - return args[0].toString() -} - -wei.min = (...values) => { - if (values.length === 0) { - throw new Error(`No arguments provided to wei.min() call`) - } - return values.reduce((min, value) => (wei.int(value) < min ? wei.int(value) : min), wei.int(values[0])) + return hre.web3.utils.toWei(amount, unit) } -wei.max = (...values) => { - if (values.length === 0) { - throw new Error(`No arguments provided to wei.min() call`) - } - return values.reduce((max, value) => (wei.int(value) > max ? wei.int(value) : max), wei.int(values[0])) +function isValidEtherUnit(maybeUnit) { + return ETHER_UNITS.some((unit) => unit === maybeUnit) } -function weiExpressionTag(strings, ...values) { - if (!Array.isArray(strings) && typeof strings !== 'string') { - throw new Error(`wei was used with invalid arg type. Make sure that was passed valid JS template string`) - } - // when wei used not like js tag but called like regular function - // the first argument will be string instead of array of strings - if (typeof strings === 'string') { - strings = [strings] - } - - // case when wei used without arguments - if (strings.length === 1 && strings[0] === '' && values.length === 0) { - throw new Error('Empty wei tag template. Please specify expression inside wei`` tag') - } - - // combine interpolations in one expression - let expression = strings[0] - for (let i = 1; i < strings.length; ++i) { - expression += values[i - 1].toString() + strings[i] +function templateToString(template, args) { + let expression = template[0] + for (let i = 1; i < template.length; ++i) { + expression += args[i - 1].toString() + template[i] } return expression } -function parseWeiExpression(expression) { - const [amount, unit = 'wei'] = expression - .replaceAll('_', '') // remove all _ from numbers written like '100_00' - .trim() // remove all leading and trealing spaces - .split(' ') // split amount and unit parts - .filter((v) => !!v) // remove all empty strings if value had redundant spaces between amount and unit parts - - if (!Number.isFinite(+amount)) { - throw new Error(`Amount ${amount} is not a number`) - } - - return BigInt(hre.web3.utils.toWei(amount, unit.toLowerCase())) +function stringifiableToString(stringifiable) { + return stringifiable.toString() } -module.exports = { - wei, +function isTemplateStringArray(maybeTemplate) { + return !!maybeTemplate.raw && Array.isArray(maybeTemplate) && maybeTemplate.every((elem) => typeof elem === 'string') } + +module.exports = { wei } diff --git a/test/scenario/execution_layer_rewards_after_the_merge.test.js b/test/scenario/execution_layer_rewards_after_the_merge.test.js index fcef6e3d8..77dd30b8d 100644 --- a/test/scenario/execution_layer_rewards_after_the_merge.test.js +++ b/test/scenario/execution_layer_rewards_after_the_merge.test.js @@ -15,7 +15,7 @@ const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') const TOTAL_BASIS_POINTS = 10 ** 4 const CURATED_MODULE_ID = 1 const LIDO_INIT_BALANCE_ETH = 1 -const ONE_DAY = 1 * 24 * 60 * 60 +const ONE_DAY_WITH_MARGIN = 1 * 24 * 60 * 60 + 60 * 10 // one day and 10 minutes const ORACLE_REPORT_LIMITS_BOILERPLATE = { churnValidatorsPerDayLimit: 255, @@ -525,7 +525,7 @@ contract('Lido: merge acceptance', (addresses) => { assert.equals(oldTotalPooledEther, ETH(107.35), 'total pooled ether') // Reporting the same balance as it was before (64.35ETH => 64.35ETH) - await advanceChainTime(ONE_DAY) + await advanceChainTime(ONE_DAY_WITH_MARGIN) const { refSlot } = await consensus.getCurrentFrame() @@ -621,7 +621,7 @@ contract('Lido: merge acceptance', (addresses) => { assert.equals(oldTotalPooledEther, ETH(114.35), 'total pooled ether') // Reporting balance decrease (64.35ETH => 62.35ETH) - await advanceChainTime(ONE_DAY) + await advanceChainTime(ONE_DAY_WITH_MARGIN) const { refSlot } = await consensus.getCurrentFrame() @@ -709,7 +709,7 @@ contract('Lido: merge acceptance', (addresses) => { assert.equals(oldTotalPooledEther, ETH(117.35), 'total pooled ether') // Reporting balance decrease (62.35ETH => 59.35ETH) - await advanceChainTime(ONE_DAY) + await advanceChainTime(ONE_DAY_WITH_MARGIN) const { refSlot } = await consensus.getCurrentFrame() @@ -779,7 +779,7 @@ contract('Lido: merge acceptance', (addresses) => { assert.equals(oldTotalPooledEther, ETH(117.35), 'total pooled ether') // Reporting balance decrease (59.35ETH => 51.35ETH) - await advanceChainTime(ONE_DAY) + await advanceChainTime(ONE_DAY_WITH_MARGIN) const { refSlot } = await consensus.getCurrentFrame() @@ -870,7 +870,7 @@ contract('Lido: merge acceptance', (addresses) => { assert.equals(oldTotalPooledEther, ETH(111.35), 'total pooled ether') // Reporting balance increase (51.35ETH => 51.49ETH) - await advanceChainTime(ONE_DAY) + await advanceChainTime(ONE_DAY_WITH_MARGIN) const { refSlot } = await consensus.getCurrentFrame() @@ -977,7 +977,7 @@ contract('Lido: merge acceptance', (addresses) => { // Do multiple oracle reports to withdraw all ETH from execution layer rewards vault while (elRewardsVaultBalance > 0) { - await advanceChainTime(ONE_DAY) + await advanceChainTime(ONE_DAY_WITH_MARGIN) const currentELBalance = await web3.eth.getBalance(elRewardsVault.address) diff --git a/test/scenario/lido_rewards_distribution_math.test.js b/test/scenario/lido_rewards_distribution_math.test.js index 73d0dee29..34f8c4ffe 100644 --- a/test/scenario/lido_rewards_distribution_math.test.js +++ b/test/scenario/lido_rewards_distribution_math.test.js @@ -652,6 +652,8 @@ contract('Lido: rewards distribution math', (addresses) => { { from: voting } ) + await waitBlocks(+(await depositSecurityModule.getMaxDeposits())) + const modulesList = await stakingRouter.getStakingModules() assert(modulesList.length, 2, 'module added')