diff --git a/.husky/pre-commit b/.husky/pre-commit index c79d6bcc2..b63c34415 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,10 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" +RED_COLOR='\033[0;31m' +NO_COLOR='\033[0m' + yarn compile -git diff --quiet lib/abi +git diff --quiet lib/abi || (echo -e "${RED_COLOR}Unstaged ABIs detected${NO_COLOR}"; exit 1) yarn lint diff --git a/contracts/0.4.24/Lido.sol b/contracts/0.4.24/Lido.sol index 47061b896..852a8fed9 100644 --- a/contracts/0.4.24/Lido.sol +++ b/contracts/0.4.24/Lido.sol @@ -36,6 +36,7 @@ interface IOracleReportSanityChecker { uint256 _postCLBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, uint256 _preCLValidators, uint256 _postCLValidators ) external view; @@ -47,11 +48,14 @@ interface IOracleReportSanityChecker { uint256 _postCLBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, - uint256 _etherToLockForWithdrawals + uint256 _sharesRequestedToBurn, + uint256 _etherToLockForWithdrawals, + uint256 _newSharesToBurnForWithdrawals ) external view returns ( uint256 withdrawals, uint256 elRewards, - uint256 sharesToBurnLimit + uint256 simulatedSharesToBurn, + uint256 sharesToBurn ); function checkWithdrawalQueueOracleReport( @@ -63,7 +67,7 @@ interface IOracleReportSanityChecker { uint256 _postTotalPooledEther, uint256 _postTotalShares, uint256 _etherLockedOnWithdrawalQueue, - uint256 _sharesBurntFromWithdrawalQueue, + uint256 _sharesBurntDueToWithdrawals, uint256 _simulatedShareRate ) external view; } @@ -78,7 +82,7 @@ interface IWithdrawalVault { interface IStakingRouter { function deposit( - uint256 _maxDepositsCount, + uint256 _depositsCount, uint256 _stakingModuleId, bytes _depositCalldata ) external payable; @@ -100,21 +104,23 @@ interface IStakingRouter { function getTotalFeeE4Precision() external view returns (uint16 totalFee); - function getStakingFeeAggregateDistributionE4Precision() external view returns (uint16 modulesFee, uint16 treasuryFee); + function getStakingFeeAggregateDistributionE4Precision() external view returns ( + uint16 modulesFee, uint16 treasuryFee + ); - function getStakingModuleMaxDepositsCount(uint256 _stakingModuleId, uint256 _depositableEther) + function getStakingModuleMaxDepositsCount(uint256 _stakingModuleId, uint256 _maxDepositsValue) external view returns (uint256); } interface IWithdrawalQueue { - function finalizationBatch(uint256 _newLastFinalizedRequestId, uint256 _shareRate) + function prefinalize(uint256[] _batches, uint256 _maxShareRate) external view - returns (uint128 eth, uint128 shares); + returns (uint256 ethToLock, uint256 sharesToBurn); - function finalize(uint256 _lastIdToFinalize) external payable; + function finalize(uint256[] _batches, uint256 _maxShareRate) external payable; function isPaused() external view returns (bool); @@ -164,10 +170,6 @@ contract Lido is Versioned, StETHPermit, AragonApp { 0xe6dc5d79630c61871e99d341ad72c5a052bed2fc8c79e5a4480a7cd31117576c; // keccak256("UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE") uint256 private constant DEPOSIT_SIZE = 32 ether; - uint256 public constant TOTAL_BASIS_POINTS = 10000; - /// @dev special value to not finalize withdrawal requests - /// see the `_lastFinalizableRequestId` arg for `handleOracleReport()` - uint256 private constant DONT_FINALIZE_WITHDRAWALS = 0; /// @dev storage slot position for the Lido protocol contracts locator bytes32 internal constant LIDO_LOCATOR_POSITION = @@ -265,7 +267,7 @@ contract Lido is Versioned, StETHPermit, AragonApp { onlyInit { uint256 amount = _bootstrapInitialHolder(); - BUFFERED_ETHER_POSITION.setStorageUint256(amount); + _setBufferedEther(amount); emit Submitted(INITIAL_TOKEN_HOLDER, amount, 0); @@ -471,7 +473,7 @@ contract Lido is Versioned, StETHPermit, AragonApp { * are treated as a user deposit */ function receiveELRewards() external payable { - require(msg.sender == getLidoLocator().elRewardsVault(), "EXECUTION_LAYER_REWARDS_VAULT_ONLY"); + require(msg.sender == getLidoLocator().elRewardsVault()); TOTAL_EL_REWARDS_COLLECTED_POSITION.setStorageUint256(getTotalELRewardsCollected().add(msg.value)); @@ -524,8 +526,9 @@ contract Lido is Versioned, StETHPermit, AragonApp { // EL values uint256 withdrawalVaultBalance; uint256 elRewardsVaultBalance; + uint256 sharesRequestedToBurn; // Decision about withdrawals processing - uint256 lastFinalizableRequestId; + uint256[] withdrawalFinalizationBatches; uint256 simulatedShareRate; } @@ -544,26 +547,28 @@ contract Lido is Versioned, StETHPermit, AragonApp { /** * @notice Updates accounting stats, collects EL rewards and distributes collected rewards - * if beacon balance increased - * @dev periodically called by the Oracle contract + * if beacon balance increased, performs withdrawal requests finalization + * @dev periodically called by the AccountingOracle contract * * @param _reportTimestamp the moment of the oracle report calculation * @param _timeElapsed seconds elapsed since the previous report calculation * @param _clValidators number of Lido validators on Consensus Layer * @param _clBalance sum of all Lido validators' balances on Consensus Layer - * @param _withdrawalVaultBalance withdrawal vault balance on Execution Layer for report block - * @param _elRewardsVaultBalance elRewards vault balance on Execution Layer for report block - * @param _lastFinalizableRequestId right boundary of requestId range if equals 0, no requests should be finalized + * @param _withdrawalVaultBalance withdrawal vault balance on Execution Layer at `_reportTimestamp` + * @param _elRewardsVaultBalance elRewards vault balance on Execution Layer at `_reportTimestamp` + * @param _sharesRequestedToBurn shares requested to burn through Burner at `_reportTimestamp` + * @param _withdrawalFinalizationBatches the ascendingly-sorted array of withdrawal request IDs obtained by calling + * WithdrawalQueue.calculateFinalizationBatches. Empty array means that no withdrawal requests should be finalized * @param _simulatedShareRate share rate that was simulated by oracle when the report data created (1e27 precision) * * NB: `_simulatedShareRate` should be calculated off-chain by calling the method with `eth_call` JSON-RPC API - * while passing `_lastFinalizableRequestId` == `_simulatedShareRate` == 0, and plugging the returned values + * while passing empty `_withdrawalFinalizationBatches` and `_simulatedShareRate` == 0, plugging the returned values * to the following formula: `_simulatedShareRate = (postTotalPooledEther * 1e27) / postTotalShares` * - * @return postTotalPooledEther amount of ether in the protocol after report - * @return postTotalShares amount of shares in the protocol after report - * @return withdrawals withdrawn from the withdrawals vault - * @return elRewards withdrawn from the execution layer rewards vault + * @return postRebaseAmounts[0]: `postTotalPooledEther` amount of ether in the protocol after report + * @return postRebaseAmounts[1]: `postTotalShares` amount of shares in the protocol after report + * @return postRebaseAmounts[2]: `withdrawals` withdrawn from the withdrawals vault + * @return postRebaseAmounts[3]: `elRewards` withdrawn from the execution layer rewards vault */ function handleOracleReport( // Oracle timings @@ -575,20 +580,13 @@ contract Lido is Versioned, StETHPermit, AragonApp { // EL values uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, // Decision about withdrawals processing - uint256 _lastFinalizableRequestId, + uint256[] _withdrawalFinalizationBatches, uint256 _simulatedShareRate - ) external returns ( - uint256 postTotalPooledEther, - uint256 postTotalShares, - uint256 withdrawals, - uint256 elRewards - ) { + ) external returns (uint256[4] postRebaseAmounts) { _whenNotStopped(); - OracleReportContracts memory contracts = _loadOracleReportContracts(); - require(msg.sender == contracts.accountingOracle, "APP_AUTH_FAILED"); - return _handleOracleReport( OracleReportedData( _reportTimestamp, @@ -597,10 +595,10 @@ contract Lido is Versioned, StETHPermit, AragonApp { _clBalance, _withdrawalVaultBalance, _elRewardsVaultBalance, - _lastFinalizableRequestId, + _sharesRequestedToBurn, + _withdrawalFinalizationBatches, _simulatedShareRate - ), - contracts + ) ); } @@ -705,17 +703,19 @@ contract Lido is Versioned, StETHPermit, AragonApp { _maxDepositsCount, stakingRouter.getStakingModuleMaxDepositsCount(_stakingModuleId, getDepositableEther()) ); - if (depositsCount == 0) return; - - uint256 depositsValue = depositsCount.mul(DEPOSIT_SIZE); - /// @dev firstly update the local state of the contract to prevent a reentrancy attack, - /// even if the StakingRouter is a trusted contract. - BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().sub(depositsValue)); - emit Unbuffered(depositsValue); - uint256 newDepositedValidators = DEPOSITED_VALIDATORS_POSITION.getStorageUint256().add(depositsCount); - DEPOSITED_VALIDATORS_POSITION.setStorageUint256(newDepositedValidators); - emit DepositedValidatorsChanged(newDepositedValidators); + uint256 depositsValue; + if (depositsCount > 0) { + depositsValue = depositsCount.mul(DEPOSIT_SIZE); + /// @dev firstly update the local state of the contract to prevent a reentrancy attack, + /// even if the StakingRouter is a trusted contract. + BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().sub(depositsValue)); + emit Unbuffered(depositsValue); + + uint256 newDepositedValidators = DEPOSITED_VALIDATORS_POSITION.getStorageUint256().add(depositsCount); + DEPOSITED_VALIDATORS_POSITION.setStorageUint256(newDepositedValidators); + emit DepositedValidatorsChanged(newDepositedValidators); + } /// @dev transfer ether to StakingRouter and make a deposit at the same time. All the ether /// sent to StakingRouter is counted as deposited. If StakingRouter can't deposit all @@ -828,7 +828,8 @@ contract Lido is Versioned, StETHPermit, AragonApp { OracleReportContracts memory _contracts, uint256 _withdrawalsToWithdraw, uint256 _elRewardsToWithdraw, - uint256 _lastFinalizableRequestId, + uint256[] _withdrawalFinalizationBatches, + uint256 _simulatedShareRate, uint256 _etherToLockOnWithdrawalQueue ) internal { // withdraw execution layer rewards and put them to the buffer @@ -844,19 +845,18 @@ contract Lido is Versioned, StETHPermit, AragonApp { // finalize withdrawals (send ether, assign shares for burning) if (_etherToLockOnWithdrawalQueue > 0) { IWithdrawalQueue withdrawalQueue = IWithdrawalQueue(_contracts.withdrawalQueue); - withdrawalQueue.finalize.value(_etherToLockOnWithdrawalQueue)(_lastFinalizableRequestId); + withdrawalQueue.finalize.value(_etherToLockOnWithdrawalQueue)( + _withdrawalFinalizationBatches, + _simulatedShareRate + ); } - uint256 preBufferedEther = _getBufferedEther(); - uint256 postBufferedEther = preBufferedEther + uint256 postBufferedEther = _getBufferedEther() .add(_elRewardsToWithdraw) // Collected from ELVault .add(_withdrawalsToWithdraw) // Collected from WithdrawalVault .sub(_etherToLockOnWithdrawalQueue); // Sent to WithdrawalQueue - // Storing even the same value costs gas, so just avoid it - if (preBufferedEther != postBufferedEther) { - BUFFERED_ETHER_POSITION.setStorageUint256(postBufferedEther); - } + _setBufferedEther(postBufferedEther); } /** @@ -873,12 +873,12 @@ contract Lido is Versioned, StETHPermit, AragonApp { if (!withdrawalQueue.isPaused()) { IOracleReportSanityChecker(_contracts.oracleReportSanityChecker).checkWithdrawalQueueOracleReport( - _reportedData.lastFinalizableRequestId, + _reportedData.withdrawalFinalizationBatches[_reportedData.withdrawalFinalizationBatches.length - 1], _reportedData.reportTimestamp ); - (etherToLock, sharesToBurn) = withdrawalQueue.finalizationBatch( - _reportedData.lastFinalizableRequestId, + (etherToLock, sharesToBurn) = withdrawalQueue.prefinalize( + _reportedData.withdrawalFinalizationBatches, _reportedData.simulatedShareRate ); } @@ -899,13 +899,12 @@ contract Lido is Versioned, StETHPermit, AragonApp { // See LIP-12 for details: // https://research.lido.fi/t/lip-12-on-chain-part-of-the-rewards-distribution-after-the-merge/1625 if (postCLTotalBalance > _reportContext.preCLBalance) { - uint256 consensusLayerRewards = postCLTotalBalance.sub(_reportContext.preCLBalance); - uint256 totalRewards = consensusLayerRewards.add(_withdrawnElRewards); + uint256 consensusLayerRewards = postCLTotalBalance - _reportContext.preCLBalance; sharesMintedAsFees = _distributeFee( _reportContext.preTotalPooledEther, _reportContext.preTotalShares, - totalRewards + consensusLayerRewards.add(_withdrawnElRewards) ); } } @@ -935,7 +934,7 @@ contract Lido is Versioned, StETHPermit, AragonApp { _mintShares(msg.sender, sharesAmount); - BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().add(msg.value)); + _setBufferedEther(_getBufferedEther().add(msg.value)); emit Submitted(msg.sender, msg.value, _referral); _emitTransferAfterMintingShares(msg.sender, sharesAmount); @@ -1062,10 +1061,9 @@ contract Lido is Versioned, StETHPermit, AragonApp { uint256 totalFee, uint256 totalRewards ) internal returns (uint256[] memory moduleRewards, uint256 totalModuleRewards) { - totalModuleRewards = 0; moduleRewards = new uint256[](recipients.length); - for (uint256 i = 0; i < recipients.length; i++) { + for (uint256 i; i < recipients.length; ++i) { if (modulesFees[i] > 0) { uint256 iModuleRewards = totalRewards.mul(modulesFees[i]).div(totalFee); moduleRewards[i] = iModuleRewards; @@ -1089,6 +1087,10 @@ contract Lido is Versioned, StETHPermit, AragonApp { return BUFFERED_ETHER_POSITION.getStorageUint256(); } + function _setBufferedEther(uint256 _newBufferedEther) internal { + BUFFERED_ETHER_POSITION.setStorageUint256(_newBufferedEther); + } + /// @dev Calculates and returns the total base balance (multiple of 32) of validators in transient state, /// i.e. submitted to the official Deposit contract but not yet visible in the CL state. /// @return transient balance in wei (1e-18 Ether) @@ -1141,23 +1143,24 @@ contract Lido is Versioned, StETHPermit, AragonApp { * @dev Size-efficient analog of the `auth(_role)` modifier * @param _role Permission name */ - function _auth(bytes32 _role) internal view auth(_role) { - // no-op + function _auth(bytes32 _role) internal view { + require(canPerform(msg.sender, _role, new uint256[](0)), "APP_AUTH_FAILED"); } /** * @dev Intermediate data structure for `_handleOracleReport` * Helps to overcome `stack too deep` issue. */ - struct OracleReportContext{ + struct OracleReportContext { uint256 preCLValidators; uint256 preCLBalance; uint256 preTotalPooledEther; uint256 preTotalShares; - uint256 sharesToBurnLimit; - uint256 sharesMintedAsFees; uint256 etherToLockOnWithdrawalQueue; uint256 sharesToBurnFromWithdrawalQueue; + uint256 simulatedSharesToBurn; + uint256 sharesToBurn; + uint256 sharesMintedAsFees; } /** @@ -1177,15 +1180,10 @@ contract Lido is Versioned, StETHPermit, AragonApp { * 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 */ - function _handleOracleReport( - OracleReportedData memory _reportedData, - OracleReportContracts memory _contracts - ) internal returns ( - uint256 postTotalPooledEther, - uint256 postTotalShares, - uint256 withdrawals, - uint256 elRewards - ) { + function _handleOracleReport(OracleReportedData memory _reportedData) internal returns (uint256[4]) { + OracleReportContracts memory contracts = _loadOracleReportContracts(); + + require(msg.sender == contracts.accountingOracle, "APP_AUTH_FAILED"); require(_reportedData.reportTimestamp <= block.timestamp, "INVALID_REPORT_TIMESTAMP"); OracleReportContext memory reportContext; @@ -1204,38 +1202,52 @@ contract Lido is Versioned, StETHPermit, AragonApp { // Step 2. // Pass the report data to sanity checker (reverts if malformed) - _checkAccountingOracleReport(_contracts, _reportedData, reportContext); + _checkAccountingOracleReport(contracts, _reportedData, reportContext); // Step 3. // Pre-calculate the ether to lock for withdrawal queue and shares to be burnt - if (_reportedData.lastFinalizableRequestId != DONT_FINALIZE_WITHDRAWALS) { + // due to withdrawal requests to finalize + if (_reportedData.withdrawalFinalizationBatches.length != 0) { ( reportContext.etherToLockOnWithdrawalQueue, reportContext.sharesToBurnFromWithdrawalQueue - ) = _calculateWithdrawals(_contracts, _reportedData); + ) = _calculateWithdrawals(contracts, _reportedData); + + if (reportContext.sharesToBurnFromWithdrawalQueue > 0) { + IBurner(contracts.burner).requestBurnShares( + contracts.withdrawalQueue, + reportContext.sharesToBurnFromWithdrawalQueue + ); + } } // Step 4. // Pass the accounting values to sanity checker to smoothen positive token rebase + + uint256 withdrawals; + uint256 elRewards; ( - withdrawals, elRewards, reportContext.sharesToBurnLimit - ) = IOracleReportSanityChecker(_contracts.oracleReportSanityChecker).smoothenTokenRebase( + withdrawals, elRewards, reportContext.simulatedSharesToBurn, reportContext.sharesToBurn + ) = IOracleReportSanityChecker(contracts.oracleReportSanityChecker).smoothenTokenRebase( reportContext.preTotalPooledEther, reportContext.preTotalShares, reportContext.preCLBalance, _reportedData.postCLBalance, _reportedData.withdrawalVaultBalance, _reportedData.elRewardsVaultBalance, - reportContext.etherToLockOnWithdrawalQueue + _reportedData.sharesRequestedToBurn, + reportContext.etherToLockOnWithdrawalQueue, + reportContext.sharesToBurnFromWithdrawalQueue ); // Step 5. // Invoke finalization of the withdrawal requests (send ether to withdrawal queue, assign shares to be burnt) _collectRewardsAndProcessWithdrawals( - _contracts, + contracts, withdrawals, elRewards, - _reportedData.lastFinalizableRequestId, + _reportedData.withdrawalFinalizationBatches, + _reportedData.simulatedShareRate, reportContext.etherToLockOnWithdrawalQueue ); @@ -1258,36 +1270,35 @@ contract Lido is Versioned, StETHPermit, AragonApp { ); // Step 7. - // Burn excess shares within the allowed limit (can postpone some shares to be burnt later) - // Return actually burnt shares of the current report's finalized withdrawal requests to use in sanity checks - uint256 burntCurrentWithdrawalShares = _burnSharesLimited( - IBurner(_contracts.burner), - _contracts.withdrawalQueue, - reportContext.sharesToBurnFromWithdrawalQueue, - reportContext.sharesToBurnLimit - ); + // 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) ( - postTotalShares, - postTotalPooledEther + uint256 postTotalShares, + uint256 postTotalPooledEther ) = _completeTokenRebase( _reportedData, reportContext, - IPostTokenRebaseReceiver(_contracts.postTokenRebaseReceiver) + IPostTokenRebaseReceiver(contracts.postTokenRebaseReceiver) ); // Step 9. Sanity check for the provided simulated share rate - if (_reportedData.lastFinalizableRequestId != DONT_FINALIZE_WITHDRAWALS) { - IOracleReportSanityChecker(_contracts.oracleReportSanityChecker).checkSimulatedShareRate( + if (_reportedData.withdrawalFinalizationBatches.length != 0) { + IOracleReportSanityChecker(contracts.oracleReportSanityChecker).checkSimulatedShareRate( postTotalPooledEther, postTotalShares, reportContext.etherToLockOnWithdrawalQueue, - burntCurrentWithdrawalShares, + reportContext.sharesToBurn.sub(reportContext.simulatedSharesToBurn), _reportedData.simulatedShareRate ); } + + return [postTotalPooledEther, postTotalShares, withdrawals, elRewards]; } /** @@ -1305,6 +1316,7 @@ contract Lido is Versioned, StETHPermit, AragonApp { _reportedData.postCLBalance, _reportedData.withdrawalVaultBalance, _reportedData.elRewardsVaultBalance, + _reportedData.sharesRequestedToBurn, _reportContext.preCLValidators, _reportedData.clValidators ); @@ -1345,43 +1357,6 @@ contract Lido is Versioned, StETHPermit, AragonApp { ); } - /* - * @dev Perform burning of `stETH` shares via the dedicated `Burner` contract. - * - * NB: some of the burning amount can be postponed for the next reports if positive token rebase smoothened. - * It's possible that underlying shares of the current oracle report's finalized withdrawals won't be burnt - * completely in a single oracle report round due to the provided `_sharesToBurnLimit` limit - * - * @return shares actually burnt for the current oracle report's finalized withdrawals - * these shares are assigned to be burnt most recently, so the amount can be calculated deducting - * `postponedSharesToBurn` shares (if any) after the burn commitment & execution - */ - function _burnSharesLimited( - IBurner _burner, - address _withdrawalQueue, - uint256 _sharesToBurnFromWithdrawalQueue, - uint256 _sharesToBurnLimit - ) internal returns (uint256 burntCurrentWithdrawalShares) { - if (_sharesToBurnFromWithdrawalQueue > 0) { - _burner.requestBurnShares(_withdrawalQueue, _sharesToBurnFromWithdrawalQueue); - } - - if (_sharesToBurnLimit > 0) { - uint256 sharesCommittedToBurnNow = _burner.commitSharesToBurn(_sharesToBurnLimit); - - if (sharesCommittedToBurnNow > 0) { - _burnShares(address(_burner), sharesCommittedToBurnNow); - } - } - - (uint256 coverShares, uint256 nonCoverShares) = _burner.getSharesRequestedToBurn(); - uint256 postponedSharesToBurn = coverShares.add(nonCoverShares); - - burntCurrentWithdrawalShares = - postponedSharesToBurn < _sharesToBurnFromWithdrawalQueue ? - _sharesToBurnFromWithdrawalQueue - postponedSharesToBurn : 0; - } - /** * @dev Load the contracts used for `handleOracleReport` internally. */ diff --git a/contracts/0.4.24/StETH.sol b/contracts/0.4.24/StETH.sol index 0261fe377..771276090 100644 --- a/contracts/0.4.24/StETH.sol +++ b/contracts/0.4.24/StETH.sol @@ -79,8 +79,11 @@ contract StETH is IERC20, Pausable { * For reference types, conventional storage variables are used since it's non-trivial * and error-prone to implement reference-type unstructured storage using Solidity v0.4; * see https://github.com/lidofinance/lido-dao/issues/181#issuecomment-736098834 + * + * keccak256("lido.StETH.totalShares") */ - bytes32 internal constant TOTAL_SHARES_POSITION = keccak256("lido.StETH.totalShares"); + bytes32 internal constant TOTAL_SHARES_POSITION = + 0xe3b4b636e601189b5f4c6742edf2538ac12bb61ed03e6da26949d69838fa447e; /** * @notice An executed shares transfer from `sender` to `recipient`. @@ -232,7 +235,7 @@ contract StETH is IERC20, Pausable { */ function transferFrom(address _sender, address _recipient, uint256 _amount) external returns (bool) { uint256 currentAllowance = allowances[_sender][msg.sender]; - require(currentAllowance >= _amount, "TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE"); + require(currentAllowance >= _amount, "ALLOWANCE_EXCEEDED"); _transfer(_sender, _recipient, _amount); _approve(_sender, msg.sender, currentAllowance.sub(_amount)); @@ -271,7 +274,7 @@ contract StETH is IERC20, Pausable { */ function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool) { uint256 currentAllowance = allowances[msg.sender][_spender]; - require(currentAllowance >= _subtractedValue, "DECREASED_ALLOWANCE_BELOW_ZERO"); + require(currentAllowance >= _subtractedValue, "ALLOWANCE_BELOW_ZERO"); _approve(msg.sender, _spender, currentAllowance.sub(_subtractedValue)); return true; } @@ -355,7 +358,7 @@ contract StETH is IERC20, Pausable { ) external returns (uint256) { uint256 currentAllowance = allowances[_sender][msg.sender]; uint256 tokensAmount = getPooledEthByShares(_sharesAmount); - require(currentAllowance >= tokensAmount, "TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE"); + require(currentAllowance >= tokensAmount, "ALLOWANCE_EXCEEDED"); _transferShares(_sender, _recipient, _sharesAmount); _approve(_sender, msg.sender, currentAllowance.sub(tokensAmount)); @@ -396,8 +399,8 @@ contract StETH is IERC20, Pausable { * - `_spender` cannot be the zero address. */ function _approve(address _owner, address _spender, uint256 _amount) internal { - require(_owner != address(0), "APPROVE_FROM_ZERO_ADDRESS"); - require(_spender != address(0), "APPROVE_TO_ZERO_ADDRESS"); + require(_owner != address(0), "APPROVE_FROM_ZERO_ADDR"); + require(_spender != address(0), "APPROVE_TO_ZERO_ADDR"); allowances[_owner][_spender] = _amount; emit Approval(_owner, _spender, _amount); @@ -423,17 +426,18 @@ contract StETH is IERC20, Pausable { * Requirements: * * - `_sender` cannot be the zero address. - * - `_recipient` cannot be the zero address. + * - `_recipient` cannot be the zero address or the `stETH` token contract itself * - `_sender` must hold at least `_sharesAmount` shares. * - the contract must not be paused. */ function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal { - require(_sender != address(0), "TRANSFER_FROM_THE_ZERO_ADDRESS"); - require(_recipient != address(0), "TRANSFER_TO_THE_ZERO_ADDRESS"); + require(_sender != address(0), "TRANSFER_FROM_ZERO_ADDR"); + require(_recipient != address(0), "TRANSFER_TO_ZERO_ADDR"); + require(_recipient != address(this), "TRANSFER_TO_STETH_CONTRACT"); _whenNotStopped(); uint256 currentSenderShares = shares[_sender]; - require(_sharesAmount <= currentSenderShares, "TRANSFER_AMOUNT_EXCEEDS_BALANCE"); + require(_sharesAmount <= currentSenderShares, "BALANCE_EXCEEDED"); shares[_sender] = currentSenderShares.sub(_sharesAmount); shares[_recipient] = shares[_recipient].add(_sharesAmount); @@ -451,7 +455,7 @@ contract StETH is IERC20, Pausable { * - the contract must not be paused. */ function _mintShares(address _recipient, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { - require(_recipient != address(0), "MINT_TO_THE_ZERO_ADDRESS"); + require(_recipient != address(0), "MINT_TO_ZERO_ADDR"); newTotalShares = _getTotalShares().add(_sharesAmount); TOTAL_SHARES_POSITION.setStorageUint256(newTotalShares); @@ -477,10 +481,10 @@ contract StETH is IERC20, Pausable { * - the contract must not be paused. */ function _burnShares(address _account, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { - require(_account != address(0), "BURN_FROM_THE_ZERO_ADDRESS"); + require(_account != address(0), "BURN_FROM_ZERO_ADDR"); uint256 accountShares = shares[_account]; - require(_sharesAmount <= accountShares, "BURN_AMOUNT_EXCEEDS_BALANCE"); + require(_sharesAmount <= accountShares, "BALANCE_EXCEEDED"); uint256 preRebaseTokenAmount = getPooledEthByShares(_sharesAmount); @@ -509,11 +513,13 @@ contract StETH is IERC20, Pausable { * Allows to get rid of zero checks for `totalShares` and `totalPooledEther` * and overcome corner cases. * + * NB: reverts if the current contract's balance is zero. + * * @dev must be invoked before using the token */ function _bootstrapInitialHolder() internal returns (uint256) { uint256 balance = address(this).balance; - require(balance != 0, "EMPTY_INIT_BALANCE"); + assert(balance != 0); if (_getTotalShares() == 0) { // if protocol is empty bootstrap it with the contract's balance diff --git a/contracts/0.4.24/StETHPermit.sol b/contracts/0.4.24/StETHPermit.sol index 6b7071746..b0105e58d 100644 --- a/contracts/0.4.24/StETHPermit.sol +++ b/contracts/0.4.24/StETHPermit.sol @@ -6,7 +6,7 @@ pragma solidity 0.4.24; import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; -import {ECDSA} from "../common/lib/ECDSA.sol"; +import {SignatureUtils} from "../common/lib/SignatureUtils.sol"; import {IEIP712StETH} from "../common/interfaces/IEIP712StETH.sol"; import {StETH} from "./StETH.sol"; @@ -69,14 +69,19 @@ contract StETHPermit is IERC2612, StETH { /** * @dev Storage position used for the EIP712 message utils contract + * + * keccak256("lido.StETHPermit.eip712StETH") */ - bytes32 internal constant EIP712_STETH_POSITION = keccak256("lido.StETHPermit.eip712StETH"); + bytes32 internal constant EIP712_STETH_POSITION = + 0x42b2d95e1ce15ce63bf9a8d9f6312cf44b23415c977ffa3b884333422af8941c; /** * @dev Typehash constant for ERC-2612 (Permit) + * + * keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)") */ bytes32 internal constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; /** * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, @@ -94,7 +99,7 @@ contract StETHPermit is IERC2612, StETH { function permit( address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s ) external { - require(block.timestamp <= _deadline, "ERC20Permit: expired deadline"); + require(block.timestamp <= _deadline, "DEADLINE_EXPIRED"); bytes32 structHash = keccak256( abi.encode(PERMIT_TYPEHASH, _owner, _spender, _value, _useNonce(_owner), _deadline) @@ -102,9 +107,7 @@ contract StETHPermit is IERC2612, StETH { bytes32 hash = IEIP712StETH(getEIP712StETH()).hashTypedDataV4(address(this), structHash); - address signer = ECDSA.recover(hash, _v, _r, _s); - require(signer == _owner, "ERC20Permit: invalid signature"); - + require(SignatureUtils.isValidSignature(_owner, hash, _v, _r, _s), "INVALID_SIGNATURE"); _approve(_owner, _spender, _value); } @@ -158,8 +161,8 @@ contract StETHPermit is IERC2612, StETH { * @dev Initialize EIP712 message utils contract for stETH */ function _initializeEIP712StETH(address _eip712StETH) internal { - require(_eip712StETH != address(0), "StETHPermit: zero eip712StETH"); - require(getEIP712StETH() == address(0), "StETHPermit: eip712StETH already set"); + require(_eip712StETH != address(0), "ZERO_EIP712STETH"); + require(getEIP712StETH() == address(0), "EIP712STETH_ALREADY_SET"); EIP712_STETH_POSITION.setStorageAddress(_eip712StETH); diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index b1c8b4eaf..82153ac11 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -80,6 +80,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // uint256 public constant MAX_NODE_OPERATORS_COUNT = 200; uint256 public constant MAX_NODE_OPERATOR_NAME_LENGTH = 255; + uint256 public constant MAX_STUCK_PENALTY_DELAY = 365 days; uint256 internal constant UINT64_MAX = 0xFFFFFFFFFFFFFFFF; @@ -220,7 +221,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 totalOperators = getNodeOperatorsCount(); Packed64x4.Packed memory signingKeysStats; Packed64x4.Packed memory operatorTargetStats; - Packed64x4.Packed memory summarySigningKeysStats = _loadSummarySigningKeysStats(); + Packed64x4.Packed memory summarySigningKeysStats = Packed64x4.Packed(0); uint64 vettedSigningKeysCountBefore; uint64 totalSigningKeysCount; uint64 depositedSigningKeysCount; @@ -322,7 +323,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } /// @notice Activates deactivated node operator with given id - /// @param _nodeOperatorId Node operator id to deactivate + /// @param _nodeOperatorId Node operator id to activate function activateNodeOperator(uint256 _nodeOperatorId) external { _onlyExistedNodeOperator(_nodeOperatorId); _auth(MANAGE_NODE_OPERATOR_ROLE); @@ -590,13 +591,14 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @param _nodeOperatorId Id of the node operator /// @param _targetLimit Target limit of the node operator /// @param _isTargetLimitActive active flag - function updateTargetValidatorsLimits(uint256 _nodeOperatorId, bool _isTargetLimitActive, uint64 _targetLimit) external { + function updateTargetValidatorsLimits(uint256 _nodeOperatorId, bool _isTargetLimitActive, uint256 _targetLimit) external { _onlyExistedNodeOperator(_nodeOperatorId); _auth(STAKING_ROUTER_ROLE); + _requireValidRange(_targetLimit <= UINT64_MAX); Packed64x4.Packed memory operatorTargetStats = _loadOperatorTargetValidatorsStats(_nodeOperatorId); operatorTargetStats.set(IS_TARGET_LIMIT_ACTIVE_OFFSET, _isTargetLimitActive ? 1 : 0); - operatorTargetStats.set(TARGET_VALIDATORS_COUNT_OFFSET, _isTargetLimitActive ? _targetLimit : 0); + operatorTargetStats.set(TARGET_VALIDATORS_COUNT_OFFSET, _isTargetLimitActive ? uint64(_targetLimit) : 0); _saveOperatorTargetValidatorsStats(_nodeOperatorId, operatorTargetStats); emit TargetValidatorsCountChanged(_nodeOperatorId, _targetLimit); @@ -609,7 +611,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { */ function _updateStuckValidatorsCount(uint256 _nodeOperatorId, uint64 _stuckValidatorsCount) internal { Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - if (_stuckValidatorsCount == stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET)) return; + uint64 curStuckValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); + if (_stuckValidatorsCount == curStuckValidatorsCount) return; Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); _requireValidRange( @@ -617,15 +620,17 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { <= signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET) - signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET) ); - stuckPenaltyStats.set(STUCK_VALIDATORS_COUNT_OFFSET, _stuckValidatorsCount); - if (_stuckValidatorsCount <= stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET)) { + uint64 curRefundedValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); + if (_stuckValidatorsCount <= curRefundedValidatorsCount && curStuckValidatorsCount > curRefundedValidatorsCount) { stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, uint64(block.timestamp + getStuckPenaltyDelay())); } + + stuckPenaltyStats.set(STUCK_VALIDATORS_COUNT_OFFSET, _stuckValidatorsCount); _saveOperatorStuckPenaltyStats(_nodeOperatorId, stuckPenaltyStats); emit StuckPenaltyStateChanged( _nodeOperatorId, _stuckValidatorsCount, - stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET), + curRefundedValidatorsCount, stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET) ); @@ -634,19 +639,22 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _updateRefundValidatorsKeysCount(uint256 _nodeOperatorId, uint64 _refundedValidatorsCount) internal { Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - if (_refundedValidatorsCount == stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET)) return; + uint64 curRefundedValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); + if (_refundedValidatorsCount == curRefundedValidatorsCount) return; Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); _requireValidRange(_refundedValidatorsCount <= signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET)); - stuckPenaltyStats.set(REFUNDED_VALIDATORS_COUNT_OFFSET, _refundedValidatorsCount); - if (stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET) <= _refundedValidatorsCount) { + uint64 curStuckValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); + if (_refundedValidatorsCount >= curStuckValidatorsCount && curRefundedValidatorsCount < curStuckValidatorsCount) { stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, uint64(block.timestamp + getStuckPenaltyDelay())); } + + stuckPenaltyStats.set(REFUNDED_VALIDATORS_COUNT_OFFSET, _refundedValidatorsCount); _saveOperatorStuckPenaltyStats(_nodeOperatorId, stuckPenaltyStats); emit StuckPenaltyStateChanged( _nodeOperatorId, - stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET), + curStuckValidatorsCount, _refundedValidatorsCount, stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET) ); @@ -669,15 +677,22 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @notice Invalidates all unused deposit data for all node operators function onWithdrawalCredentialsChanged() external { + _auth(STAKING_ROUTER_ROLE); uint256 operatorsCount = getNodeOperatorsCount(); if (operatorsCount > 0) { - invalidateReadyToDepositKeysRange(0, operatorsCount - 1); + _invalidateReadyToDepositKeysRange(0, operatorsCount - 1); } } - /// @notice Invalidates all unused validators keys for all node operators - function invalidateReadyToDepositKeysRange(uint256 _indexFrom, uint256 _indexTo) public { + /// @notice Invalidates all unused validators keys for node operators in the given range + /// @param _indexFrom the first index (inclusive) of the node operator to invalidate keys for + /// @param _indexTo the last index (inclusive) of the node operator to invalidate keys for + function invalidateReadyToDepositKeysRange(uint256 _indexFrom, uint256 _indexTo) external { _auth(MANAGE_NODE_OPERATOR_ROLE); + _invalidateReadyToDepositKeysRange(_indexFrom, _indexTo); + } + + function _invalidateReadyToDepositKeysRange(uint256 _indexFrom, uint256 _indexTo) internal { _requireValidRange(_indexFrom <= _indexTo && _indexTo < getNodeOperatorsCount()); uint64 trimmedKeysCount; @@ -704,6 +719,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } if (totalTrimmedKeysCount > 0) { + Packed64x4.Packed memory summarySigningKeysStats = _loadSummarySigningKeysStats(); + summarySigningKeysStats.set( + SUMMARY_TOTAL_KEYS_COUNT_OFFSET, + summarySigningKeysStats.get(SUMMARY_TOTAL_KEYS_COUNT_OFFSET) - totalTrimmedKeysCount + ); + _saveSummarySigningKeysStats(summarySigningKeysStats); _increaseValidatorsKeysNonce(); } } @@ -766,9 +787,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { newMaxSigningKeysCount = vettedSigningKeysCount; } else { // correct max count according to target if target is enabled - uint64 targetLimit = exitedSigningKeysCount.add(operatorTargetStats.get(TARGET_VALIDATORS_COUNT_OFFSET)); + // targetLimit is limited to UINT64_MAX + uint256 targetLimit = Math256.min(uint256(exitedSigningKeysCount).add(operatorTargetStats.get(TARGET_VALIDATORS_COUNT_OFFSET)), UINT64_MAX); if (targetLimit > depositedSigningKeysCount) { - newMaxSigningKeysCount = uint64(Math256.min(uint256(vettedSigningKeysCount), uint256(targetLimit))); + newMaxSigningKeysCount = uint64(Math256.min(vettedSigningKeysCount, targetLimit)); } } } // else newMaxSigningKeysCount = depositedSigningKeysCount, so depositable keys count = 0 @@ -934,10 +956,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { if (totalActiveValidatorsCount == 0) return (recipients, shares, penalized); - uint256 perValidatorReward = _totalRewardShares.div(totalActiveValidatorsCount); - for (idx = 0; idx < activeCount; ++idx) { - shares[idx] = shares[idx].mul(perValidatorReward); + /// @dev unsafe division used below for gas savings. It's safe in the current case + /// because SafeMath.div() only validates that the divider isn't equal to zero. + /// totalActiveValidatorsCount guaranteed greater than zero. + shares[idx] = shares[idx].mul(_totalRewardShares) / totalActiveValidatorsCount; } return (recipients, shares, penalized); @@ -994,7 +1017,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // upd totals Packed64x4.Packed memory summarySigningKeysStats = _loadSummarySigningKeysStats(); summarySigningKeysStats.set( - TOTAL_KEYS_COUNT_OFFSET, summarySigningKeysStats.get(SUMMARY_TOTAL_KEYS_COUNT_OFFSET).add(uint64(_keysCount)) + SUMMARY_TOTAL_KEYS_COUNT_OFFSET, summarySigningKeysStats.get(SUMMARY_TOTAL_KEYS_COUNT_OFFSET).add(uint64(_keysCount)) ); _saveSummarySigningKeysStats(summarySigningKeysStats); @@ -1274,8 +1297,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { (address[] memory recipients, uint256[] memory shares, bool[] memory penalized) = getRewardsDistribution(sharesToDistribute); - distributed = 0; - + uint256 toBurn; for (uint256 idx; idx < recipients.length; ++idx) { /// @dev skip ultra-low amounts processing to avoid transfer zero amount in case of a penalty if (shares[idx] < 2) continue; @@ -1283,13 +1305,16 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @dev half reward punishment /// @dev ignore remainder since it accumulated on contract balance shares[idx] >>= 1; - IBurner(getLocator().burner()).requestBurnShares(address(this), shares[idx]); + toBurn = toBurn.add(shares[idx]); emit NodeOperatorPenalized(recipients[idx], shares[idx]); } stETH.transferShares(recipients[idx], shares[idx]); distributed = distributed.add(shares[idx]); emit RewardsDistributed(recipients[idx], shares[idx]); } + if (toBurn > 0) { + IBurner(getLocator().burner()).requestBurnShares(address(this), toBurn); + } } function getLocator() public view returns (ILidoLocator) { @@ -1308,6 +1333,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @dev set new stuck penalty delay, duration in sec function _setStuckPenaltyDelay(uint256 _delay) internal { + _requireValidRange(_delay <= MAX_STUCK_PENALTY_DELAY); STUCK_PENALTY_DELAY_POSITION.setStorageUint256(_delay); emit StuckPenaltyDelayChanged(_delay); } diff --git a/contracts/0.4.24/oracle/LegacyOracle.sol b/contracts/0.4.24/oracle/LegacyOracle.sol index 4893ba5a1..ecd7e07fd 100644 --- a/contracts/0.4.24/oracle/LegacyOracle.sol +++ b/contracts/0.4.24/oracle/LegacyOracle.sol @@ -161,8 +161,10 @@ contract LegacyOracle is Versioned, AragonApp { * * Returns the epoch calculated from current timestamp */ - function getCurrentEpochId() external view returns (uint256 epochId) { - (epochId, ,) = _getCurrentFrameFromAccountingOracle(); + function getCurrentEpochId() external view returns (uint256) { + ChainSpec memory spec = _getChainSpec(); + // solhint-disable-line not-rely-on-time + return (_getTime() - spec.genesisTime) / (spec.slotsPerEpoch * spec.secondsPerSlot); } /** @@ -314,6 +316,10 @@ contract LegacyOracle is Versioned, AragonApp { _setContractVersion(4); } + function _getTime() internal view returns (uint256) { + return block.timestamp; // solhint-disable-line not-rely-on-time + } + function _getChainSpec() internal view @@ -331,6 +337,7 @@ contract LegacyOracle is Versioned, AragonApp { require(_chainSpec.slotsPerEpoch > 0, "BAD_SLOTS_PER_EPOCH"); require(_chainSpec.secondsPerSlot > 0, "BAD_SECONDS_PER_SLOT"); require(_chainSpec.genesisTime > 0, "BAD_GENESIS_TIME"); + require(_chainSpec.epochsPerFrame > 0, "BAD_EPOCHS_PER_FRAME"); uint256 data = ( uint256(_chainSpec.epochsPerFrame) << 192 | @@ -369,12 +376,13 @@ contract LegacyOracle is Versioned, AragonApp { ChainSpec memory spec = _getChainSpec(); IHashConsensus consensus = _getAccountingConsensusContract(); uint256 refSlot; - (refSlot, frameEndTime) = consensus.getCurrentFrame(); - // new accounting oracle's frame ends at the timestamp of the frame's last slot; old oracle's frame - // ended a second before the timestamp of the first slot of the next frame - frameEndTime += spec.secondsPerSlot - 1; + (refSlot,) = consensus.getCurrentFrame(); + // new accounting oracle's ref. slot is the last slot of the epoch preceding the one the frame starts at frameStartTime = spec.genesisTime + (refSlot + 1) * spec.secondsPerSlot; + // new accounting oracle's frame ends at the timestamp of the frame's last slot; old oracle's frame + // ended a second before the timestamp of the first slot of the next frame + frameEndTime = frameStartTime + spec.secondsPerSlot * spec.slotsPerEpoch * spec.epochsPerFrame - 1; frameEpochId = (refSlot + 1) / spec.slotsPerEpoch; } diff --git a/contracts/0.4.24/test_helpers/MockLegacyOracle.sol b/contracts/0.4.24/test_helpers/MockLegacyOracle.sol index bb59b1b84..f12162aa4 100644 --- a/contracts/0.4.24/test_helpers/MockLegacyOracle.sol +++ b/contracts/0.4.24/test_helpers/MockLegacyOracle.sol @@ -15,6 +15,10 @@ interface ILegacyOracle { function getLastCompletedEpochId() external view returns (uint256); } +interface ITimeProvider { + function getTime() external view returns (uint256); +} + contract MockLegacyOracle is ILegacyOracle, LegacyOracle { @@ -27,12 +31,6 @@ contract MockLegacyOracle is ILegacyOracle, LegacyOracle { HandleConsensusLayerReportCallData public lastCall__handleConsensusLayerReport; - uint64 internal _epochsPerFrame; - uint64 internal _slotsPerEpoch; - uint64 internal _secondsPerSlot; - uint64 internal _genesisTime; - uint256 internal _lastCompletedEpochId; - function getBeaconSpec() external view returns ( uint64 epochsPerFrame, @@ -40,14 +38,33 @@ contract MockLegacyOracle is ILegacyOracle, LegacyOracle { uint64 secondsPerSlot, uint64 genesisTime ) { - return ( - _epochsPerFrame, - _slotsPerEpoch, - _secondsPerSlot, - _genesisTime - ); + + ChainSpec memory spec = _getChainSpec(); + epochsPerFrame = spec.epochsPerFrame; + slotsPerEpoch = spec.slotsPerEpoch; + secondsPerSlot = spec.secondsPerSlot; + genesisTime = spec.genesisTime; } + function setBeaconSpec( uint64 epochsPerFrame, + uint64 slotsPerEpoch, + uint64 secondsPerSlot, + uint64 genesisTime) external { + _setChainSpec(ChainSpec(epochsPerFrame,slotsPerEpoch,secondsPerSlot,genesisTime)); + } + + + function _getTime() internal view returns (uint256) { + address accountingOracle = ACCOUNTING_ORACLE_POSITION.getStorageAddress(); + return ITimeProvider(accountingOracle).getTime(); + } + + function getTime() external view returns (uint256) { + return _getTime(); + } + + + function handleConsensusLayerReport(uint256 refSlot, uint256 clBalance, uint256 clValidators) external { @@ -65,18 +82,16 @@ contract MockLegacyOracle is ILegacyOracle, LegacyOracle { uint64 genesisTime, uint256 lastCompletedEpochId ) external { - _epochsPerFrame = epochsPerFrame; - _slotsPerEpoch = slotsPerEpoch; - _secondsPerSlot = secondsPerSlot; - _genesisTime = genesisTime; - _lastCompletedEpochId = lastCompletedEpochId; - + _setChainSpec(ChainSpec(epochsPerFrame,slotsPerEpoch,secondsPerSlot,genesisTime)); + LAST_COMPLETED_EPOCH_ID_POSITION.setStorageUint256(lastCompletedEpochId); } - function getLastCompletedEpochId() external view returns (uint256) { - return _lastCompletedEpochId; + + function setLastCompletedEpochId(uint256 lastCompletedEpochId) external { + LAST_COMPLETED_EPOCH_ID_POSITION.setStorageUint256(lastCompletedEpochId); } - function setLastCompletedEpochId(uint256 lastCompletedEpochId) external { - _lastCompletedEpochId = lastCompletedEpochId; + function initializeAsV3() external { + CONTRACT_VERSION_POSITION_DEPRECATED.setStorageUint256(3); } + } diff --git a/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol b/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol index 8f0912221..c8a313d26 100644 --- a/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol +++ b/contracts/0.4.24/test_helpers/NodeOperatorsRegistryMock.sol @@ -127,20 +127,16 @@ contract NodeOperatorsRegistryMock is NodeOperatorsRegistry { view returns ( uint256 totalSigningKeysCount, - uint256 vettedSigningKeysCount, + uint256 maxValidatorsCount, uint256 depositedSigningKeysCount, uint256 exitedSigningKeysCount ) { - uint256 nodeOperatorsCount = getNodeOperatorsCount(); - Packed64x4.Packed memory signingKeysStats; - for (uint i; i < nodeOperatorsCount; i++) { - signingKeysStats = _loadOperatorSigningKeysStats(i); - totalSigningKeysCount += signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET); - vettedSigningKeysCount += signingKeysStats.get(VETTED_KEYS_COUNT_OFFSET); - depositedSigningKeysCount += signingKeysStats.get(DEPOSITED_KEYS_COUNT_OFFSET); - exitedSigningKeysCount += signingKeysStats.get(EXITED_KEYS_COUNT_OFFSET); - } + Packed64x4.Packed memory summarySigningKeysStats = _loadSummarySigningKeysStats(); + totalSigningKeysCount = summarySigningKeysStats.get(SUMMARY_TOTAL_KEYS_COUNT_OFFSET); + maxValidatorsCount = summarySigningKeysStats.get(SUMMARY_MAX_VALIDATORS_COUNT_OFFSET); + depositedSigningKeysCount = summarySigningKeysStats.get(SUMMARY_DEPOSITED_KEYS_COUNT_OFFSET); + exitedSigningKeysCount = summarySigningKeysStats.get(SUMMARY_EXITED_KEYS_COUNT_OFFSET); } function testing_setBaseVersion(uint256 _newBaseVersion) external { @@ -202,7 +198,7 @@ contract NodeOperatorsRegistryMock is NodeOperatorsRegistry { event ValidatorsKeysLoaded(bytes publicKeys, bytes signatures); - function testing__distributeRewards() external returns (uint256) { + function testing_distributeRewards() external returns (uint256) { return _distributeRewards(); } } diff --git a/contracts/0.4.24/test_helpers/SiginigKyesMock.sol b/contracts/0.4.24/test_helpers/SigningKeysMock.sol similarity index 98% rename from contracts/0.4.24/test_helpers/SiginigKyesMock.sol rename to contracts/0.4.24/test_helpers/SigningKeysMock.sol index 4b2da4ab8..b83ef641f 100644 --- a/contracts/0.4.24/test_helpers/SiginigKyesMock.sol +++ b/contracts/0.4.24/test_helpers/SigningKeysMock.sol @@ -6,8 +6,6 @@ pragma solidity 0.4.24; import {SigningKeys} from "../lib/SigningKeys.sol"; -import "hardhat/console.sol"; - contract SigningKeysMock { using SigningKeys for bytes32; diff --git a/contracts/0.4.24/test_helpers/StETHMock.sol b/contracts/0.4.24/test_helpers/StETHMock.sol index 612166c60..de24a2676 100644 --- a/contracts/0.4.24/test_helpers/StETHMock.sol +++ b/contracts/0.4.24/test_helpers/StETHMock.sol @@ -38,6 +38,12 @@ contract StETHMock is StETH { _emitTransferAfterMintingShares(_to, _sharesAmount); } + function mintSteth(address _to) public payable { + uint256 sharesAmount = getSharesByPooledEth(msg.value); + mintShares(_to, sharesAmount); + setTotalPooledEther(_getTotalPooledEther().add(msg.value)); + } + function burnShares(address _account, uint256 _sharesAmount) public returns (uint256 newTotalShares) { return _burnShares(_account, _sharesAmount); } diff --git a/contracts/0.4.24/test_helpers/VersionedMock.sol b/contracts/0.4.24/test_helpers/VersionedMock.sol new file mode 100644 index 000000000..c78513f95 --- /dev/null +++ b/contracts/0.4.24/test_helpers/VersionedMock.sol @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.4.24; + +import { Versioned } from "../utils/Versioned.sol"; + +contract VersionedMock is Versioned { + constructor() public {} + + function getPetrifiedVersionMark() external pure returns (uint256) { + return PETRIFIED_VERSION_MARK; + } + + function checkContractVersion(uint256 version) external view { + _checkContractVersion(version); + } + + function setContractVersion(uint256 version) external { + _setContractVersion(version); + } +} diff --git a/contracts/0.4.24/utils/Pausable.sol b/contracts/0.4.24/utils/Pausable.sol index 93da9bd0c..d74c708e3 100644 --- a/contracts/0.4.24/utils/Pausable.sol +++ b/contracts/0.4.24/utils/Pausable.sol @@ -12,7 +12,9 @@ contract Pausable { event Stopped(); event Resumed(); - bytes32 internal constant ACTIVE_FLAG_POSITION = keccak256("lido.Pausable.activeFlag"); + // keccak256("lido.Pausable.activeFlag") + bytes32 internal constant ACTIVE_FLAG_POSITION = + 0x644132c4ddd5bb6f0655d5fe2870dcec7870e6be4758890f366b83441f9fdece; function _whenNotStopped() internal view { require(ACTIVE_FLAG_POSITION.getStorageBool(), "CONTRACT_IS_STOPPED"); diff --git a/contracts/0.8.9/Burner.sol b/contracts/0.8.9/Burner.sol index c4997062c..82b9faf43 100644 --- a/contracts/0.8.9/Burner.sol +++ b/contracts/0.8.9/Burner.sol @@ -57,6 +57,7 @@ contract Burner is IBurner, AccessControlEnumerable { error ZeroRecoveryAmount(); error StETHRecoveryWrongFunc(); error ZeroBurnAmount(); + error BurnAmountExceedsActual(uint256 requestedAmount, uint256 actualAmount); error ZeroAddress(string field); bytes32 public constant REQUEST_BURN_MY_STETH_ROLE = keccak256("REQUEST_BURN_MY_STETH_ROLE"); @@ -278,19 +279,16 @@ contract Burner is IBurner, AccessControlEnumerable { * NB: The real burn enactment to be invoked after the call (via internal Lido._burnShares()) * * Increments `totalCoverSharesBurnt` and `totalNonCoverSharesBurnt` counters. - * Resets `coverSharesBurnRequested` and `nonCoverSharesBurnRequested` counters to zero. - * Does nothing if there are no pending burning requests. + * Decrements `coverSharesBurnRequested` and `nonCoverSharesBurnRequested` counters. + * Does nothing if zero amount passed. * - * @param _sharesToBurnLimit limit of the shares to be burnt - * @return sharesToBurnNow the actual value that can be burnt + * @param _sharesToBurn amount of shares to be burnt */ - function commitSharesToBurn( - uint256 _sharesToBurnLimit - ) external virtual override returns (uint256 sharesToBurnNow) { + function commitSharesToBurn(uint256 _sharesToBurn) external virtual override { if (msg.sender != STETH) revert AppAuthLidoFailed(); - if (_sharesToBurnLimit == 0) { - return 0; + if (_sharesToBurn == 0) { + return; } uint256 memCoverSharesBurnRequested = coverSharesBurnRequested; @@ -298,12 +296,13 @@ contract Burner is IBurner, AccessControlEnumerable { uint256 burnAmount = memCoverSharesBurnRequested + memNonCoverSharesBurnRequested; - if (burnAmount == 0) { - return 0; + if (_sharesToBurn > burnAmount) { + revert BurnAmountExceedsActual(_sharesToBurn, burnAmount); } + uint256 sharesToBurnNow; if (memCoverSharesBurnRequested > 0) { - uint256 sharesToBurnNowForCover = Math.min(_sharesToBurnLimit, memCoverSharesBurnRequested); + uint256 sharesToBurnNowForCover = Math.min(_sharesToBurn, memCoverSharesBurnRequested); totalCoverSharesBurnt += sharesToBurnNowForCover; uint256 stETHToBurnNowForCover = IStETH(STETH).getPooledEthByShares(sharesToBurnNowForCover); @@ -312,9 +311,9 @@ contract Burner is IBurner, AccessControlEnumerable { coverSharesBurnRequested -= sharesToBurnNowForCover; sharesToBurnNow += sharesToBurnNowForCover; } - if ((memNonCoverSharesBurnRequested > 0) && (sharesToBurnNow < _sharesToBurnLimit)) { + if (memNonCoverSharesBurnRequested > 0 && sharesToBurnNow < _sharesToBurn) { uint256 sharesToBurnNowForNonCover = Math.min( - _sharesToBurnLimit - sharesToBurnNow, + _sharesToBurn - sharesToBurnNow, memNonCoverSharesBurnRequested ); @@ -325,6 +324,7 @@ contract Burner is IBurner, AccessControlEnumerable { nonCoverSharesBurnRequested -= sharesToBurnNowForNonCover; sharesToBurnNow += sharesToBurnNowForNonCover; } + assert(sharesToBurnNow == _sharesToBurn); } /** diff --git a/contracts/0.8.9/DepositSecurityModule.sol b/contracts/0.8.9/DepositSecurityModule.sol index 8f8cba651..4439ff348 100644 --- a/contracts/0.8.9/DepositSecurityModule.sol +++ b/contracts/0.8.9/DepositSecurityModule.sol @@ -49,7 +49,6 @@ contract DepositSecurityModule { event DepositsPaused(address indexed guardian, uint24 indexed stakingModuleId); event DepositsUnpaused(uint24 indexed stakingModuleId); - error StakingModuleIdTooLarge(); error ZeroAddress(string field); error DuplicateAddress(address addr); error NotAnOwner(address caller); @@ -134,11 +133,6 @@ contract DepositSecurityModule { _; } - modifier validStakingModuleId(uint256 _stakingModuleId) { - if (_stakingModuleId > type(uint24).max) revert StakingModuleIdTooLarge(); - _; - } - /** * Sets new owner. Only callable by the current owner. */ @@ -337,7 +331,7 @@ contract DepositSecurityModule { uint256 blockNumber, uint256 stakingModuleId, Signature memory sig - ) external validStakingModuleId(stakingModuleId) { + ) external { // In case of an emergency function `pauseDeposits` is supposed to be called // by all guardians. Thus only the first call will do the actual change. But // the other calls would be OK operations from the point of view of protocol’s logic. @@ -369,7 +363,7 @@ contract DepositSecurityModule { * * Only callable by the owner. */ - function unpauseDeposits(uint256 stakingModuleId) external validStakingModuleId(stakingModuleId) onlyOwner { + function unpauseDeposits(uint256 stakingModuleId) external onlyOwner { /// @dev unpause only paused modules (skip stopped) if (STAKING_ROUTER.getStakingModuleIsDepositsPaused(stakingModuleId)) { STAKING_ROUTER.resumeStakingModule(stakingModuleId); @@ -382,7 +376,7 @@ contract DepositSecurityModule { * guardian attestations of non-stale deposit root and `nonce`, and the number of * such attestations will be enough to reach quorum. */ - function canDeposit(uint256 stakingModuleId) external view validStakingModuleId(stakingModuleId) returns (bool) { + function canDeposit(uint256 stakingModuleId) external view returns (bool) { bool isModuleActive = STAKING_ROUTER.getStakingModuleIsActive(stakingModuleId); uint256 lastDepositBlock = STAKING_ROUTER.getStakingModuleLastDepositBlock(stakingModuleId); bool isLidoCanDeposit = LIDO.canDeposit(); @@ -418,7 +412,7 @@ contract DepositSecurityModule { uint256 nonce, bytes calldata depositCalldata, Signature[] calldata sortedGuardianSignatures - ) external validStakingModuleId(stakingModuleId) { + ) external { if (quorum == 0 || sortedGuardianSignatures.length < quorum) revert DepositNoQuorum(); bytes32 onchainDepositRoot = IDepositContract(DEPOSIT_CONTRACT).get_deposit_root(); diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 1ec148083..e47c09e46 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -25,6 +25,9 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version event StakingModuleStatusSet(uint256 indexed stakingModuleId, StakingModuleStatus status, address setBy); event StakingModuleExitedValidatorsIncompleteReporting(uint256 indexed stakingModuleId, uint256 unreportedExitedValidatorsCount); event WithdrawalCredentialsSet(bytes32 withdrawalCredentials, address setBy); + event WithdrawalsCredentialsChangeFailed(uint256 indexed stakingModuleId, bytes lowLevelRevertData); + event ExitedAndStuckValidatorsCountsUpdateFailed(uint256 indexed stakingModuleId, bytes lowLevelRevertData); + event RewardsMintedReportFailed(uint256 indexed stakingModuleId, bytes lowLevelRevertData); /// Emitted when the StakingRouter received ETH event StakingRouterETHDeposited(uint256 indexed stakingModuleId, uint256 amount); @@ -39,7 +42,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version error InvalidReportData(uint256 code); error ExitedValidatorsCountCannotDecrease(); error StakingModulesLimitExceeded(); - error StakingModuleIdTooLarge(); error StakingModuleUnregistered(); error AppAuthLidoFailed(); error StakingModuleStatusTheSame(); @@ -49,8 +51,9 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 currentNodeOpExitedValidatorsCount, uint256 currentNodeOpStuckValidatorsCount ); - error InvalidDepositsValue(uint256 etherValue); + error InvalidDepositsValue(uint256 etherValue, uint256 depositsCount); error StakingModuleAddressExists(); + error ArraysLengthMismatch(uint256 firstArrayLength, uint256 secondArrayLength); enum StakingModuleStatus { Active, // deposits and rewards allowed @@ -74,8 +77,10 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @notice name of staking module string name; /// @notice block.timestamp of the last deposit of the staking module + /// @dev NB: lastDepositAt gets updated even if the deposit value was 0 and no actual deposit happened uint64 lastDepositAt; /// @notice block.number of the last deposit of the staking module + /// @dev NB: lastDepositBlock gets updated even if the deposit value was 0 and no actual deposit happened uint256 lastDepositBlock; /// @notice number of exited validators uint256 exitedValidatorsCount; @@ -117,13 +122,8 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 public constant FEE_PRECISION_POINTS = 10 ** 20; // 100 * 10 ** 18 uint256 public constant TOTAL_BASIS_POINTS = 10000; - - uint256 internal constant UINT24_MAX = type(uint24).max; - - modifier validStakingModuleId(uint256 _stakingModuleId) { - if (_stakingModuleId > UINT24_MAX) revert StakingModuleIdTooLarge(); - _; - } + uint256 public constant MAX_STAKING_MODULES_COUNT = 32; + uint256 public constant MAX_STAKING_MODULE_NAME_LENGTH = 32; constructor(address _depositContract) BeaconChainDepositor(_depositContract) {} @@ -173,17 +173,23 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _stakingModuleFee, uint256 _treasuryFee ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { - if (_targetShare > TOTAL_BASIS_POINTS) revert ValueOver100Percent("_targetShare"); - if (_stakingModuleFee + _treasuryFee > TOTAL_BASIS_POINTS) revert ValueOver100Percent("_stakingModuleFee + _treasuryFee"); - if (_stakingModuleAddress == address(0)) revert ZeroAddress("_stakingModuleAddress"); - if (bytes(_name).length == 0 || bytes(_name).length > 32) revert StakingModuleWrongName(); + if (_targetShare > TOTAL_BASIS_POINTS) + revert ValueOver100Percent("_targetShare"); + if (_stakingModuleFee + _treasuryFee > TOTAL_BASIS_POINTS) + revert ValueOver100Percent("_stakingModuleFee + _treasuryFee"); + if (_stakingModuleAddress == address(0)) + revert ZeroAddress("_stakingModuleAddress"); + if (bytes(_name).length == 0 || bytes(_name).length > MAX_STAKING_MODULE_NAME_LENGTH) + revert StakingModuleWrongName(); uint256 newStakingModuleIndex = getStakingModulesCount(); - if (newStakingModuleIndex >= 32) revert StakingModulesLimitExceeded(); + if (newStakingModuleIndex >= MAX_STAKING_MODULES_COUNT) + revert StakingModulesLimitExceeded(); for (uint256 i; i < newStakingModuleIndex; ) { - if (_stakingModuleAddress == _getStakingModuleByIndex(i).stakingModuleAddress) revert StakingModuleAddressExists(); + if (_stakingModuleAddress == _getStakingModuleByIndex(i).stakingModuleAddress) + revert StakingModuleAddressExists(); unchecked { ++i; } @@ -198,9 +204,10 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version newStakingModule.targetShare = uint16(_targetShare); newStakingModule.stakingModuleFee = uint16(_stakingModuleFee); newStakingModule.treasuryFee = uint16(_treasuryFee); - /// @dev since `enum` is `uint8` by nature, so the `status` is stored as `uint8` to avoid possible problems when upgrading. - /// But for human readability, we use `enum` as function parameter type. - /// More about conversion in the docs https://docs.soliditylang.org/en/v0.8.17/types.html#enums + /// @dev since `enum` is `uint8` by nature, so the `status` is stored as `uint8` to avoid + /// possible problems when upgrading. But for human readability, we use `enum` as + /// function parameter type. More about conversion in the docs + /// https://docs.soliditylang.org/en/v0.8.17/types.html#enums newStakingModule.status = uint8(StakingModuleStatus.Active); _setStakingModuleIndexById(newStakingModuleId, newStakingModuleIndex); @@ -224,15 +231,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _targetShare, uint256 _stakingModuleFee, uint256 _treasuryFee - ) external - validStakingModuleId(_stakingModuleId) - onlyRole(STAKING_MODULE_MANAGE_ROLE) - { + ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { if (_targetShare > TOTAL_BASIS_POINTS) revert ValueOver100Percent("_targetShare"); if (_stakingModuleFee + _treasuryFee > TOTAL_BASIS_POINTS) revert ValueOver100Percent("_stakingModuleFee + _treasuryFee"); - uint256 stakingModuleIndex = _getStakingModuleIndexById(_stakingModuleId); - StakingModule storage stakingModule = _getStakingModuleByIndex(stakingModuleIndex); + StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); stakingModule.targetShare = uint16(_targetShare); stakingModule.treasuryFee = uint16(_treasuryFee); @@ -242,6 +245,22 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version emit StakingModuleFeesSet(_stakingModuleId, _stakingModuleFee, _treasuryFee, msg.sender); } + /// @notice Updates the limit of the validators that can be used for deposit + /// @param _stakingModuleId Id of the staking module + /// @param _nodeOperatorId Id of the node operator + /// @param _isTargetLimitActive Active flag + /// @param _targetLimit Target limit of the node operator + function updateTargetValidatorsLimits( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + bool _isTargetLimitActive, + uint256 _targetLimit + ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { + address moduleAddr = _getStakingModuleById(_stakingModuleId).stakingModuleAddress; + IStakingModule(moduleAddr) + .updateTargetValidatorsLimits(_nodeOperatorId, _isTargetLimitActive, _targetLimit); + } + /// @notice Updates the number of the refunded validators in the staking module with the given /// node operator id /// @param _stakingModuleId Id of the staking module @@ -251,7 +270,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _stakingModuleId, uint256 _nodeOperatorId, uint256 _refundedValidatorsCount - ) external validStakingModuleId(_stakingModuleId) onlyRole(STAKING_MODULE_MANAGE_ROLE) { + ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { address moduleAddr = _getStakingModuleById(_stakingModuleId).stakingModuleAddress; IStakingModule(moduleAddr) .updateRefundedValidatorsCount(_nodeOperatorId, _refundedValidatorsCount); @@ -261,9 +280,21 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version external onlyRole(REPORT_REWARDS_MINTED_ROLE) { + if (_stakingModuleIds.length != _totalShares.length) { + revert ArraysLengthMismatch(_stakingModuleIds.length, _totalShares.length); + } + for (uint256 i = 0; i < _stakingModuleIds.length; ) { - address moduleAddr = _getStakingModuleById(_stakingModuleIds[i]).stakingModuleAddress; - IStakingModule(moduleAddr).onRewardsMinted(_totalShares[i]); + if (_totalShares[i] > 0) { + address moduleAddr = _getStakingModuleById(_stakingModuleIds[i]).stakingModuleAddress; + try IStakingModule(moduleAddr).onRewardsMinted(_totalShares[i]) {} + catch (bytes memory lowLevelRevertData) { + emit RewardsMintedReportFailed( + _stakingModuleIds[i], + lowLevelRevertData + ); + } + } unchecked { ++i; } } } @@ -275,8 +306,14 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version external onlyRole(REPORT_EXITED_VALIDATORS_ROLE) { + if (_stakingModuleIds.length != _exitedValidatorsCounts.length) { + revert ArraysLengthMismatch(_stakingModuleIds.length, _exitedValidatorsCounts.length); + } + + uint256 stakingModuleId; for (uint256 i = 0; i < _stakingModuleIds.length; ) { - StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleIds[i]); + stakingModuleId = _stakingModuleIds[i]; + StakingModule storage stakingModule = _getStakingModuleById(stakingModuleId); uint256 prevReportedExitedValidatorsCount = stakingModule.exitedValidatorsCount; if (_exitedValidatorsCounts[i] < prevReportedExitedValidatorsCount) { @@ -292,7 +329,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version if (totalExitedValidatorsCount < prevReportedExitedValidatorsCount) { // not all of the exited validators were async reported to the module emit StakingModuleExitedValidatorsIncompleteReporting( - stakingModule.id, + stakingModuleId, prevReportedExitedValidatorsCount - totalExitedValidatorsCount ); } @@ -429,7 +466,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version (uint256 exitedValidatorsCount, , ) = moduleContract.getStakingModuleSummary(); if (exitedValidatorsCount == stakingModule.exitedValidatorsCount) { // oracle finished updating exited validators for all node ops - moduleContract.onExitedAndStuckValidatorsCountsUpdated(); + try moduleContract.onExitedAndStuckValidatorsCountsUpdated() {} + catch (bytes memory lowLevelRevertData) { + emit ExitedAndStuckValidatorsCountsUpdateFailed( + stakingModule.id, + lowLevelRevertData + ); + } } unchecked { ++i; } @@ -480,7 +523,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version function getStakingModule(uint256 _stakingModuleId) public view - validStakingModuleId(_stakingModuleId) returns (StakingModule memory) { return _getStakingModuleById(_stakingModuleId); @@ -496,8 +538,9 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /** * @dev Returns status of staking module */ - function getStakingModuleStatus(uint256 _stakingModuleId) public view - validStakingModuleId(_stakingModuleId) + function getStakingModuleStatus(uint256 _stakingModuleId) + public + view returns (StakingModuleStatus) { return StakingModuleStatus(_getStakingModuleById(_stakingModuleId).status); @@ -706,14 +749,12 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version * @notice set the staking module status flag for participation in further deposits and/or reward distribution */ function setStakingModuleStatus(uint256 _stakingModuleId, StakingModuleStatus _status) external - validStakingModuleId(_stakingModuleId) onlyRole(STAKING_MODULE_MANAGE_ROLE) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); - StakingModuleStatus _prevStatus = StakingModuleStatus(stakingModule.status); - if (_prevStatus == _status) revert StakingModuleStatusTheSame(); - stakingModule.status = uint8(_status); - emit StakingModuleStatusSet(_stakingModuleId, _status, msg.sender); + if (StakingModuleStatus(stakingModule.status) == _status) + revert StakingModuleStatusTheSame(); + _setStakingModuleStatus(stakingModule, _status); } /** @@ -721,14 +762,12 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version * @param _stakingModuleId id of the staking module to be paused */ function pauseStakingModule(uint256 _stakingModuleId) external - validStakingModuleId(_stakingModuleId) onlyRole(STAKING_MODULE_PAUSE_ROLE) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); - StakingModuleStatus _prevStatus = StakingModuleStatus(stakingModule.status); - if (_prevStatus != StakingModuleStatus.Active) revert StakingModuleNotActive(); - stakingModule.status = uint8(StakingModuleStatus.DepositsPaused); - emit StakingModuleStatusSet(_stakingModuleId, StakingModuleStatus.DepositsPaused, msg.sender); + if (StakingModuleStatus(stakingModule.status) != StakingModuleStatus.Active) + revert StakingModuleNotActive(); + _setStakingModuleStatus(stakingModule, StakingModuleStatus.DepositsPaused); } /** @@ -736,63 +775,59 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version * @param _stakingModuleId id of the staking module to be unpaused */ function resumeStakingModule(uint256 _stakingModuleId) external - validStakingModuleId(_stakingModuleId) onlyRole(STAKING_MODULE_RESUME_ROLE) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); - StakingModuleStatus _prevStatus = StakingModuleStatus(stakingModule.status); - if (_prevStatus != StakingModuleStatus.DepositsPaused) revert StakingModuleNotPaused(); - stakingModule.status = uint8(StakingModuleStatus.Active); - emit StakingModuleStatusSet(_stakingModuleId, StakingModuleStatus.Active, msg.sender); + if (StakingModuleStatus(stakingModule.status) != StakingModuleStatus.DepositsPaused) + revert StakingModuleNotPaused(); + _setStakingModuleStatus(stakingModule, StakingModuleStatus.Active); } - function getStakingModuleIsStopped(uint256 _stakingModuleId) external view - validStakingModuleId(_stakingModuleId) - returns (bool) + function getStakingModuleIsStopped(uint256 _stakingModuleId) external view returns (bool) { return getStakingModuleStatus(_stakingModuleId) == StakingModuleStatus.Stopped; } - function getStakingModuleIsDepositsPaused(uint256 _stakingModuleId) external view - validStakingModuleId(_stakingModuleId) + function getStakingModuleIsDepositsPaused(uint256 _stakingModuleId) + external + view returns (bool) { return getStakingModuleStatus(_stakingModuleId) == StakingModuleStatus.DepositsPaused; } - function getStakingModuleIsActive(uint256 _stakingModuleId) external view - validStakingModuleId(_stakingModuleId) - returns (bool) - { + function getStakingModuleIsActive(uint256 _stakingModuleId) external view returns (bool) { return getStakingModuleStatus(_stakingModuleId) == StakingModuleStatus.Active; } - function getStakingModuleNonce(uint256 _stakingModuleId) external view - validStakingModuleId(_stakingModuleId) - returns (uint256) - { + function getStakingModuleNonce(uint256 _stakingModuleId) external view returns (uint256) { return IStakingModule(_getStakingModuleAddressById(_stakingModuleId)).getNonce(); } - function getStakingModuleLastDepositBlock(uint256 _stakingModuleId) external view - validStakingModuleId(_stakingModuleId) + function getStakingModuleLastDepositBlock(uint256 _stakingModuleId) + external + view returns (uint256) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); return stakingModule.lastDepositBlock; } - function getStakingModuleActiveValidatorsCount(uint256 _stakingModuleId) external view - validStakingModuleId(_stakingModuleId) + function getStakingModuleActiveValidatorsCount(uint256 _stakingModuleId) + external + view returns (uint256 activeValidatorsCount) { + StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); ( uint256 totalExitedValidatorsCount, uint256 totalDepositedValidatorsCount, /* uint256 depositableValidatorsCount */ - ) = IStakingModule(_getStakingModuleAddressById(_stakingModuleId)).getStakingModuleSummary(); + ) = IStakingModule(stakingModule.stakingModuleAddress).getStakingModuleSummary(); - activeValidatorsCount = totalDepositedValidatorsCount - totalExitedValidatorsCount; + activeValidatorsCount = totalDepositedValidatorsCount - Math256.max( + stakingModule.exitedValidatorsCount, totalExitedValidatorsCount + ); } /// @dev calculate the max count of deposits which the staking module can provide data for based @@ -803,7 +838,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version function getStakingModuleMaxDepositsCount(uint256 _stakingModuleId, uint256 _maxDepositsValue) public view - validStakingModuleId(_stakingModuleId) returns (uint256) { ( @@ -856,7 +890,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 precisionPoints ) { - (uint256 totalActiveValidators, StakingModuleCache[] memory stakingModulesCache) = _loadStakingModulesCache(false); + (uint256 totalActiveValidators, StakingModuleCache[] memory stakingModulesCache) = _loadStakingModulesCache(); uint256 stakingModulesCount = stakingModulesCache.length; /// @dev return empty response if there are no staking modules or active validators yet @@ -874,7 +908,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint96 stakingModuleFee; for (uint256 i; i < stakingModulesCount; ) { - stakingModuleIds[i] = stakingModulesCache[i].stakingModuleId; + stakingModuleIds[rewardedStakingModulesCount] = stakingModulesCache[i].stakingModuleId; /// @dev skip staking modules which have no active validators if (stakingModulesCache[i].activeValidatorsCount > 0) { stakingModuleValidatorsShare = ((stakingModulesCache[i].activeValidatorsCount * precisionPoints) / totalActiveValidators); @@ -907,6 +941,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version if (rewardedStakingModulesCount < stakingModulesCount) { uint256 trim = stakingModulesCount - rewardedStakingModulesCount; assembly { + mstore(stakingModuleIds, sub(mload(stakingModuleIds), trim)) mstore(recipients, sub(mload(recipients), trim)) mstore(stakingModuleFees, sub(mload(stakingModuleFees), trim)) } @@ -950,51 +985,52 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version (allocated, allocations, ) = _getDepositsAllocation(_depositsCount); } - /** - * @dev Invokes a deposit call to the official Deposit contract - * @param _depositsCount number of deposits to make - * @param _stakingModuleId id of the staking module to be deposited - * @param _depositCalldata staking module calldata - */ + /// @dev Invokes a deposit call to the official Deposit contract + /// @param _depositsCount number of deposits to make + /// @param _stakingModuleId id of the staking module to be deposited + /// @param _depositCalldata staking module calldata function deposit( uint256 _depositsCount, uint256 _stakingModuleId, bytes calldata _depositCalldata - ) external payable validStakingModuleId(_stakingModuleId) { + ) external payable { if (msg.sender != LIDO_POSITION.getStorageAddress()) revert AppAuthLidoFailed(); - uint256 depositsValue = msg.value; - if (depositsValue == 0 || depositsValue != _depositsCount * DEPOSIT_SIZE) { - revert InvalidDepositsValue(depositsValue); - } - bytes32 withdrawalCredentials = getWithdrawalCredentials(); if (withdrawalCredentials == 0) revert EmptyWithdrawalsCredentials(); StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); - if (StakingModuleStatus(stakingModule.status) != StakingModuleStatus.Active) { + if (StakingModuleStatus(stakingModule.status) != StakingModuleStatus.Active) revert StakingModuleNotActive(); - } - - (bytes memory publicKeysBatch, bytes memory signaturesBatch) = - IStakingModule(stakingModule.stakingModuleAddress) - .obtainDepositData(_depositsCount, _depositCalldata); - - uint256 etherBalanceBeforeDeposits = address(this).balance; - _makeBeaconChainDeposits32ETH( - _depositsCount, - abi.encodePacked(withdrawalCredentials), - publicKeysBatch, - signaturesBatch - ); - uint256 etherBalanceAfterDeposits = address(this).balance; - - /// @dev all sent ETH must be deposited and self balance stay the same - assert(etherBalanceBeforeDeposits - etherBalanceAfterDeposits == depositsValue); + /// @dev firstly update the local state of the contract to prevent a reentrancy attack + /// even though the staking modules are trusted contracts stakingModule.lastDepositAt = uint64(block.timestamp); stakingModule.lastDepositBlock = block.number; + + uint256 depositsValue = msg.value; emit StakingRouterETHDeposited(_stakingModuleId, depositsValue); + + if (depositsValue != _depositsCount * DEPOSIT_SIZE) + revert InvalidDepositsValue(depositsValue, _depositsCount); + + if (_depositsCount > 0) { + (bytes memory publicKeysBatch, bytes memory signaturesBatch) = + IStakingModule(stakingModule.stakingModuleAddress) + .obtainDepositData(_depositsCount, _depositCalldata); + + uint256 etherBalanceBeforeDeposits = address(this).balance; + _makeBeaconChainDeposits32ETH( + _depositsCount, + abi.encodePacked(withdrawalCredentials), + publicKeysBatch, + signaturesBatch + ); + uint256 etherBalanceAfterDeposits = address(this).balance; + + /// @dev all sent ETH must be deposited and self balance stay the same + assert(etherBalanceBeforeDeposits - etherBalanceAfterDeposits == depositsValue); + } } /** @@ -1007,9 +1043,14 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 stakingModulesCount = getStakingModulesCount(); for (uint256 i; i < stakingModulesCount; ) { - IStakingModule(_getStakingModuleAddressByIndex(i)).onWithdrawalCredentialsChanged(); - unchecked { - ++i; + StakingModule storage stakingModule = _getStakingModuleByIndex(i); + unchecked { ++i; } + + try IStakingModule(stakingModule.stakingModuleAddress) + .onWithdrawalCredentialsChanged() {} + catch (bytes memory lowLevelRevertData) { + _setStakingModuleStatus(stakingModule, StakingModuleStatus.DepositsPaused); + emit WithdrawalsCredentialsChangeFailed(stakingModule.id, lowLevelRevertData); } } @@ -1026,7 +1067,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version function _checkValidatorsByNodeOperatorReportData( bytes calldata _nodeOperatorIds, bytes calldata _validatorsCounts - ) internal { + ) internal pure { if (_nodeOperatorIds.length % 8 != 0 || _validatorsCounts.length % 16 != 0) { revert InvalidReportData(3); } @@ -1039,24 +1080,18 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } - /** - * @dev load modules into a memory cache - * - * @param _zeroValidatorsCountsOfInactiveModules if true, active and available validators for - * inactive modules are set to zero - * - * @return totalActiveValidators total active validators across all modules (excluding inactive - * if _zeroValidatorsCountsOfInactiveModules is true) - * @return stakingModulesCache array of StakingModuleCache structs - */ - function _loadStakingModulesCache(bool _zeroValidatorsCountsOfInactiveModules) internal view returns ( + /// @dev load modules into a memory cache + /// + /// @return totalActiveValidators total active validators across all modules + /// @return stakingModulesCache array of StakingModuleCache structs + function _loadStakingModulesCache() internal view returns ( uint256 totalActiveValidators, StakingModuleCache[] memory stakingModulesCache ) { uint256 stakingModulesCount = getStakingModulesCount(); stakingModulesCache = new StakingModuleCache[](stakingModulesCount); for (uint256 i; i < stakingModulesCount; ) { - stakingModulesCache[i] = _loadStakingModulesCacheItem(i, _zeroValidatorsCountsOfInactiveModules); + stakingModulesCache[i] = _loadStakingModulesCacheItem(i); totalActiveValidators += stakingModulesCache[i].activeValidatorsCount; unchecked { ++i; @@ -1064,10 +1099,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } - function _loadStakingModulesCacheItem( - uint256 _stakingModuleIndex, - bool _zeroValidatorsCountsIfInactive - ) internal view returns (StakingModuleCache memory cacheItem) { + function _loadStakingModulesCacheItem(uint256 _stakingModuleIndex) + internal + view + returns (StakingModuleCache memory cacheItem) + { StakingModule storage stakingModuleData = _getStakingModuleByIndex(_stakingModuleIndex); cacheItem.stakingModuleAddress = stakingModuleData.stakingModuleAddress; @@ -1077,17 +1113,28 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version cacheItem.targetShare = stakingModuleData.targetShare; cacheItem.status = StakingModuleStatus(stakingModuleData.status); - if (!_zeroValidatorsCountsIfInactive || cacheItem.status == StakingModuleStatus.Active) { - uint256 totalExitedValidators; - uint256 totalDepositedValidators; - (totalExitedValidators, totalDepositedValidators, cacheItem.availableValidatorsCount) - = IStakingModule(cacheItem.stakingModuleAddress).getStakingModuleSummary(); - - // the module might not receive all exited validators data yet => we need to replacing - // the exitedValidatorsCount with the one that the staking router is aware of - cacheItem.activeValidatorsCount = - totalDepositedValidators - - Math256.max(totalExitedValidators, stakingModuleData.exitedValidatorsCount); + ( + uint256 totalExitedValidators, + uint256 totalDepositedValidators, + uint256 depositableValidatorsCount + ) = IStakingModule(cacheItem.stakingModuleAddress).getStakingModuleSummary(); + + cacheItem.availableValidatorsCount = cacheItem.status == StakingModuleStatus.Active + ? depositableValidatorsCount + : 0; + + // the module might not receive all exited validators data yet => we need to replacing + // the exitedValidatorsCount with the one that the staking router is aware of + cacheItem.activeValidatorsCount = + totalDepositedValidators - + Math256.max(totalExitedValidators, stakingModuleData.exitedValidatorsCount); + } + + function _setStakingModuleStatus(StakingModule storage _stakingModule, StakingModuleStatus _status) internal { + StakingModuleStatus prevStatus = StakingModuleStatus(_stakingModule.status); + if (prevStatus != _status) { + _stakingModule.status = uint8(_status); + emit StakingModuleStatusSet(_stakingModule.id, _status, msg.sender); } } @@ -1097,7 +1144,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version // calculate total used validators for operators uint256 totalActiveValidators; - (totalActiveValidators, stakingModulesCache) = _loadStakingModulesCache(true); + (totalActiveValidators, stakingModulesCache) = _loadStakingModulesCache(); uint256 stakingModulesCount = stakingModulesCache.length; allocations = new uint256[](stakingModulesCount); @@ -1145,11 +1192,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version return _getStakingModuleById(_stakingModuleId).stakingModuleAddress; } - function _getStakingModuleAddressByIndex(uint256 _stakingModuleIndex) internal view returns (address) { - return _getStakingModuleByIndex(_stakingModuleIndex).stakingModuleAddress; - } - - function _getStorageStakingModulesMapping() internal pure returns (mapping(uint256 => StakingModule) storage result) { bytes32 position = STAKING_MODULES_MAPPING_POSITION; assembly { diff --git a/contracts/0.8.9/WithdrawalQueue.sol b/contracts/0.8.9/WithdrawalQueue.sol index 1f190a64d..162c5fed6 100644 --- a/contracts/0.8.9/WithdrawalQueue.sol +++ b/contracts/0.8.9/WithdrawalQueue.sol @@ -46,7 +46,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); bytes32 public constant FINALIZE_ROLE = keccak256("FINALIZE_ROLE"); - bytes32 public constant BUNKER_MODE_REPORT_ROLE = keccak256("BUNKER_MODE_REPORT_ROLE"); + bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE"); /// @notice minimal possible sum that is possible to withdraw uint256 public constant MIN_STETH_WITHDRAWAL_AMOUNT = 100; @@ -62,7 +62,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit IWstETH public immutable WSTETH; /// @notice Emitted when the contract initialized - event InitializedV1(address _admin, address _pauser, address _resumer, address _finalizer, address _bunkerReporter); + event InitializedV1(address _admin); event BunkerModeEnabled(uint256 _sinceTimestamp); event BunkerModeDisabled(); @@ -82,30 +82,40 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @notice Initialize the contract storage explicitly. /// @param _admin admin address that can change every role. - /// @param _pauser address that will be able to pause the withdrawals - /// @param _resumer address that will be able to resume the withdrawals after pause - /// @param _finalizer address that can finalize requests in the queue - /// @param _bunkerReporter address that can report a bunker mode /// @dev Reverts if `_admin` equals to `address(0)` /// @dev NB! It's initialized in paused state by default and should be resumed explicitly to start /// @dev NB! Bunker mode is disabled by default - function initialize(address _admin, address _pauser, address _resumer, address _finalizer, address _bunkerReporter) + function initialize(address _admin) external { if (_admin == address(0)) revert AdminZeroAddress(); - _initialize(_admin, _pauser, _resumer, _finalizer, _bunkerReporter); + _initialize(_admin); } /// @notice Resume withdrawal requests placement and finalization - function resume() external whenPaused onlyRole(RESUME_ROLE) { + function resume() external { + _checkPaused(); + _checkRole(RESUME_ROLE, msg.sender); _resume(); } /// @notice Pause withdrawal requests placement and finalization. Claiming finalized requests will still be available /// @param _duration pause duration, seconds (use `PAUSE_INFINITELY` for unlimited) - function pause(uint256 _duration) external onlyRole(PAUSE_ROLE) { - _pause(_duration); + /// @dev Reverts with `ResumedExpected()` if contract is already paused + /// @dev Reverts with `AccessControl:...` reason if sender has no `PAUSE_ROLE` + /// @dev Reverts with `ZeroPauseDuration()` if zero duration is passed + function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { + _pauseFor(_duration); + } + + /// @notice Pause withdrawal requests placement and finalization. Claiming finalized requests will still be available + /// @param _pauseUntilInclusive the last second to pause until inclusive + /// @dev Reverts with `ResumeSinceInPast()` if the timestamp is in the past + /// @dev Reverts with `AccessControl:...` reason if sender has no `PAUSE_ROLE` + /// @dev Reverts with `ResumedExpected()` if contract is already paused + function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { + _pauseUntil(_pauseUntilInclusive); } /// @notice Request the sequence of stETH withdrawals according to passed `withdrawalRequestInputs` data @@ -116,9 +126,9 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @return requestIds an array of the created withdrawal requests function requestWithdrawals(uint256[] calldata amounts, address _owner) public - whenResumed returns (uint256[] memory requestIds) { + _checkResumed(); if (_owner == address(0)) _owner = msg.sender; requestIds = new uint256[](amounts.length); for (uint256 i = 0; i < amounts.length; ++i) { @@ -135,9 +145,9 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @return requestIds an array of the created withdrawal requests function requestWithdrawalsWstETH(uint256[] calldata amounts, address _owner) public - whenResumed returns (uint256[] memory requestIds) { + _checkResumed(); if (_owner == address(0)) _owner = msg.sender; requestIds = new uint256[](amounts.length); for (uint256 i = 0; i < amounts.length; ++i) { @@ -163,7 +173,6 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @return requestIds an array of the created withdrawal requests function requestWithdrawalsWithPermit(uint256[] calldata _amounts, address _owner, PermitInput calldata _permit) external - whenResumed returns (uint256[] memory requestIds) { STETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s); @@ -182,7 +191,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit uint256[] calldata _amounts, address _owner, PermitInput calldata _permit - ) external whenResumed returns (uint256[] memory requestIds) { + ) external returns (uint256[] memory requestIds) { WSTETH.permit(msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s); return requestWithdrawalsWstETH(_amounts, _owner); } @@ -213,7 +222,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @notice Returns array of claimable eth amounts that is locked for each request /// @param _requestIds array of request ids to find claimable ether for /// @param _hints checkpoint hint for each id. - /// Can be retrieved with `findCheckpointHints()` or `findCheckpointHintsUnbounded()` + /// Can be retrieved with `findCheckpointHints()` function getClaimableEther(uint256[] calldata _requestIds, uint256[] calldata _hints) external view @@ -228,7 +237,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @notice Claim a batch of withdrawal requests once finalized (claimable) sending ether to `_recipient` /// @param _requestIds array of request ids to claim /// @param _hints checkpoint hint for each id. - /// Can be retrieved with `findCheckpointHints()` or `findCheckpointHintsUnbounded()` + /// Can be retrieved with `findCheckpointHints()` /// @param _recipient address where claimed ether will be sent to /// @dev /// Reverts if recipient is equal to zero @@ -249,7 +258,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @notice Claim a batch of withdrawal requests once finalized (claimable) sending locked ether to the owner /// @param _requestIds array of request ids to claim /// @param _hints checkpoint hint for each id. - /// Can be retrieved with `findCheckpointHints()` or `findCheckpointHintsUnbounded()` + /// Can be retrieved with `findCheckpointHints()` /// @dev /// Reverts if any requestId or hint in arguments are not valid /// Reverts if any request is not finalized or already claimed @@ -280,7 +289,7 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit /// @param _lastIndex right boundary of the search range /// @return hintIds the hints for `claimWithdrawal` to find the checkpoint for the passed request ids function findCheckpointHints(uint256[] calldata _requestIds, uint256 _firstIndex, uint256 _lastIndex) - public + external view returns (uint256[] memory hintIds) { @@ -294,36 +303,30 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit } } - /// @notice Finds the list of hints for the given `_requestIds` searching among the checkpoints with indices - /// in the range `[1, lastCheckpointIndex]`. NB! Array of request ids should be sorted - /// @dev WARNING! OOG is possible if used onchain. - /// @param _requestIds ids of the requests sorted in the ascending order to get hints for - function findCheckpointHintsUnbounded(uint256[] calldata _requestIds) - public - view - returns (uint256[] memory hintIds) + /// @notice Finalize requests from last finalized one up to `_lastRequestIdToFinalize` + /// @dev ether to finalize all the requests should be calculated using `finalizationValue()` and sent along + function finalize(uint256[] calldata _batches, uint256 _maxShareRate) + external + payable { - return findCheckpointHints(_requestIds, 1, getLastCheckpointIndex()); - } + _checkResumed(); + _checkRole(FINALIZE_ROLE, msg.sender); - /// @notice Finalize requests from last finalized one up to `_lastRequestIdToFinalize` - /// @dev ether to finalize all the requests should be calculated using `finalizationBatch()` and sent along - /// - /// @param _nextFinalizedRequestId request index in the queue that will be last finalized request in a batch - function finalize(uint256 _nextFinalizedRequestId) external payable whenResumed onlyRole(FINALIZE_ROLE) { - _finalize(_nextFinalizedRequestId, msg.value); + _finalize(_batches, msg.value, _maxShareRate); } - /// @notice Update bunker mode state + /// @notice Update bunker mode state and last report timestamp /// @dev should be called by oracle /// - /// @param _isBunkerModeNow oracle report - /// @param _sinceTimestamp timestamp of start of the bunker mode - function updateBunkerMode(bool _isBunkerModeNow, uint256 _sinceTimestamp) - external - onlyRole(BUNKER_MODE_REPORT_ROLE) - { - if (_sinceTimestamp >= block.timestamp) revert InvalidReportTimestamp(); + /// @param _isBunkerModeNow is bunker mode reported by oracle + /// @param _bunkerStartTimestamp timestamp of start of the bunker mode + /// @param _currentReportTimestamp timestamp of the current report ref slot + function onOracleReport(bool _isBunkerModeNow, uint256 _bunkerStartTimestamp, uint256 _currentReportTimestamp) external { + _checkRole(ORACLE_ROLE, msg.sender); + if (_bunkerStartTimestamp >= block.timestamp) revert InvalidReportTimestamp(); + if (_currentReportTimestamp >= block.timestamp) revert InvalidReportTimestamp(); + + _setLastReportTimestamp(_currentReportTimestamp); bool isBunkerModeWasSetBefore = isBunkerModeActive(); @@ -331,9 +334,9 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit if (_isBunkerModeNow != isBunkerModeWasSetBefore) { // write previous timestamp to enable bunker or max uint to disable if (_isBunkerModeNow) { - BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(_sinceTimestamp); + BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(_bunkerStartTimestamp); - emit BunkerModeEnabled(_sinceTimestamp); + emit BunkerModeEnabled(_bunkerStartTimestamp); } else { BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(BUNKER_MODE_DISABLED_TIMESTAMP); @@ -357,23 +360,19 @@ abstract contract WithdrawalQueue is AccessControlEnumerable, PausableUntil, Wit function _emitTransfer(address from, address to, uint256 _requestId) internal virtual; /// @dev internal initialization helper. Doesn't check provided addresses intentionally - function _initialize(address _admin, address _pauser, address _resumer, address _finalizer, address _bunkerReporter) + function _initialize(address _admin) internal { _initializeQueue(); - _pause(PAUSE_INFINITELY); + _pauseFor(PAUSE_INFINITELY); _initializeContractVersionTo(1); _grantRole(DEFAULT_ADMIN_ROLE, _admin); - if (_pauser != address(0)) _grantRole(PAUSE_ROLE, _pauser); - if (_resumer != address(0)) _grantRole(RESUME_ROLE, _resumer); - if (_finalizer != address(0)) _grantRole(FINALIZE_ROLE, _finalizer); - if (_bunkerReporter != address(0)) _grantRole(BUNKER_MODE_REPORT_ROLE, _bunkerReporter); BUNKER_MODE_SINCE_TIMESTAMP_POSITION.setStorageUint256(BUNKER_MODE_DISABLED_TIMESTAMP); - emit InitializedV1(_admin, _pauser, _resumer, _finalizer, _bunkerReporter); + emit InitializedV1(_admin); } function _requestWithdrawal(uint256 _amountOfStETH, address _owner) internal returns (uint256 requestId) { diff --git a/contracts/0.8.9/WithdrawalQueueBase.sol b/contracts/0.8.9/WithdrawalQueueBase.sol index 74487cec4..4e5dfd896 100644 --- a/contracts/0.8.9/WithdrawalQueueBase.sol +++ b/contracts/0.8.9/WithdrawalQueueBase.sol @@ -6,10 +6,9 @@ pragma solidity 0.8.9; import "@openzeppelin/contracts-v4.4/utils/structs/EnumerableSet.sol"; import {UnstructuredStorage} from "./lib/UnstructuredStorage.sol"; -import {UnstructuredRefStorage} from "./lib/UnstructuredRefStorage.sol"; /// @title Queue to store and manage WithdrawalRequests. -/// @dev Use an optimizations to store discounts heavily inspired +/// @dev Use an optimizations to store max share rates for finalized requests heavily inspired /// by Aragon MiniMe token https://github.com/aragon/aragon-minime/blob/master/contracts/MiniMeToken.sol /// /// @author folkyatina @@ -18,29 +17,30 @@ abstract contract WithdrawalQueueBase { using UnstructuredStorage for bytes32; /// @notice precision base for share rate and discounting factor values in the contract - uint256 public constant E27_PRECISION_BASE = 1e27; - - /// @dev discount factor value that means no discount applying - uint96 internal constant NO_DISCOUNT = uint96(E27_PRECISION_BASE); - + uint256 internal constant E27_PRECISION_BASE = 1e27; + /// @dev maximal length of the batches array that oracle should deliver on finalization + uint256 public constant MAX_BATCHES_LENGTH = 36; /// @dev return value for the `find...` methods in case of no result uint256 internal constant NOT_FOUND = 0; - // queue for withdrawal requests, indexes (requestId) start from 1 + /// @dev queue for withdrawal requests, indexes (requestId) start from 1 bytes32 internal constant QUEUE_POSITION = keccak256("lido.WithdrawalQueue.queue"); - // length of the queue + /// @dev length of the queue bytes32 internal constant LAST_REQUEST_ID_POSITION = keccak256("lido.WithdrawalQueue.lastRequestId"); - // length of the finalized part of the queue. Always <= `requestCounter` + /// @dev length of the finalized part of the queue. Always <= `requestCounter` bytes32 internal constant LAST_FINALIZED_REQUEST_ID_POSITION = keccak256("lido.WithdrawalQueue.lastFinalizedRequestId"); - /// finalization discount history, indexes start from 1 + /// @dev finalization discount history, indexes start from 1 bytes32 internal constant CHECKPOINTS_POSITION = keccak256("lido.WithdrawalQueue.checkpoints"); - /// length of the checkpoints + /// @dev length of the checkpoints bytes32 internal constant LAST_CHECKPOINT_INDEX_POSITION = keccak256("lido.WithdrawalQueue.lastCheckpointIndex"); - /// amount of eth locked on contract for withdrawal + /// @dev amount of eth locked on contract for withdrawal bytes32 internal constant LOCKED_ETHER_AMOUNT_POSITION = keccak256("lido.WithdrawalQueue.lockedEtherAmount"); - /// withdrawal requests mapped to the owners + /// @dev withdrawal requests mapped to the owners bytes32 internal constant REQUEST_BY_OWNER_POSITION = keccak256("lido.WithdrawalQueue.requestsByOwner"); + /// @dev timestamp of the last oracle report + bytes32 internal constant LAST_REPORT_TIMESTAMP_POSITION = keccak256("lido.WithdrawalQueue.lastReportTimestamp"); + /// @notice structure representing a request for withdrawal. struct WithdrawalRequest { @@ -51,18 +51,17 @@ abstract contract WithdrawalQueueBase { /// @notice address that can claim or transfer the request address owner; /// @notice block.timestamp when the request was created - uint64 timestamp; + uint40 timestamp; /// @notice flag if the request was claimed bool claimed; + /// @notice timestamp of last oracle report for this request + uint40 reportTimestamp; } - /// @notice structure to store discount factors for requests in the queue - struct DiscountCheckpoint { - /// @notice first `_requestId` the discount is valid for - /// @dev storing in uint160 to pack into one slot. Overflowing here is unlikely - uint160 fromRequestId; - /// @notice discount factor with 1e27 precision (0 - 100% discount, 1e27 - means no discount) - uint96 discountFactor; + /// @notice structure to store discounts for requests that are affected by negative rebase + struct Checkpoint { + uint256 fromRequestId; + uint256 maxShareRate; } /// @notice output format struct for `_getWithdrawalStatus()` method @@ -103,6 +102,9 @@ abstract contract WithdrawalQueueBase { error NotOwner(address _sender, address _owner); error InvalidRequestId(uint256 _requestId); error InvalidRequestIdRange(uint256 startId, uint256 endId); + error InvalidState(); + error BatchesAreNotSorted(); + error EmptyBatches(); error RequestNotFoundOrNotFinalized(uint256 _requestId); error NotEnoughEther(); error RequestAlreadyClaimed(uint256 _requestId); @@ -140,169 +142,225 @@ abstract contract WithdrawalQueueBase { _getQueue()[getLastRequestId()].cumulativeStETH - _getQueue()[getLastFinalizedRequestId()].cumulativeStETH; } - /// @notice Search for the latest request in the queue in the range of `[startId, endId]`, - /// that has `request.timestamp <= maxTimestamp` + // + // FINALIZATION FLOW + // + // Process when protocol is fixing the withdrawal request value and lock the required amount of ETH. + // The value of a request after finalization can be: + // - nominal (when the amount of eth locked for this request are equal to the request's stETH) + // - discounted (when the amount of eth will be lower, because the protocol share rate dropped + // before request is finalized, so it will be equal to `request's shares` * `protocol share rate`) + // The parameters that are required for finalization are: + // - current share rate of the protocol + // - id of the last request that can be finalized + // - the amount of eth that must be locked for these requests + // To calculate the eth amount we'll need to know which requests int the queue will be finalized as nominal + // and which as discounted and the exact value of the discount. It's impossible to calculate without the unbounded + // loop over the unfinalized part of the queue. So, we need to extract a part of the algorithm off-chain, bring the + // result with oracle report and check it later and check the resukt later. + // So, we came to this solution: + // Off-chain + // 1. Oracle iterates over the queue off-chain and calculate the id of the latest finalizable request + // in the queue. Then it splits all the requests that will be finalized into batches the way, + // that requests in a batch are all nominal or all discounted. + // And passes them in the report as the array of the ending ids of these batches. So it can be reconstructed like + // `[lastFinalizedRequestId+1, batches[0]], [batches[0]+1, batches[1]] ... [batches[n-2], batches[n-1]]` + // 2. Contract checks the validity of the batches on-chain and calculate the amount of eth required to + // finalize them. It can be done without unbounded loop using partial sums that are calculated on request enqueueing. + // 3. Contract marks the request's as finalized and locks the eth for claiming. It also, + // set's the discount checkpoint for these request's if required that will be applied on claim for each request's + // individually depending on request's share rate. + + /// @notice transient state that is used to pass intemediate results between several `calculateFinalizationBatches` + // invokations + struct BatchesCalculationState { + /// @notice amount of ether available in the protocol that can be used to finalize withdrawal requests + /// Will decrease on each invokation and will be equal to the remainder when calculation is finished + /// Should be set before the first invokation + uint256 remainingEthBudget; + /// @notice flag that is `true` if returned state is final and `false` if more invokations required + bool finished; + /// @notice static array to store all the batches ending request id + uint256[MAX_BATCHES_LENGTH] batches; + /// @notice length of the filled part of `batches` array + uint256 batchesLength; + } + + /// @notice Offchain view for the oracle daemon that calculates how many requests can be finalized within + /// the given budget and timestamp and share rate limits. Returned requests are split into the batches. + /// Each batch consist of the requests that all have the share rate below the `_maxShareRate` or above it. + /// Below you can see an example how 14 requests with different share rates will be split into 5 batches by + /// this algorithm /// - /// @return finalizableRequestId or 0, if there are no requests in a range with requested timestamp - function findLastFinalizableRequestIdByTimestamp(uint256 _maxTimestamp, uint256 _startId, uint256 _endId) - public + /// ^ share rate + /// | + /// | • • + /// | • • • • • + /// |----------------------•------ _maxShareRate + /// | • • • • • + /// | • + /// +-------------------------------> requestId + /// | 1st| 2nd |3| 4th | 5th | + /// + /// @param _maxShareRate current share rate of the protocol with 1e27 precision + /// @param _maxTimestamp max timestamp of the request that can be finalized + /// @param _maxRequestsPerCall max request number that can be processed by the call. Better to me max possible + /// number for EL node to handle before hitting `out of gas`. More this number is less calls it will require to + /// calculate the result + /// @param _state structure that accumulates the state across multiple invokations to overcome gas limits. + /// To start calculation you should pass `state.remainingEthBudget` and `state.finished == false` and then invoke + /// the function with returned `state` until it returns a state with `finished` flag set + /// @return state that was changed during this function invokation. + /// If (state.finished) than calculation is finished and returned `state` is ready to be used + function calculateFinalizationBatches( + uint256 _maxShareRate, + uint256 _maxTimestamp, + uint256 _maxRequestsPerCall, + BatchesCalculationState memory _state + ) + external view - returns (uint256 finalizableRequestId) + returns (BatchesCalculationState memory) { - if (_maxTimestamp == 0) revert ZeroTimestamp(); - if (_startId <= getLastFinalizedRequestId() || _endId > getLastRequestId()) { - revert InvalidRequestIdRange(_startId, _endId); - } - - if (_startId > _endId) return NOT_FOUND; // we have an empty range to search in + if (_state.finished || _state.remainingEthBudget == 0) revert InvalidState(); - uint256 min = _startId; - uint256 max = _endId; + uint256 currentId; + WithdrawalRequest memory prevRequest; + uint256 prevRequestShareRate; - finalizableRequestId = NOT_FOUND; + if (_state.batchesLength == 0) { + currentId = getLastFinalizedRequestId() + 1; - while (min <= max) { - uint256 mid = (max + min) / 2; - if (_getQueue()[mid].timestamp <= _maxTimestamp) { - finalizableRequestId = mid; + prevRequest = _getQueue()[currentId - 1]; + } else { + uint256 lastHandledRequestId = _state.batches[_state.batchesLength - 1]; + currentId = lastHandledRequestId + 1; - // Ignore left half - min = mid + 1; - } else { - // Ignore right half - max = mid - 1; - } - } - } - - /// @notice Search for the latest request in the queue in the range of `[startId, endId]`, - /// that can be finalized within the given `_ethBudget` by `_shareRate` - /// @param _ethBudget amount of ether available for withdrawal fulfillment - /// @param _shareRate share/ETH rate that will be used for fulfillment - /// @param _startId requestId to start search from. Should be > lastFinalizedRequestId - /// @param _endId requestId to search upon to. Should be <= lastRequestId - /// - /// @return finalizableRequestId or 0, if there are no requests finalizable within the given `_ethBudget` - function findLastFinalizableRequestIdByBudget( - uint256 _ethBudget, - uint256 _shareRate, - uint256 _startId, - uint256 _endId - ) public view returns (uint256 finalizableRequestId) { - if (_ethBudget == 0) revert ZeroAmountOfETH(); - if (_shareRate == 0) revert ZeroShareRate(); - if (_startId <= getLastFinalizedRequestId() || _endId > getLastRequestId()) { - revert InvalidRequestIdRange(_startId, _endId); + prevRequest = _getQueue()[lastHandledRequestId]; + (prevRequestShareRate,,) = _calcBatch(_getQueue()[lastHandledRequestId - 1], prevRequest); } - if (_startId > _endId) return NOT_FOUND; // we have an empty range to search in + uint256 nextCallRequestId = currentId + _maxRequestsPerCall; + uint256 queueLength = getLastRequestId() + 1; - uint256 min = _startId; - uint256 max = _endId; + while (currentId < queueLength && currentId < nextCallRequestId) { + WithdrawalRequest memory request = _getQueue()[currentId]; - finalizableRequestId = NOT_FOUND; + if (request.timestamp > _maxTimestamp) break; // max timestamp break - while (min <= max) { - uint256 mid = (max + min) / 2; - (uint256 requiredEth,) = finalizationBatch(mid, _shareRate); + (uint256 requestShareRate, uint256 ethToFinalize, uint256 shares) = _calcBatch(prevRequest, request); - if (requiredEth <= _ethBudget) { - finalizableRequestId = mid; + if (requestShareRate > _maxShareRate) { + // discounted + ethToFinalize = (shares * _maxShareRate) / E27_PRECISION_BASE; + } - // Ignore left half - min = mid + 1; + if (ethToFinalize > _state.remainingEthBudget) break; // budget break + _state.remainingEthBudget -= ethToFinalize; + + if (_state.batchesLength != 0 && ( + // share rate of requests in the same batch can differ by 1-2 wei because of the rounding error + // (issue: https://github.com/lidofinance/lido-dao/issues/442 ) + // so we're counting requests that are placed during the same report day + // as equal even if their actual share rate are different + prevRequest.reportTimestamp == request.reportTimestamp || + // both requests are below or + prevRequestShareRate <= _maxShareRate && requestShareRate <= _maxShareRate || + // both are above the line + prevRequestShareRate > _maxShareRate && requestShareRate > _maxShareRate + )) { + _state.batches[_state.batchesLength - 1] = currentId; // extend the last batch } else { - // Ignore right half - max = mid - 1; + // to be able to check batches on-chain we need it to have fixed max length + if (_state.batchesLength == MAX_BATCHES_LENGTH) break; + + // create a new batch + _state.batches[_state.batchesLength] = currentId; + ++_state.batchesLength; } + + prevRequestShareRate = requestShareRate; + prevRequest = request; + unchecked{ ++currentId; } } - } - /// @notice Returns last `requestId` finalizable under given conditions - /// @param _ethBudget max amount of eth to be used for finalization - /// @param _shareRate share rate that will be applied to requests - /// @param _maxTimestamp timestamp that requests should be created before - /// - /// @dev WARNING! OOG is possible if used onchain, contains unbounded loop inside - /// @return finalizableRequestId or 0, if there are no requests finalizable under given conditions - function findLastFinalizableRequestId(uint256 _ethBudget, uint256 _shareRate, uint256 _maxTimestamp) - external - view - returns (uint256 finalizableRequestId) - { - uint256 firstUnfinalizedRequestId = getLastFinalizedRequestId() + 1; - finalizableRequestId = - findLastFinalizableRequestIdByBudget(_ethBudget, _shareRate, firstUnfinalizedRequestId, getLastRequestId()); - return findLastFinalizableRequestIdByTimestamp(_maxTimestamp, firstUnfinalizedRequestId, finalizableRequestId); + _state.finished = currentId == queueLength || currentId < nextCallRequestId; + + return _state; } - /// @notice Calculates the amount of ETH required to finalize the batch of requests in the queue up to - /// `_nextFinalizedRequestId` with given `_shareRate` and the amount of shares that should be burned after - /// @param _nextFinalizedRequestId id of the ending request in the finalization batch (>0 and <=lastRequestId) - /// @param _shareRate share rate that will be used to calculate the batch (1e27 precision, >0) - /// - /// @return ethToLock amount of ETH required to finalize the batch - /// @return sharesToBurn amount of shares that should be burned after the finalization - function finalizationBatch(uint256 _nextFinalizedRequestId, uint256 _shareRate) - public + /// @notice Checks the finalization batches, calculates required ether and the amount of shares to burn and + /// @param _batches finalization batches calculated offchain using `calculateFinalizationBatches` + /// @param _maxShareRate max possible share rate that will be used for request finalization with 1e27 precision + /// @return ethToLock amount of ether that should be sent with `finalize()` method later + /// @return sharesToBurn amount of shares that belongs tho finalizable requests + function prefinalize(uint256[] calldata _batches, uint256 _maxShareRate) + external view returns (uint256 ethToLock, uint256 sharesToBurn) { - if (_shareRate == 0) revert ZeroShareRate(); - if (_nextFinalizedRequestId > getLastRequestId()) revert InvalidRequestId(_nextFinalizedRequestId); - uint256 lastFinalizedRequestId = getLastFinalizedRequestId(); - if (_nextFinalizedRequestId <= lastFinalizedRequestId) revert InvalidRequestId(_nextFinalizedRequestId); + if (_maxShareRate == 0) revert ZeroShareRate(); + if (_batches.length == 0) revert EmptyBatches(); - WithdrawalRequest memory requestToFinalize = _getQueue()[_nextFinalizedRequestId]; - WithdrawalRequest memory lastFinalizedRequest = _getQueue()[lastFinalizedRequestId]; + if (_batches[0] <= getLastFinalizedRequestId()) revert InvalidRequestId(_batches[0]); + if (_batches[_batches.length - 1] > getLastRequestId()) revert InvalidRequestId(_batches[_batches.length - 1]); + + uint256 currentBatchIndex; + uint256 prevBatchEndRequestId = getLastFinalizedRequestId(); + WithdrawalRequest memory prevBatchEnd = _getQueue()[prevBatchEndRequestId]; + while (currentBatchIndex < _batches.length) { + uint256 batchEndRequestId = _batches[currentBatchIndex]; + if (batchEndRequestId <= prevBatchEndRequestId) revert BatchesAreNotSorted(); - uint256 amountOfETHRequested = requestToFinalize.cumulativeStETH - lastFinalizedRequest.cumulativeStETH; - uint256 amountOfShares = requestToFinalize.cumulativeShares - lastFinalizedRequest.cumulativeShares; + WithdrawalRequest memory batchEnd = _getQueue()[batchEndRequestId]; - ethToLock = amountOfETHRequested; - sharesToBurn = amountOfShares; + (uint256 batchShareRate, uint256 stETH, uint256 shares) = _calcBatch(prevBatchEnd, batchEnd); - uint256 currentValueInETH = (amountOfShares * _shareRate) / E27_PRECISION_BASE; - if (currentValueInETH < amountOfETHRequested) { - ethToLock = currentValueInETH; + if (batchShareRate > _maxShareRate) { + // discounted + ethToLock += shares * _maxShareRate / E27_PRECISION_BASE; + } else { + // nominal + ethToLock += stETH; + } + sharesToBurn += shares; + + prevBatchEndRequestId = batchEndRequestId; + prevBatchEnd = batchEnd; + unchecked{ ++currentBatchIndex; } } } - /// @dev Finalize requests from last finalized one up to `_nextFinalizedRequestId` + /// @dev Finalize requests in the queue /// Emits WithdrawalBatchFinalized event. - function _finalize(uint256 _nextFinalizedRequestId, uint256 _amountOfETH) internal { - if (_nextFinalizedRequestId > getLastRequestId()) revert InvalidRequestId(_nextFinalizedRequestId); + /// Checks that: + /// - _amountOfETH is less or equal to the nominal value of all requests to be finalized + function _finalize(uint256[] memory _batches, uint256 _amountOfETH, uint256 _maxShareRate) internal { + if (_batches.length == 0) revert EmptyBatches(); + uint256 lastRequestIdToBeFinalized = _batches[_batches.length - 1]; + if (lastRequestIdToBeFinalized > getLastRequestId()) revert InvalidRequestId(lastRequestIdToBeFinalized); uint256 lastFinalizedRequestId = getLastFinalizedRequestId(); - uint256 firstUnfinalizedRequestId = lastFinalizedRequestId + 1; - if (_nextFinalizedRequestId <= lastFinalizedRequestId) revert InvalidRequestId(_nextFinalizedRequestId); + if (lastRequestIdToBeFinalized <= lastFinalizedRequestId) revert InvalidRequestId(lastRequestIdToBeFinalized); WithdrawalRequest memory lastFinalizedRequest = _getQueue()[lastFinalizedRequestId]; - WithdrawalRequest memory requestToFinalize = _getQueue()[_nextFinalizedRequestId]; + WithdrawalRequest memory requestToFinalize = _getQueue()[lastRequestIdToBeFinalized]; uint128 stETHToFinalize = requestToFinalize.cumulativeStETH - lastFinalizedRequest.cumulativeStETH; if (_amountOfETH > stETHToFinalize) revert TooMuchEtherToFinalize(_amountOfETH, stETHToFinalize); - uint256 discountFactor = NO_DISCOUNT; - if (stETHToFinalize > _amountOfETH) { - discountFactor = _amountOfETH * E27_PRECISION_BASE / stETHToFinalize; - } - + uint256 firstRequestIdToFinalize = lastFinalizedRequestId + 1; uint256 lastCheckpointIndex = getLastCheckpointIndex(); - DiscountCheckpoint storage lastCheckpoint = _getCheckpoints()[lastCheckpointIndex]; - if (discountFactor != lastCheckpoint.discountFactor) { - // add a new discount if it differs from the previous - _getCheckpoints()[lastCheckpointIndex + 1] = - DiscountCheckpoint(uint160(firstUnfinalizedRequestId), uint96(discountFactor)); - _setLastCheckpointIndex(lastCheckpointIndex + 1); - } + // add a new checkpoint with current finalization max share rate + _getCheckpoints()[lastCheckpointIndex + 1] = Checkpoint(firstRequestIdToFinalize, _maxShareRate); + _setLastCheckpointIndex(lastCheckpointIndex + 1); _setLockedEtherAmount(getLockedEtherAmount() + _amountOfETH); - _setLastFinalizedRequestId(_nextFinalizedRequestId); + _setLastFinalizedRequestId(lastRequestIdToBeFinalized); emit WithdrawalBatchFinalized( - firstUnfinalizedRequestId, - _nextFinalizedRequestId, + firstRequestIdToFinalize, + lastRequestIdToBeFinalized, _amountOfETH, requestToFinalize.cumulativeShares - lastFinalizedRequest.cumulativeShares, block.timestamp @@ -325,14 +383,22 @@ abstract contract WithdrawalQueueBase { requestId = lastRequestId + 1; _setLastRequestId(requestId); - _getQueue()[requestId] = - WithdrawalRequest(cumulativeStETH, cumulativeShares, _owner, uint64(block.timestamp), false); + + WithdrawalRequest memory newRequest = WithdrawalRequest( + cumulativeStETH, + cumulativeShares, + _owner, + uint40(block.timestamp), + false, + uint40(_getLastReportTimestamp()) + ); + _getQueue()[requestId] = newRequest; assert(_getRequestsByOwner()[_owner].add(requestId)); emit WithdrawalRequested(requestId, msg.sender, _owner, _amountOfStETH, _amountOfShares); } - /// @notice Returns status of the withdrawal request with `_requestId` id + /// @dev Returns status of the withdrawal request with `_requestId` id function _getStatus(uint256 _requestId) internal view returns (WithdrawalRequestStatus memory status) { if (_requestId == 0 || _requestId > getLastRequestId()) revert InvalidRequestId(_requestId); @@ -349,7 +415,7 @@ abstract contract WithdrawalQueueBase { ); } - /// @notice View function to find a checkpoint hint for `claimWithdrawal()` + /// @dev View function to find a checkpoint hint for `claimWithdrawal()` /// Search will be performed in the range of `[_firstIndex, _lastIndex]` /// /// NB!: Range search ought to be used to optimize gas cost. @@ -401,7 +467,7 @@ abstract contract WithdrawalQueueBase { return min; } - /// @notice Claim `_requestId` request and transfer locked ether to `_recipient`. Emits WithdrawalClaimed event + /// @dev Claim `_requestId` request and transfer locked ether to `_recipient`. Emits WithdrawalClaimed event /// @param _requestId request id to claim /// @param _hint hint for discount checkpoint index to avoid extensive search over the checkpoints. /// @param _recipient address to send ether to @@ -418,14 +484,16 @@ abstract contract WithdrawalQueueBase { assert(_getRequestsByOwner()[request.owner].remove(_requestId)); uint256 ethWithDiscount = _calculateClaimableEther(request, _requestId, _hint); - + // because of the stETH rounding issue + // (issue: https://github.com/lidofinance/lido-dao/issues/442 ) + // some dust (1-2 wei per request) will be accumulated upon claiming _setLockedEtherAmount(getLockedEtherAmount() - ethWithDiscount); _sendValue(payable(_recipient), ethWithDiscount); emit WithdrawalClaimed(_requestId, msg.sender, _recipient, ethWithDiscount); } - /// @notice Calculates discounted ether value for `_requestId` using a provided `_hint`. Checks if hint is valid + /// @dev Calculates discounted ether value for `_requestId` using a provided `_hint`. Checks if hint is valid /// @return claimableEther discounted eth for `_requestId`. Returns 0 if request is not claimable function _calculateClaimableEther(WithdrawalRequest storage _request, uint256 _requestId, uint256 _hint) internal @@ -437,30 +505,35 @@ abstract contract WithdrawalQueueBase { uint256 lastCheckpointIndex = getLastCheckpointIndex(); if (_hint > lastCheckpointIndex) revert InvalidHint(_hint); - DiscountCheckpoint memory hintCheckpoint = _getCheckpoints()[_hint]; + Checkpoint memory checkpoint = _getCheckpoints()[_hint]; + // Reverts if requestId is not in range [checkpoint[hint], checkpoint[hint+1]) // ______(>______ // ^ hint - if (_requestId < hintCheckpoint.fromRequestId) revert InvalidHint(_hint); + if (_requestId < checkpoint.fromRequestId) revert InvalidHint(_hint); if (_hint < lastCheckpointIndex) { // ______(>______(>________ // hint hint+1 ^ - DiscountCheckpoint memory nextCheckpoint = _getCheckpoints()[_hint + 1]; - if (nextCheckpoint.fromRequestId <= _requestId) { - revert InvalidHint(_hint); - } + Checkpoint memory nextCheckpoint = _getCheckpoints()[_hint + 1]; + if (nextCheckpoint.fromRequestId <= _requestId) revert InvalidHint(_hint); } - uint256 ethRequested = _request.cumulativeStETH - _getQueue()[_requestId - 1].cumulativeStETH; - return ethRequested * hintCheckpoint.discountFactor / E27_PRECISION_BASE; + WithdrawalRequest memory prevRequest = _getQueue()[_requestId - 1]; + (uint256 batchShareRate, uint256 eth, uint256 shares) = _calcBatch(prevRequest, _request); + + if (batchShareRate > checkpoint.maxShareRate) { + eth = shares * checkpoint.maxShareRate / E27_PRECISION_BASE; + } + + return eth; } - // quazi-constructor + /// @dev quazi-constructor function _initializeQueue() internal { // setting dummy zero structs in checkpoints and queue beginning // to avoid uint underflows and related if-branches // 0-index is reserved as 'not_found' response in the interface everywhere - _getQueue()[0] = WithdrawalRequest(0, 0, address(0), uint64(block.number), true); - _getCheckpoints()[getLastCheckpointIndex()] = DiscountCheckpoint(0, 0); + _getQueue()[0] = WithdrawalRequest(0, 0, address(0), uint40(block.timestamp), true, 0); + _getCheckpoints()[getLastCheckpointIndex()] = Checkpoint(0, 0); } function _sendValue(address _recipient, uint256 _amount) internal { @@ -471,8 +544,19 @@ abstract contract WithdrawalQueueBase { if (!success) revert CantSendValueRecipientMayHaveReverted(); } + /// @dev calculate batch stats (shareRate, stETH and shares) for the batch of `(_preStartRequest, _endRequest]` + function _calcBatch( + WithdrawalRequest memory _preStartRequest, + WithdrawalRequest memory _endRequest + ) internal pure returns (uint256 shareRate, uint256 stETH, uint256 shares) { + stETH = _endRequest.cumulativeStETH - _preStartRequest.cumulativeStETH; + shares = _endRequest.cumulativeShares - _preStartRequest.cumulativeShares; + + shareRate = stETH * E27_PRECISION_BASE / shares; + } + // - // Internal getters and setters + // Internal getters and setters for unstructured storage // function _getQueue() internal pure returns (mapping(uint256 => WithdrawalRequest) storage queue) { bytes32 position = QUEUE_POSITION; @@ -481,7 +565,7 @@ abstract contract WithdrawalQueueBase { } } - function _getCheckpoints() internal pure returns (mapping(uint256 => DiscountCheckpoint) storage checkpoints) { + function _getCheckpoints() internal pure returns (mapping(uint256 => Checkpoint) storage checkpoints) { bytes32 position = CHECKPOINTS_POSITION; assembly { checkpoints.slot := position @@ -499,6 +583,10 @@ abstract contract WithdrawalQueueBase { } } + function _getLastReportTimestamp() internal view returns (uint256) { + return LAST_REPORT_TIMESTAMP_POSITION.getStorageUint256(); + } + function _setLastRequestId(uint256 _lastRequestId) internal { LAST_REQUEST_ID_POSITION.setStorageUint256(_lastRequestId); } @@ -514,4 +602,8 @@ abstract contract WithdrawalQueueBase { function _setLockedEtherAmount(uint256 _lockedEtherAmount) internal { LOCKED_ETHER_AMOUNT_POSITION.setStorageUint256(_lockedEtherAmount); } + + function _setLastReportTimestamp(uint256 _lastReportTimestamp) internal { + LAST_REPORT_TIMESTAMP_POSITION.setStorageUint256(_lastReportTimestamp); + } } diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index dee857212..e7c878de9 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -103,6 +103,16 @@ interface IStakingModule { /// @param _refundedValidatorsCount New number of refunded validators of the node operator function updateRefundedValidatorsCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) external; + /// @notice Updates the limit of the validators that can be used for deposit + /// @param _nodeOperatorId Id of the node operator + /// @param _isTargetLimitActive Active flag + /// @param _targetLimit Target limit of the node operator + function updateTargetValidatorsLimits( + uint256 _nodeOperatorId, + bool _isTargetLimitActive, + uint256 _targetLimit + ) external; + /// @notice Unsafely updates the number of validators in the EXITED/STUCK states for node operator with given id /// 'unsafely' means that this method can both increase and decrease exited and stuck counters /// @param _nodeOperatorId Id of the node operator diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index f6ef31d40..53463f59d 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -21,8 +21,9 @@ interface ILido { // EL values uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, // Decision about withdrawals processing - uint256 _lastFinalizableRequestId, + uint256[] calldata _withdrawalFinalizationBatches, uint256 _simulatedShareRate ) external; } @@ -80,7 +81,7 @@ interface IStakingRouter { interface IWithdrawalQueue { - function updateBunkerMode(bool isBunkerMode, uint256 prevReportTimestamp) external; + function onOracleReport(bool isBunkerMode, uint256 prevReportTimestamp, uint256 currentReportTimestamp) external; } @@ -232,19 +233,25 @@ contract AccountingOracle is BaseOracle { /// at the reference slot. uint256 elRewardsVaultBalance; + /// @dev The shares amount requested to burn through Burner as observed + /// at the reference slot. The value can be obtained in the following way: + /// `(coverSharesToBurn, nonCoverSharesToBurn) = IBurner(burner).getSharesRequestedToBurn() + /// sharesRequestedToBurn = coverSharesToBurn + nonCoverSharesToBurn` + uint256 sharesRequestedToBurn; + /// /// Decision /// - /// @dev The id of the last withdrawal request that should be finalized as the result - /// of applying this oracle report. The zero value means that no requests should be - /// finalized. - uint256 lastFinalizableWithdrawalRequestId; + /// @dev The ascendingly-sorted array of withdrawal request IDs obtained by calling + /// WithdrawalQueue.calculateFinalizationBatches. Empty array means that no withdrawal + /// requests should be finalized. + uint256[] withdrawalFinalizationBatches; /// @dev The share/ETH rate with the 10^27 precision (i.e. the price of one stETH share - /// in ETH where one ETH is denominated as 10^27) used for finalizing withdrawal requests - /// up to (and including) the one passed in the lastWithdrawalRequestIdToFinalize field. - /// Must be set to zero if lastWithdrawalRequestIdToFinalize is zero. + /// in ETH where one ETH is denominated as 10^27) that would be effective as the result of + /// applying this oracle report at the reference slot, with withdrawalFinalizationBatches + /// set to empty array and simulatedShareRate set to 0. uint256 simulatedShareRate; /// @dev Whether, based on the state observed at the reference slot, the protocol should @@ -598,9 +605,10 @@ contract AccountingOracle is BaseOracle { slotsElapsed ); - withdrawalQueue.updateBunkerMode( + withdrawalQueue.onOracleReport( data.isBunkerMode, - GENESIS_TIME + prevRefSlot * SECONDS_PER_SLOT + GENESIS_TIME + prevRefSlot * SECONDS_PER_SLOT, + GENESIS_TIME + data.refSlot * SECONDS_PER_SLOT ); ILido(LIDO).handleOracleReport( @@ -610,7 +618,8 @@ contract AccountingOracle is BaseOracle { data.clBalanceGwei * 1e9, data.withdrawalVaultBalance, data.elRewardsVaultBalance, - data.lastFinalizableWithdrawalRequestId, + data.sharesRequestedToBurn, + data.withdrawalFinalizationBatches, data.simulatedShareRate ); @@ -684,7 +693,7 @@ contract AccountingOracle is BaseOracle { } function _checkCanSubmitExtraData(ExtraDataProcessingState memory procState, uint256 format) - internal + internal view { _checkMsgSenderIsAllowedToSubmitData(); _checkProcessingDeadline(); diff --git a/contracts/0.8.9/oracle/BaseOracle.sol b/contracts/0.8.9/oracle/BaseOracle.sol index 1292db0d2..e44057cfc 100644 --- a/contracts/0.8.9/oracle/BaseOracle.sol +++ b/contracts/0.8.9/oracle/BaseOracle.sol @@ -26,6 +26,8 @@ interface IConsensusContract { ); function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame); + + function getInitialRefSlot() external view returns (uint256); } @@ -38,7 +40,7 @@ abstract contract BaseOracle is IReportAsyncProcessor, AccessControlEnumerable, error VersionCannotBeSame(); error UnexpectedChainConfig(); error OnlyConsensusContractCanSubmitReport(); - error RefSlotCannotBeLessThanProcessingOne(uint256 refSlot, uint256 processingRefSlot); + error InitialRefSlotCannotBeLessThanProcessingOne(uint256 initialRefSlot, uint256 processingRefSlot); error RefSlotMustBeGreaterThanProcessingOne(uint256 refSlot, uint256 processingRefSlot); error RefSlotCannotDecrease(uint256 refSlot, uint256 prevRefSlot); error ProcessingDeadlineMissed(uint256 deadline); @@ -319,9 +321,9 @@ abstract contract BaseOracle is IReportAsyncProcessor, AccessControlEnumerable, revert UnexpectedChainConfig(); } - (uint256 refSlot, ) = IConsensusContract(addr).getCurrentFrame(); - if (refSlot < lastProcessingRefSlot) { - revert RefSlotCannotBeLessThanProcessingOne(refSlot, lastProcessingRefSlot); + uint256 initialRefSlot = IConsensusContract(addr).getInitialRefSlot(); + if (initialRefSlot < lastProcessingRefSlot) { + revert InitialRefSlotCannotBeLessThanProcessingOne(initialRefSlot, lastProcessingRefSlot); } CONSENSUS_CONTRACT_POSITION.setStorageAddress(addr); diff --git a/contracts/0.8.9/oracle/HashConsensus.sol b/contracts/0.8.9/oracle/HashConsensus.sol index 92161fc96..b8a949d85 100644 --- a/contracts/0.8.9/oracle/HashConsensus.sol +++ b/contracts/0.8.9/oracle/HashConsensus.sol @@ -76,6 +76,8 @@ contract HashConsensus is AccessControlEnumerable { error DuplicateMember(); error AddressCannotBeZero(); error InitialEpochIsYetToArrive(); + error InitialEpochAlreadyArrived(); + error InitialEpochRefSlotCannotBeEarlierThanProcessingSlot(); error EpochsPerFrameCannotBeZero(); error NonMember(); error UnexpectedConsensusVersion(uint256 expected, uint256 received); @@ -205,7 +207,6 @@ contract HashConsensus is AccessControlEnumerable { uint256 secondsPerSlot, uint256 genesisTime, uint256 epochsPerFrame, - uint256 initialEpoch, uint256 fastLaneLengthSlots, address admin, address reportProcessor @@ -216,8 +217,12 @@ contract HashConsensus is AccessControlEnumerable { if (admin == address(0)) revert AdminCannotBeZero(); if (reportProcessor == address(0)) revert ReportProcessorCannotBeZero(); + _setupRole(DEFAULT_ADMIN_ROLE, admin); - _setFrameConfig(initialEpoch, epochsPerFrame, fastLaneLengthSlots, FrameConfig(0, 0, 0)); + + uint256 farFutureEpoch = _computeEpochAtTimestamp(type(uint64).max); + _setFrameConfig(farFutureEpoch, epochsPerFrame, fastLaneLengthSlots, FrameConfig(0, 0, 0)); + _reportProcessor = reportProcessor; } @@ -252,6 +257,35 @@ contract HashConsensus is AccessControlEnumerable { return (frame.refSlot, frame.reportProcessingDeadlineSlot); } + /// @notice Returns the earliest possible reference slot. + /// + function getInitialRefSlot() external view returns (uint256) { + return _getInitialFrame().refSlot; + } + + /// @notice Sets initial epoch given that the current initial epoch is in the future. + /// + /// @param initialEpoch The new initial epoch. + /// + function updateInitialEpoch(uint256 initialEpoch) external onlyRole(DEFAULT_ADMIN_ROLE) { + FrameConfig memory prevConfig = _frameConfig; + + if (_computeEpochAtTimestamp(_getTime()) >= prevConfig.initialEpoch) { + revert InitialEpochAlreadyArrived(); + } + + _setFrameConfig( + initialEpoch, + prevConfig.epochsPerFrame, + prevConfig.fastLaneLengthSlots, + prevConfig + ); + + if (_getInitialFrame().refSlot < _getLastProcessingRefSlot()) { + revert InitialEpochRefSlotCannotBeEarlierThanProcessingSlot(); + } + } + function setFrameConfig(uint256 epochsPerFrame, uint256 fastLaneLengthSlots) external onlyRole(MANAGE_FRAME_CONFIG_ROLE) { @@ -532,10 +566,19 @@ contract HashConsensus is AccessControlEnumerable { return _getFrameAtTimestamp(_getTime(), config); } + function _getInitialFrame() internal view returns (ConsensusFrame memory) { + return _getFrameAtIndex(0, _frameConfig); + } + function _getFrameAtTimestamp(uint256 timestamp, FrameConfig memory config) internal view returns (ConsensusFrame memory) { - uint256 frameIndex = _computeFrameIndex(timestamp, config); + return _getFrameAtIndex(_computeFrameIndex(timestamp, config), config); + } + + function _getFrameAtIndex(uint256 frameIndex, FrameConfig memory config) + internal view returns (ConsensusFrame memory) + { uint256 frameStartEpoch = _computeStartEpochOfFrameWithIndex(frameIndex, config); uint256 frameStartSlot = _computeStartSlotAtEpoch(frameStartEpoch); uint256 nextFrameStartSlot = frameStartSlot + config.epochsPerFrame * SLOTS_PER_EPOCH; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index dd5abea1a..d55120563 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -98,18 +98,14 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { function initialize( address admin, - address pauser, - address resumer, address consensusContract, uint256 consensusVersion, uint256 lastProcessingRefSlot ) external { if (admin == address(0)) revert AdminCannotBeZero(); _setupRole(DEFAULT_ADMIN_ROLE, admin); - if (pauser != address(0)) _grantRole(PAUSE_ROLE, pauser); - if (resumer != address(0)) _grantRole(RESUME_ROLE, resumer); - _pause(PAUSE_INFINITELY); + _pauseFor(PAUSE_INFINITELY); _initialize(consensusContract, consensusVersion, lastProcessingRefSlot); } @@ -129,8 +125,17 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { /// @dev Reverts with `AccessControl:...` reason if sender has no `PAUSE_ROLE` /// @dev Reverts with `ZeroPauseDuration()` if zero duration is passed /// - function pause(uint256 _duration) external onlyRole(PAUSE_ROLE) { - _pause(_duration); + function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { + _pauseFor(_duration); + } + + /// @notice Pause accepting report data + /// @param _pauseUntilInclusive the last second to pause until + /// @dev Reverts with `ResumeSinceInPast()` if the timestamp is in the past + /// @dev Reverts with `AccessControl:...` reason if sender has no `PAUSE_ROLE` + /// @dev Reverts with `ResumedExpected()` if contract is already paused + function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { + _pauseUntil(_pauseUntilInclusive); } /// diff --git a/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol b/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol index f15e06e5d..f4b40ac52 100644 --- a/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol +++ b/contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol @@ -10,19 +10,28 @@ import {Math256} from "../../common/lib/Math256.sol"; import {AccessControlEnumerable} from "../utils/access/AccessControlEnumerable.sol"; import {PositiveTokenRebaseLimiter, TokenRebaseLimiterData} from "../lib/PositiveTokenRebaseLimiter.sol"; import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; +import {IBurner} from "../../common/interfaces/IBurner.sol"; interface IWithdrawalQueue { - function getWithdrawalRequestStatus(uint256 _requestId) + struct WithdrawalRequestStatus { + /// @notice stETH token amount that was locked on withdrawal queue for this request + uint256 amountOfStETH; + /// @notice amount of stETH shares locked on withdrawal queue for this request + uint256 amountOfShares; + /// @notice address that can claim or transfer this request + address owner; + /// @notice timestamp of when the request was created, in seconds + uint256 timestamp; + /// @notice true, if request is finalized + bool isFinalized; + /// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed) + bool isClaimed; + } + + function getWithdrawalStatus(uint256[] calldata _requestIds) external view - returns ( - uint256 amountOfStETH, - uint256 amountOfShares, - address recipient, - uint256 timestamp, - bool isFinalized, - bool isClaimed - ); + returns (WithdrawalRequestStatus[] memory statuses); } /// @notice The set of restrictions used in the sanity checks of the oracle report @@ -327,12 +336,15 @@ contract OracleReportSanityChecker is AccessControlEnumerable { /// current oracle report /// @param _postCLBalance sum of all Lido validators' balances on the Consensus Layer after the /// current oracle report - /// @param _withdrawalVaultBalance withdrawal vault balance on Execution Layer for report block - /// @param _elRewardsVaultBalance elRewards vault balance on Execution Layer for report block + /// @param _withdrawalVaultBalance withdrawal vault balance on Execution Layer for the report calculation moment + /// @param _elRewardsVaultBalance elRewards vault balance on Execution Layer for the report calculation moment + /// @param _sharesRequestedToBurn shares requested to burn through Burner for the report calculation moment /// @param _etherToLockForWithdrawals ether to lock on withdrawals queue contract + /// @param _newSharesToBurnForWithdrawals new shares to burn due to withdrawal request finalization /// @return withdrawals ETH amount allowed to be taken from the withdrawals vault /// @return elRewards ETH amount allowed to be taken from the EL rewards vault - /// @return sharesToBurnLimit amount allowed to be burnt as part of the current token rebase + /// @return simulatedSharesToBurn simulated amount to be burnt (if no ether locked on withdrawals) + /// @return sharesToBurn amount to be burnt (accounting for withdrawals finalization) function smoothenTokenRebase( uint256 _preTotalPooledEther, uint256 _preTotalShares, @@ -340,8 +352,15 @@ contract OracleReportSanityChecker is AccessControlEnumerable { uint256 _postCLBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, - uint256 _etherToLockForWithdrawals - ) external view returns (uint256 withdrawals, uint256 elRewards, uint256 sharesToBurnLimit) { + uint256 _sharesRequestedToBurn, + uint256 _etherToLockForWithdrawals, + uint256 _newSharesToBurnForWithdrawals + ) external view returns ( + uint256 withdrawals, + uint256 elRewards, + uint256 simulatedSharesToBurn, + uint256 sharesToBurn + ) { TokenRebaseLimiterData memory tokenRebaseLimiter = PositiveTokenRebaseLimiter.initLimiterState( getMaxPositiveTokenRebase(), _preTotalPooledEther, @@ -356,9 +375,10 @@ contract OracleReportSanityChecker is AccessControlEnumerable { withdrawals = tokenRebaseLimiter.consumeLimit(_withdrawalVaultBalance); elRewards = tokenRebaseLimiter.consumeLimit(_elRewardsVaultBalance); - tokenRebaseLimiter.raiseLimit(_etherToLockForWithdrawals); - sharesToBurnLimit = tokenRebaseLimiter.getSharesToBurnLimit(); + simulatedSharesToBurn = Math256.min(tokenRebaseLimiter.getSharesToBurnLimit(), _sharesRequestedToBurn); + tokenRebaseLimiter.raiseLimit(_etherToLockForWithdrawals); + sharesToBurn = Math256.min(tokenRebaseLimiter.getSharesToBurnLimit(), _newSharesToBurnForWithdrawals + _sharesRequestedToBurn); } /// @notice Applies sanity checks to the accounting params of Lido's oracle report @@ -367,8 +387,9 @@ contract OracleReportSanityChecker is AccessControlEnumerable { /// current oracle report (NB: also include the initial balance of newly appeared validators) /// @param _postCLBalance sum of all Lido validators' balances on the Consensus Layer after the /// current oracle report - /// @param _withdrawalVaultBalance withdrawal vault balance on Execution Layer for report block - /// @param _elRewardsVaultBalance el rewards vault balance on Execution Layer for report block + /// @param _withdrawalVaultBalance withdrawal vault balance on Execution Layer for the report reference slot + /// @param _elRewardsVaultBalance el rewards vault balance on Execution Layer for the report reference slot + /// @param _sharesRequestedToBurn shares requested to burn for the report reference slot /// @param _preCLValidators Lido-participating validators on the CL side before the current oracle report /// @param _postCLValidators Lido-participating validators on the CL side after the current oracle report function checkAccountingOracleReport( @@ -377,6 +398,7 @@ contract OracleReportSanityChecker is AccessControlEnumerable { uint256 _postCLBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, uint256 _preCLValidators, uint256 _postCLValidators ) external view { @@ -390,13 +412,16 @@ contract OracleReportSanityChecker is AccessControlEnumerable { // 2. EL rewards vault reported balance _checkELRewardsVaultBalance(elRewardsVault.balance, _elRewardsVaultBalance); - // 3. Consensus Layer one-off balance decrease + // 3. Burn requests + _checkSharesRequestedToBurn(_sharesRequestedToBurn); + + // 4. Consensus Layer one-off balance decrease _checkOneOffCLBalanceDecrease(limitsList, _preCLBalance, _postCLBalance + _withdrawalVaultBalance); - // 4. Consensus Layer annual balances increase + // 5. Consensus Layer annual balances increase _checkAnnualBalancesIncrease(limitsList, _preCLBalance, _postCLBalance, _timeElapsed); - // 5. Appeared validators increase + // 6. Appeared validators increase if (_postCLValidators > _preCLValidators) { _checkValidatorsChurnLimit(limitsList, (_postCLValidators - _preCLValidators), _timeElapsed); } @@ -453,8 +478,7 @@ contract OracleReportSanityChecker is AccessControlEnumerable { } /// @notice Applies sanity checks to the withdrawal requests finalization - /// @param _lastFinalizableRequestId right boundary of requestId range if equals 0, no requests - /// should be finalized + /// @param _lastFinalizableRequestId last finalizable withdrawal request id /// @param _reportTimestamp timestamp when the originated oracle report was submitted function checkWithdrawalQueueOracleReport( uint256 _lastFinalizableRequestId, @@ -466,20 +490,20 @@ contract OracleReportSanityChecker is AccessControlEnumerable { LimitsList memory limitsList = _limits.unpack(); address withdrawalQueue = LIDO_LOCATOR.withdrawalQueue(); - _checkRequestIdToFinalizeUpTo(limitsList, withdrawalQueue, _lastFinalizableRequestId, _reportTimestamp); + _checkLastFinalizableId(limitsList, withdrawalQueue, _lastFinalizableRequestId, _reportTimestamp); } /// @notice Applies sanity checks to the simulated share rate for withdrawal requests finalization /// @param _postTotalPooledEther total pooled ether after report applied /// @param _postTotalShares total shares after report applied /// @param _etherLockedOnWithdrawalQueue ether locked on withdrawal queue for the current oracle report - /// @param _sharesBurntFromWithdrawalQueue shares burnt from withdrawal queue for the current oracle report + /// @param _sharesBurntDueToWithdrawals shares burnt due to withdrawals finalization /// @param _simulatedShareRate share rate provided with the oracle report (simulated via off-chain "eth_call") function checkSimulatedShareRate( uint256 _postTotalPooledEther, uint256 _postTotalShares, uint256 _etherLockedOnWithdrawalQueue, - uint256 _sharesBurntFromWithdrawalQueue, + uint256 _sharesBurntDueToWithdrawals, uint256 _simulatedShareRate ) external view { LimitsList memory limitsList = _limits.unpack(); @@ -490,7 +514,7 @@ contract OracleReportSanityChecker is AccessControlEnumerable { _checkSimulatedShareRate( limitsList, _postTotalPooledEther + _etherLockedOnWithdrawalQueue, - _postTotalShares + _sharesBurntFromWithdrawalQueue, + _postTotalShares + _sharesBurntDueToWithdrawals, _simulatedShareRate ); } @@ -513,6 +537,14 @@ contract OracleReportSanityChecker is AccessControlEnumerable { } } + function _checkSharesRequestedToBurn(uint256 _sharesRequestedToBurn) internal view { + (uint256 coverShares, uint256 nonCoverShares) = IBurner(LIDO_LOCATOR.burner()).getSharesRequestedToBurn(); + uint256 actualSharesToBurn = coverShares + nonCoverShares; + if (_sharesRequestedToBurn > actualSharesToBurn) { + revert IncorrectSharesRequestedToBurn(actualSharesToBurn); + } + } + function _checkOneOffCLBalanceDecrease( LimitsList memory _limitsList, uint256 _preCLBalance, @@ -568,16 +600,19 @@ contract OracleReportSanityChecker is AccessControlEnumerable { if (_appearedValidators > churnLimit) revert IncorrectAppearedValidators(_appearedValidators); } - function _checkRequestIdToFinalizeUpTo( + function _checkLastFinalizableId( LimitsList memory _limitsList, address _withdrawalQueue, - uint256 _requestIdToFinalizeUpTo, + uint256 _lastFinalizableId, uint256 _reportTimestamp ) internal view { - (, , , uint256 requestTimestampToFinalizeUpTo, , ) = IWithdrawalQueue(_withdrawalQueue) - .getWithdrawalRequestStatus(_requestIdToFinalizeUpTo); - if (_reportTimestamp < requestTimestampToFinalizeUpTo + _limitsList.requestTimestampMargin) - revert IncorrectRequestFinalization(requestTimestampToFinalizeUpTo); + uint256[] memory requestIds = new uint256[](1); + requestIds[0] = _lastFinalizableId; + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses = IWithdrawalQueue(_withdrawalQueue) + .getWithdrawalStatus(requestIds); + if (_reportTimestamp < statuses[0].timestamp + _limitsList.requestTimestampMargin) + revert IncorrectRequestFinalization(statuses[0].timestamp); } function _checkSimulatedShareRate( @@ -592,16 +627,23 @@ contract OracleReportSanityChecker is AccessControlEnumerable { if (actualShareRate == 0) { // can't finalize anything if the actual share rate is zero - revert IncorrectSimulatedShareRate(MAX_BASIS_POINTS); + revert ActualShareRateIsZero(); } - uint256 simulatedShareDiff = Math256.abs( - SafeCast.toInt256(_simulatedShareRate) - SafeCast.toInt256(actualShareRate) - ); + if (_simulatedShareRate > actualShareRate) { + // the simulated share rate can't be higher than the actual one + // invariant: rounding only can lower the simulated share rate + revert TooHighSimulatedShareRate(_simulatedShareRate, actualShareRate); + } + + uint256 simulatedShareDiff = actualShareRate - _simulatedShareRate; uint256 simulatedShareDeviation = (MAX_BASIS_POINTS * simulatedShareDiff) / actualShareRate; if (simulatedShareDeviation > _limitsList.simulatedShareRateDeviationBPLimit) { - revert IncorrectSimulatedShareRate(simulatedShareDeviation); + // the simulated share rate can be lower than the actual one due to rounding + // e.g., new user-submitted ether & minted `stETH` + // between an oracle reference slot and an actual accounting report delivery + revert TooLowSimulatedShareRate(_simulatedShareRate, actualShareRate); } } @@ -671,13 +713,16 @@ contract OracleReportSanityChecker is AccessControlEnumerable { error IncorrectLimitValue(uint256 value, uint256 maxAllowedValue); error IncorrectWithdrawalsVaultBalance(uint256 actualWithdrawalVaultBalance); error IncorrectELRewardsVaultBalance(uint256 actualELRewardsVaultBalance); + error IncorrectSharesRequestedToBurn(uint256 actualSharesToBurn); error IncorrectCLBalanceDecrease(uint256 oneOffCLBalanceDecreaseBP); error IncorrectCLBalanceIncrease(uint256 annualBalanceDiff); error IncorrectAppearedValidators(uint256 churnLimit); error IncorrectNumberOfExitRequestsPerReport(uint256 maxRequestsCount); error IncorrectExitedValidators(uint256 churnLimit); error IncorrectRequestFinalization(uint256 requestCreationBlock); - error IncorrectSimulatedShareRate(uint256 simulatedShareDeviation); + error ActualShareRateIsZero(); + error TooHighSimulatedShareRate(uint256 simulatedShareRate, uint256 actualShareRate); + error TooLowSimulatedShareRate(uint256 simulatedShareRate, uint256 actualShareRate); error MaxAccountingExtraDataItemsCountExceeded(uint256 maxItemsCount, uint256 receivedItemsCount); error ExitedValidatorsLimitExceeded(uint256 limitPerDay, uint256 exitedPerDay); error TooManyNodeOpsPerExtraDataItem(uint256 itemIndex, uint256 nodeOpsCount); diff --git a/contracts/0.8.9/test_helpers/AccessControlEnumerableMock.sol b/contracts/0.8.9/test_helpers/AccessControlEnumerableMock.sol new file mode 100644 index 000000000..c8e5b8487 --- /dev/null +++ b/contracts/0.8.9/test_helpers/AccessControlEnumerableMock.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (access/AccessControlEnumerable.sol) +// +// Mock contract for AccessControlEnumerable. Copied from tree: +// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6bd6b76d1156e20e45d1016f355d154141c7e5b9/contracts/mocks/AccessControlEnumerableMock.sol +// +pragma solidity 0.8.9; + +import "../utils/access/AccessControlEnumerable.sol"; + +contract AccessControlEnumerableMock is AccessControlEnumerable { + constructor() { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + } + + function setRoleAdmin(bytes32 roleId, bytes32 adminRoleId) public { + _setRoleAdmin(roleId, adminRoleId); + } + + function senderProtected(bytes32 roleId) public onlyRole(roleId) {} +} diff --git a/contracts/0.8.9/test_helpers/AccountingOracleMock.sol b/contracts/0.8.9/test_helpers/AccountingOracleMock.sol index 8495b3de2..e50c43872 100644 --- a/contracts/0.8.9/test_helpers/AccountingOracleMock.sol +++ b/contracts/0.8.9/test_helpers/AccountingOracleMock.sol @@ -7,22 +7,6 @@ pragma solidity 0.8.9; import {AccountingOracle, ILido} from "../oracle/AccountingOracle.sol"; -// TODO: remove and use ILido -interface ILidoTemporary { - function handleOracleReport( - // CL values - uint256 _clValidators, - uint256 _clBalance, - // EL values - uint256 _withdrawalVaultBalance, - uint256 _elRewardsVaultBalance, - // decision - uint256 _requestIdToFinalizeUpTo, - uint256 _finalizationShareRate - ) external returns (uint256, uint256); -} - - contract AccountingOracleMock { address public immutable LIDO; uint256 public immutable SECONDS_PER_SLOT; @@ -38,29 +22,18 @@ contract AccountingOracleMock { AccountingOracle.ReportData calldata data, uint256 /* contractVersion */ ) external { - // TODO: remove the line below - // solhint-disable-next-line uint256 slotsElapsed = data.refSlot - _lastRefSlot; _lastRefSlot = data.refSlot; - // TODO: update to use the actual signature - // ILido(LIDO).handleOracleReport( - // slotsElapsed * SECONDS_PER_SLOT, - // data.numValidators, - // data.clBalanceGwei * 1e9, - // data.withdrawalVaultBalance, - // data.elRewardsVaultBalance, - // data.lastFinalizableWithdrawalRequestId, - // data.simulatedShareRate, - // data.isBunkerMode - // ); - - ILidoTemporary(LIDO).handleOracleReport( + ILido(LIDO).handleOracleReport( + data.refSlot * SECONDS_PER_SLOT, + slotsElapsed * SECONDS_PER_SLOT, data.numValidators, data.clBalanceGwei * 1e9, data.withdrawalVaultBalance, data.elRewardsVaultBalance, - data.lastFinalizableWithdrawalRequestId, + data.sharesRequestedToBurn, + data.withdrawalFinalizationBatches, data.simulatedShareRate ); } diff --git a/contracts/0.8.9/test_helpers/GenericStub.sol b/contracts/0.8.9/test_helpers/GenericStub.sol index 85d1957c3..2450113a8 100644 --- a/contracts/0.8.9/test_helpers/GenericStub.sol +++ b/contracts/0.8.9/test_helpers/GenericStub.sol @@ -69,35 +69,43 @@ contract GenericStub { } function GenericStub__addStub(MethodStub memory _stub) external { - StubState storage currentState = _getState(_currentStateIndexOneBased - 1); - currentState.stubs.push(); + 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 = currentState.stubs.length - 1; - currentState.stubs[newStubIndex].input = _stub.input; - currentState.stubs[newStubIndex].output = _stub.output; - currentState.stubs[newStubIndex].forwardETH = _stub.forwardETH; - currentState.stubs[newStubIndex].isRevert = _stub.isRevert; - currentState.stubs[newStubIndex].nextStateIndexOneBased = _stub.nextStateIndexOneBased; - - for(uint256 i = 0; i < _stub.logs.length; ++i) { - currentState.stubs[newStubIndex].logs.push(_stub.logs[i]); + 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]); } - currentState.indicesByIdOneBased[stubId] = newStubIndex + 1; + state.indicesByIdOneBased[stubId] = newStubIndex + 1; } function GenericStub__addState() external { _states.push(); - _currentStateIndexOneBased = _states.length; } function GenericStub__setState(uint256 _stateIndex) external { require(_stateIndex != 0, "INVALID_INDEX"); if (_stateIndex > _states.length) { - revert GenericStub__StateIndexOutOfBounds(_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); @@ -108,9 +116,20 @@ contract GenericStub { _currentStateIndexOneBased = stub.nextStateIndexOneBased; } if (stub.isRevert) { - assembly { revert(add(output, 32), outputLength) } + 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) } - assembly { return(add(output, 32), outputLength) } } function _logEvents(Log[] memory _logs) internal { @@ -122,52 +141,84 @@ contract GenericStub { bytes memory data = _logs[i].data; uint256 dataLength = data.length; if (_logs[i].logType == LogType.LOG0) { - assembly { log0(add(data, 32), dataLength) } + assembly { + log0(add(data, 32), dataLength) + } } else if (_logs[i].logType == LogType.LOG1) { - assembly { log1(add(data, 32), dataLength, t1) } + assembly { + log1(add(data, 32), dataLength, t1) + } } else if (_logs[i].logType == LogType.LOG2) { - assembly { log2(add(data, 32), dataLength, t1, t2) } + assembly { + log2(add(data, 32), dataLength, t1, t2) + } } else if (_logs[i].logType == LogType.LOG3) { - assembly { log3(add(data, 32), dataLength, t1, t2, t3) } + 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) } + 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); + 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); + 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]; + 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]; + return + methodStubIndex != 0 + ? currentState.stubs[methodStubIndex - 1] + : currentState.stubs[methodStubWildcardIndex - 1]; } - function _getState(uint256 _stateIndex) internal view returns (StubState storage) { + function _getState( + uint256 _stateIndex + ) internal view returns (StubState storage) { if (_stateIndex >= _states.length) { - revert GenericStub__StateIndexOutOfBounds(_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); -} \ No newline at end of file +} diff --git a/contracts/0.8.9/test_helpers/ModuleSolo.sol b/contracts/0.8.9/test_helpers/ModuleSolo.sol index 3ccdc1b33..4bbe96f06 100644 --- a/contracts/0.8.9/test_helpers/ModuleSolo.sol +++ b/contracts/0.8.9/test_helpers/ModuleSolo.sol @@ -90,6 +90,12 @@ contract ModuleSolo is IStakingModule { function updateRefundedValidatorsCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) external {} + function updateTargetValidatorsLimits( + uint256 _nodeOperatorId, + bool _isTargetLimitActive, + uint256 _targetLimit + ) external {} + function onExitedAndStuckValidatorsCountsUpdated() external {} function unsafeUpdateValidatorsCount( diff --git a/contracts/0.8.9/test_helpers/NFTDescriptorMock.sol b/contracts/0.8.9/test_helpers/NFTDescriptorMock.sol new file mode 100644 index 000000000..eca029393 --- /dev/null +++ b/contracts/0.8.9/test_helpers/NFTDescriptorMock.sol @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity 0.8.9; + +import { Strings } from "@openzeppelin/contracts-v4.4/utils/Strings.sol"; +import { INFTDescriptor } from "../WithdrawalQueueERC721.sol"; + +contract NFTDescriptorMock is INFTDescriptor { + using Strings for uint256; + + bytes32 private BASE_TOKEN_URI; + + constructor(string memory _baseURI) INFTDescriptor() { + BASE_TOKEN_URI = _toBytes32(_baseURI); + } + + function constructTokenURI( + uint256 _requestId + ) external view returns (string memory) { + string memory baseURI = _toString(BASE_TOKEN_URI); + return string(abi.encodePacked(baseURI, _requestId.toString())); + } + + function baseTokenURI() external view returns (string memory) { + return _toString(BASE_TOKEN_URI); + } + + function setBaseTokenURI(string memory _baseURI) external { + BASE_TOKEN_URI = _toBytes32(_baseURI); + } + + function _toBytes32(string memory _str) internal pure returns (bytes32) { + bytes memory bstr = bytes(_str); + require(bstr.length <= 32, "NFTDescriptor: string too long"); + return bytes32(uint256(bytes32(bstr)) | bstr.length); + } + + function _toString(bytes32 _sstr) internal pure returns (string memory) { + uint256 len = uint256(_sstr) & 0xFF; + string memory str = new string(32); + /// @solidity memory-safe-assembly + assembly { + mstore(str, len) + mstore(add(str, 0x20), _sstr) + } + return str; + } +} diff --git a/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol b/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol index 195cdd32f..5556f3339 100644 --- a/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol +++ b/contracts/0.8.9/test_helpers/OracleReportSanityCheckerMocks.sol @@ -18,31 +18,36 @@ contract LidoStub { } } -contract WithdrawalQueueStub { - mapping(uint256 => uint256) private _blockNumbers; +contract WithdrawalQueueStub is IWithdrawalQueue { + mapping(uint256 => uint256) private _timestamps; - function setRequestBlockNumber(uint256 _requestId, uint256 _blockNumber) external { - _blockNumbers[_requestId] = _blockNumber; + function setRequestTimestamp(uint256 _requestId, uint256 _timestamp) external { + _timestamps[_requestId] = _timestamp; } - function getWithdrawalRequestStatus(uint256 _requestId) - external - view - returns ( - uint256, - uint256, - address, - uint256 blockNumber, - bool, - bool - ) - { - blockNumber = _blockNumbers[_requestId]; + function getWithdrawalStatus( + uint256[] calldata _requestIds + ) external view returns ( + WithdrawalRequestStatus[] memory statuses + ) { + statuses = new WithdrawalRequestStatus[](_requestIds.length); + for (uint256 i; i < _requestIds.length; ++i) { + statuses[i].timestamp = _timestamps[_requestIds[i]]; + } + } +} + +contract BurnerStub { + function getSharesRequestedToBurn() external view returns ( + uint256 coverShares, uint256 nonCoverShares + ) { + return (0, 0); } } interface ILidoLocator { function lido() external view returns (address); + function burner() external view returns (address); function withdrawalVault() external view returns (address); function withdrawalQueue() external view returns (address); } @@ -52,17 +57,20 @@ contract LidoLocatorStub is ILidoLocator { address private immutable WITHDRAWAL_VAULT; address private immutable WITHDRAWAL_QUEUE; address private immutable EL_REWARDS_VAULT; + address private immutable BURNER; constructor( address _lido, address _withdrawalVault, address _withdrawalQueue, - address _elRewardsVault + address _elRewardsVault, + address _burner ) { LIDO = _lido; WITHDRAWAL_VAULT = _withdrawalVault; WITHDRAWAL_QUEUE = _withdrawalQueue; EL_REWARDS_VAULT = _elRewardsVault; + BURNER = _burner; } function lido() external view returns (address) { @@ -80,6 +88,10 @@ contract LidoLocatorStub is ILidoLocator { function elRewardsVault() external view returns (address) { return EL_REWARDS_VAULT; } + + function burner() external view returns (address) { + return BURNER; + } } contract OracleReportSanityCheckerStub { @@ -93,13 +105,13 @@ contract OracleReportSanityCheckerStub { uint256 _postCLBalance, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, + uint256 _sharesRequestedToBurn, uint256 _preCLValidators, uint256 _postCLValidators ) external view {} function checkWithdrawalQueueOracleReport( - uint256 _lastFinalizableRequestId, - uint256 _simulatedShareRate, + uint256[] calldata _withdrawalFinalizationBatches, uint256 _reportTimestamp ) external view {} @@ -107,7 +119,7 @@ contract OracleReportSanityCheckerStub { uint256 _postTotalPooledEther, uint256 _postTotalShares, uint256 _etherLockedOnWithdrawalQueue, - uint256 _sharesBurntFromWithdrawalQueue, + uint256 _sharesBurntDueToWithdrawals, uint256 _simulatedShareRate ) external view {} @@ -118,11 +130,20 @@ contract OracleReportSanityCheckerStub { uint256, uint256 _withdrawalVaultBalance, uint256 _elRewardsVaultBalance, - uint256 _etherToLockForWithdrawals - ) external view returns (uint256 withdrawals, uint256 elRewards, uint256 sharesToBurnLimit) { + uint256, + uint256 _etherToLockForWithdrawals, + uint256 + ) external view returns ( + uint256 withdrawals, + uint256 elRewards, + uint256 simulatedSharesToBurn, + uint256 sharesToBurn + ) { withdrawals = _withdrawalVaultBalance; elRewards = _elRewardsVaultBalance; - sharesToBurnLimit = _etherToLockForWithdrawals; + + simulatedSharesToBurn = 0; + sharesToBurn = _etherToLockForWithdrawals; } function checkAccountingExtraDataListItemsCount(uint256 _extraDataListItemsCount) external view {} diff --git a/contracts/0.8.9/test_helpers/PausableUntilPrivateExposed.sol b/contracts/0.8.9/test_helpers/PausableUntilPrivateExposed.sol index acf1d7973..be89de367 100644 --- a/contracts/0.8.9/test_helpers/PausableUntilPrivateExposed.sol +++ b/contracts/0.8.9/test_helpers/PausableUntilPrivateExposed.sol @@ -20,8 +20,11 @@ contract PausableUntilPrivateExposed is PausableUntil { _resume(); } - function pause(uint256 _duration) external { - _pause(_duration); + function pauseFor(uint256 _duration) external { + _pauseFor(_duration); } + function pauseUntil(uint256 _pauseUntilInclusive) external { + _pauseUntil(_pauseUntilInclusive); + } } diff --git a/contracts/0.8.9/test_helpers/StakingModuleMock.sol b/contracts/0.8.9/test_helpers/StakingModuleMock.sol index 00d1227ad..718a12a6e 100644 --- a/contracts/0.8.9/test_helpers/StakingModuleMock.sol +++ b/contracts/0.8.9/test_helpers/StakingModuleMock.sol @@ -4,6 +4,7 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; +import {Math256} from "../../common/lib/Math256.sol"; import {IStakingModule} from "../interfaces/IStakingModule.sol"; contract StakingModuleMock is IStakingModule { @@ -11,6 +12,7 @@ contract StakingModuleMock is IStakingModule { uint256 private _activeValidatorsCount; uint256 private _availableValidatorsCount; uint256 private _nonce; + uint256 private nodeOperatorsCount; function getActiveValidatorsCount() public view returns (uint256) { return _activeValidatorsCount; @@ -32,6 +34,17 @@ contract StakingModuleMock is IStakingModule { depositableValidatorsCount = _availableValidatorsCount; } + struct NodeOperatorSummary { + bool isTargetLimitActive; + uint256 targetValidatorsCount; + uint256 stuckValidatorsCount; + uint256 refundedValidatorsCount; + uint256 stuckPenaltyEndTimestamp; + uint256 totalExitedValidators; + uint256 totalDepositedValidators; + uint256 depositableValidatorsCount; + } + mapping(uint256 => NodeOperatorSummary) internal nodeOperatorsSummary; function getNodeOperatorSummary(uint256 _nodeOperatorId) external view returns ( bool isTargetLimitActive, uint256 targetValidatorsCount, @@ -41,7 +54,28 @@ contract StakingModuleMock is IStakingModule { uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount - ) {} + ) { + NodeOperatorSummary storage _summary = nodeOperatorsSummary[_nodeOperatorId]; + isTargetLimitActive = _summary.isTargetLimitActive; + targetValidatorsCount = _summary.targetValidatorsCount; + stuckValidatorsCount = _summary.stuckValidatorsCount; + refundedValidatorsCount = _summary.refundedValidatorsCount; + stuckPenaltyEndTimestamp = _summary.stuckPenaltyEndTimestamp; + totalExitedValidators = _summary.totalExitedValidators; + totalDepositedValidators = _summary.totalDepositedValidators; + depositableValidatorsCount = _summary.depositableValidatorsCount; + } + function setNodeOperatorSummary(uint256 _nodeOperatorId, NodeOperatorSummary memory _summary) external { + NodeOperatorSummary storage summary = nodeOperatorsSummary[_nodeOperatorId]; + summary.isTargetLimitActive = _summary.isTargetLimitActive; + summary.targetValidatorsCount = _summary.targetValidatorsCount; + summary.stuckValidatorsCount = _summary.stuckValidatorsCount; + summary.refundedValidatorsCount = _summary.refundedValidatorsCount; + summary.stuckPenaltyEndTimestamp = _summary.stuckPenaltyEndTimestamp; + summary.totalExitedValidators = _summary.totalExitedValidators; + summary.totalDepositedValidators = _summary.totalDepositedValidators; + summary.depositableValidatorsCount = _summary.depositableValidatorsCount; + } function getNonce() external view returns (uint256) { return _nonce; @@ -51,7 +85,11 @@ contract StakingModuleMock is IStakingModule { _nonce = _newNonce; } - function getNodeOperatorsCount() external view returns (uint256) {} + function getNodeOperatorsCount() public view returns (uint256) { return nodeOperatorsCount; } + + function testing_setNodeOperatorsCount(uint256 _count) external { + nodeOperatorsCount = _count; + } function getActiveNodeOperatorsCount() external view returns (uint256) {} @@ -60,10 +98,30 @@ contract StakingModuleMock is IStakingModule { function getNodeOperatorIds(uint256 _offset, uint256 _limit) external view - returns (uint256[] memory nodeOperatorIds) {} + returns (uint256[] memory nodeOperatorIds) { + uint256 nodeOperatorsCount = getNodeOperatorsCount(); + if (_offset < nodeOperatorsCount && _limit != 0) { + nodeOperatorIds = new uint256[](Math256.min(_limit, nodeOperatorsCount - _offset)); + for (uint256 i = 0; i < nodeOperatorIds.length; ++i) { + nodeOperatorIds[i] = _offset + i; + } + } - function onRewardsMinted(uint256 _totalShares) external {} + } + /// @dev onRewardsMinted mock + // solhint-disable-next-line + struct Call_onRewardsMinted { + uint256 callCount; + uint256 totalShares; + } + Call_onRewardsMinted public lastCall_onRewardsMinted; + function onRewardsMinted(uint256 _totalShares) external { + lastCall_onRewardsMinted.totalShares += _totalShares; + ++lastCall_onRewardsMinted.callCount; + } + + // solhint-disable-next-line struct Call_updateValidatorsCount { bytes nodeOperatorIds; bytes validatorsCounts; @@ -91,7 +149,38 @@ contract StakingModuleMock is IStakingModule { ++lastCall_updateExitedValidatorsCount.callCount; } - function updateRefundedValidatorsCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) external {} + // solhint-disable-next-line + struct Call_updateRefundedValidatorsCount { + uint256 nodeOperatorId; + uint256 refundedValidatorsCount; + uint256 callCount; + } + Call_updateRefundedValidatorsCount public lastCall_updateRefundedValidatorsCount; + function updateRefundedValidatorsCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) external { + lastCall_updateRefundedValidatorsCount.nodeOperatorId = _nodeOperatorId; + lastCall_updateRefundedValidatorsCount.refundedValidatorsCount = _refundedValidatorsCount; + ++lastCall_updateRefundedValidatorsCount.callCount; + } + + // solhint-disable-next-line + struct Call_updateTargetValidatorsLimits { + uint256 nodeOperatorId; + bool isTargetLimitActive; + uint256 targetLimit; + uint256 callCount; + } + + Call_updateTargetValidatorsLimits public lastCall_updateTargetValidatorsLimits; + function updateTargetValidatorsLimits( + uint256 _nodeOperatorId, + bool _isTargetLimitActive, + uint256 _targetLimit + ) external { + lastCall_updateTargetValidatorsLimits.nodeOperatorId = _nodeOperatorId; + lastCall_updateTargetValidatorsLimits.isTargetLimitActive = _isTargetLimitActive; + lastCall_updateTargetValidatorsLimits.targetLimit = _targetLimit; + ++lastCall_updateTargetValidatorsLimits.callCount; + } uint256 public callCount_onExitedAndStuckValidatorsCountsUpdated; @@ -99,23 +188,39 @@ contract StakingModuleMock is IStakingModule { ++callCount_onExitedAndStuckValidatorsCountsUpdated; } + // solhint-disable-next-line + struct Call_unsafeUpdateValidatorsCount { + uint256 nodeOperatorId; + uint256 exitedValidatorsKeysCount; + uint256 stuckValidatorsKeysCount; + uint256 callCount; + } + Call_unsafeUpdateValidatorsCount public lastCall_unsafeUpdateValidatorsCount; function unsafeUpdateValidatorsCount( - uint256 /* _nodeOperatorId */, - uint256 /* _exitedValidatorsKeysCount */, - uint256 /* _stuckValidatorsKeysCount */ - ) external {} + uint256 _nodeOperatorId, + uint256 _exitedValidatorsKeysCount, + uint256 _stuckValidatorsKeysCount + ) external { + lastCall_unsafeUpdateValidatorsCount.nodeOperatorId = _nodeOperatorId; + lastCall_unsafeUpdateValidatorsCount.exitedValidatorsKeysCount = _exitedValidatorsKeysCount; + lastCall_unsafeUpdateValidatorsCount.stuckValidatorsKeysCount = _stuckValidatorsKeysCount; + ++lastCall_unsafeUpdateValidatorsCount.callCount; + } function onWithdrawalCredentialsChanged() external { _availableValidatorsCount = _activeValidatorsCount; } - function obtainDepositData(uint256 _depositsCount, bytes calldata _calldata) + function obtainDepositData(uint256 _depositsCount, bytes calldata) external returns ( bytes memory publicKeys, bytes memory signatures ) - {} + { + publicKeys = new bytes(48 * _depositsCount); + signatures = new bytes(96 * _depositsCount); + } function setTotalExitedValidatorsCount(uint256 newExitedValidatorsCount) external { _exitedValidatorsCount = newExitedValidatorsCount; diff --git a/contracts/0.8.9/test_helpers/VersionedMock.sol b/contracts/0.8.9/test_helpers/VersionedMock.sol new file mode 100644 index 000000000..93521e7aa --- /dev/null +++ b/contracts/0.8.9/test_helpers/VersionedMock.sol @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import { Versioned } from "../utils/Versioned.sol"; + +contract VersionedMock is Versioned { + constructor() Versioned() {} + + function getPetrifiedVersionMark() external pure returns (uint256) { + return PETRIFIED_VERSION_MARK; + } + + function initializeContractVersionTo(uint256 version) external { + _initializeContractVersionTo(version); + } + + function checkContractVersion(uint256 version) external view { + _checkContractVersion(version); + } + + function updateContractVersion(uint256 newVersion) external { + _updateContractVersion(newVersion); + } +} diff --git a/contracts/0.8.9/test_helpers/WithdrawalQueueERC721Mock.sol b/contracts/0.8.9/test_helpers/WithdrawalQueueERC721Mock.sol new file mode 100644 index 000000000..4fe82c69e --- /dev/null +++ b/contracts/0.8.9/test_helpers/WithdrawalQueueERC721Mock.sol @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity 0.8.9; + +import {WithdrawalQueueERC721} from "../WithdrawalQueueERC721.sol"; + +contract WithdrawalQueueERC721Mock is WithdrawalQueueERC721 { + constructor( + address _wstETH, + string memory _name, + string memory _symbol + ) WithdrawalQueueERC721(_wstETH, _name, _symbol) { + } + + function getQueueItem(uint256 id) external view returns (WithdrawalRequest memory) { + return _getQueue()[id]; + } + + function getCheckpointItem(uint256 id) external view returns (Checkpoint memory) { + return _getCheckpoints()[id]; + } +} diff --git a/contracts/0.8.9/test_helpers/oracle/HashConsensusTimeTravellable.sol b/contracts/0.8.9/test_helpers/oracle/HashConsensusTimeTravellable.sol index 238cc9506..f4807747b 100644 --- a/contracts/0.8.9/test_helpers/oracle/HashConsensusTimeTravellable.sol +++ b/contracts/0.8.9/test_helpers/oracle/HashConsensusTimeTravellable.sol @@ -14,7 +14,6 @@ contract HashConsensusTimeTravellable is HashConsensus { uint256 secondsPerSlot, uint256 genesisTime, uint256 epochsPerFrame, - uint256 startEpoch, uint256 fastLaneLengthSlots, address admin, address reportProcessor @@ -23,7 +22,6 @@ contract HashConsensusTimeTravellable is HashConsensus { secondsPerSlot, genesisTime, epochsPerFrame, - startEpoch, fastLaneLengthSlots, admin, reportProcessor diff --git a/contracts/0.8.9/test_helpers/oracle/MockConsensusContract.sol b/contracts/0.8.9/test_helpers/oracle/MockConsensusContract.sol index 01cb51864..5e8a36916 100644 --- a/contracts/0.8.9/test_helpers/oracle/MockConsensusContract.sol +++ b/contracts/0.8.9/test_helpers/oracle/MockConsensusContract.sol @@ -23,13 +23,16 @@ contract MockConsensusContract is IConsensusContract { uint256 refSlot; uint256 reportProcessingDeadlineSlot; } + struct FrameConfig { uint64 initialEpoch; uint64 epochsPerFrame; uint64 fastLaneLengthSlots; } + FrameConfig internal _frameConfig; ConsensusFrame internal _consensusFrame; + uint256 internal _initialRefSlot; constructor( uint256 slotsPerEpoch, @@ -50,6 +53,8 @@ contract MockConsensusContract is IConsensusContract { _consensusFrame.index = 10; _consensusFrame.refSlot = 1; _consensusFrame.reportProcessingDeadlineSlot = 7001; + + _initialRefSlot = initialEpoch * slotsPerEpoch - 1; } function getIsMember(address addr) external view returns (bool) { @@ -66,6 +71,10 @@ contract MockConsensusContract is IConsensusContract { _consensusFrame.reportProcessingDeadlineSlot = reportProcessingDeadlineSlot; } + function setInitialRefSlot(uint256 initialRefSlot) external { + _initialRefSlot = initialRefSlot; + } + function getChainConfig() external view @@ -78,6 +87,10 @@ contract MockConsensusContract is IConsensusContract { return (_frameConfig.initialEpoch, _frameConfig.epochsPerFrame); } + function getInitialRefSlot() external view returns (uint256) { + return _initialRefSlot; + } + function _setFrameConfig(uint256 initialEpoch, uint256 epochsPerFrame, uint256 fastLaneLengthSlots) internal { _frameConfig = FrameConfig(initialEpoch.toUint64(), epochsPerFrame.toUint64(), fastLaneLengthSlots.toUint64()); } diff --git a/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol b/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol index 0853b7518..c8e2117e9 100644 --- a/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol +++ b/contracts/0.8.9/test_helpers/oracle/MockLidoForAccountingOracle.sol @@ -14,8 +14,9 @@ contract MockLidoForAccountingOracle is ILido { uint256 clBalance; uint256 withdrawalVaultBalance; uint256 elRewardsVaultBalance; - uint256 lastWithdrawalRequestIdToFinalize; - uint256 finalizationShareRate; + uint256 sharesRequestedToBurn; + uint256[] withdrawalFinalizationBatches; + uint256 simulatedShareRate; uint256 callCount; } @@ -36,8 +37,9 @@ contract MockLidoForAccountingOracle is ILido { uint256 clBalance, uint256 withdrawalVaultBalance, uint256 elRewardsVaultBalance, - uint256 lastWithdrawalRequestIdToFinalize, - uint256 finalizationShareRate + uint256 sharesRequestedToBurn, + uint256[] calldata withdrawalFinalizationBatches, + uint256 simulatedShareRate ) external { _handleOracleReportLastCall.currentReportTimestamp = currentReportTimestamp; _handleOracleReportLastCall.secondsElapsedSinceLastReport = secondsElapsedSinceLastReport; @@ -45,8 +47,9 @@ contract MockLidoForAccountingOracle is ILido { _handleOracleReportLastCall.clBalance = clBalance; _handleOracleReportLastCall.withdrawalVaultBalance = withdrawalVaultBalance; _handleOracleReportLastCall.elRewardsVaultBalance = elRewardsVaultBalance; - _handleOracleReportLastCall.lastWithdrawalRequestIdToFinalize = lastWithdrawalRequestIdToFinalize; - _handleOracleReportLastCall.finalizationShareRate = finalizationShareRate; + _handleOracleReportLastCall.sharesRequestedToBurn = sharesRequestedToBurn; + _handleOracleReportLastCall.withdrawalFinalizationBatches = withdrawalFinalizationBatches; + _handleOracleReportLastCall.simulatedShareRate = simulatedShareRate; ++_handleOracleReportLastCall.callCount; } } diff --git a/contracts/0.8.9/test_helpers/oracle/MockWithdrawalQueueForAccoutingOracle.sol b/contracts/0.8.9/test_helpers/oracle/MockWithdrawalQueueForAccoutingOracle.sol index 6526b42db..4142d29b6 100644 --- a/contracts/0.8.9/test_helpers/oracle/MockWithdrawalQueueForAccoutingOracle.sol +++ b/contracts/0.8.9/test_helpers/oracle/MockWithdrawalQueueForAccoutingOracle.sol @@ -5,18 +5,19 @@ pragma solidity 0.8.9; import { IWithdrawalQueue } from "../../oracle/AccountingOracle.sol"; contract MockWithdrawalQueueForAccountingOracle is IWithdrawalQueue { - - struct UpdateBunkerModeCallData { + struct OnOracleReportCallData { bool isBunkerMode; uint256 prevReportTimestamp; + uint256 currentReportTimestamp; uint256 callCount; } - UpdateBunkerModeCallData public lastCall__updateBunkerMode; + OnOracleReportCallData public lastCall__onOracleReport; - function updateBunkerMode(bool isBunkerMode, uint256 prevReportTimestamp) external { - lastCall__updateBunkerMode.isBunkerMode = isBunkerMode; - lastCall__updateBunkerMode.prevReportTimestamp = prevReportTimestamp; - ++lastCall__updateBunkerMode.callCount; + function onOracleReport(bool isBunkerMode, uint256 prevReportTimestamp, uint256 currentReportTimestamp) external { + lastCall__onOracleReport.isBunkerMode = isBunkerMode; + lastCall__onOracleReport.prevReportTimestamp = prevReportTimestamp; + lastCall__onOracleReport.currentReportTimestamp = currentReportTimestamp; + ++lastCall__onOracleReport.callCount; } } diff --git a/contracts/0.8.9/utils/PausableUntil.sol b/contracts/0.8.9/utils/PausableUntil.sol index 0fe8a8356..91e2c4153 100644 --- a/contracts/0.8.9/utils/PausableUntil.sol +++ b/contracts/0.8.9/utils/PausableUntil.sol @@ -13,7 +13,7 @@ contract PausableUntil { /// Special value for the infinite pause uint256 public constant PAUSE_INFINITELY = type(uint256).max; - /// @notice Emitted when paused by the `pause(duration)` call + /// @notice Emitted when paused by the `pauseFor` or `pauseUntil` call event Paused(uint256 duration); /// @notice Emitted when resumed by the `resume` call event Resumed(); @@ -21,21 +21,30 @@ contract PausableUntil { error ZeroPauseDuration(); error PausedExpected(); error ResumedExpected(); + error PauseUntilMustBeInFuture(); /// @notice Reverts when resumed modifier whenPaused() { - if (!isPaused()) { - revert PausedExpected(); - } + _checkPaused(); _; } /// @notice Reverts when paused modifier whenResumed() { + _checkResumed(); + _; + } + + function _checkPaused() internal view { + if (!isPaused()) { + revert PausedExpected(); + } + } + + function _checkResumed() internal view { if (isPaused()) { revert ResumedExpected(); } - _; } /// @notice Returns whether the contract is paused @@ -51,24 +60,44 @@ contract PausableUntil { return RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256(); } - function _resume() internal whenPaused { + function _resume() internal { + _checkPaused(); RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(block.timestamp); - emit Resumed(); } - function _pause(uint256 _duration) internal whenResumed { + function _pauseFor(uint256 _duration) internal { + _checkResumed(); if (_duration == 0) revert ZeroPauseDuration(); - uint256 pausedUntil; + uint256 resumeSince; if (_duration == PAUSE_INFINITELY) { - pausedUntil = PAUSE_INFINITELY; + resumeSince = PAUSE_INFINITELY; } else { - pausedUntil = block.timestamp + _duration; + resumeSince = block.timestamp + _duration; } + _setPausedState(resumeSince); + } + + function _pauseUntil(uint256 _pauseUntilInclusive) internal { + _checkResumed(); + if (_pauseUntilInclusive < block.timestamp) revert PauseUntilMustBeInFuture(); - RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(pausedUntil); + uint256 resumeSince; + if (_pauseUntilInclusive != PAUSE_INFINITELY) { + resumeSince = _pauseUntilInclusive + 1; + } else { + resumeSince = PAUSE_INFINITELY; + } + _setPausedState(resumeSince); + } - emit Paused(_duration); + function _setPausedState(uint256 _resumeSince) internal { + RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(_resumeSince); + if (_resumeSince == PAUSE_INFINITELY) { + emit Paused(PAUSE_INFINITELY); + } else { + emit Paused(_resumeSince - block.timestamp); + } } } diff --git a/contracts/common/interfaces/IBurner.sol b/contracts/common/interfaces/IBurner.sol index 0bd1ff5f8..1e565563a 100644 --- a/contracts/common/interfaces/IBurner.sol +++ b/contracts/common/interfaces/IBurner.sol @@ -11,7 +11,7 @@ interface IBurner { * * NB: The real burn enactment to be invoked after the call (via internal Lido._burnShares()) */ - function commitSharesToBurn(uint256 _stETHSharesToBurnLimit) external returns (uint256 stETHsharesToBurnNow); + function commitSharesToBurn(uint256 _stETHSharesToBurn) external; /** * Request burn shares @@ -21,9 +21,7 @@ interface IBurner { /** * Returns the current amount of shares locked on the contract to be burnt. */ - function getSharesRequestedToBurn() external view returns ( - uint256 coverShares, uint256 nonCoverShares - ); + function getSharesRequestedToBurn() external view returns (uint256 coverShares, uint256 nonCoverShares); /** * Returns the total cover shares ever burnt. diff --git a/contracts/common/lib/SignatureUtils.sol b/contracts/common/lib/SignatureUtils.sol new file mode 100644 index 000000000..4fd309cc4 --- /dev/null +++ b/contracts/common/lib/SignatureUtils.sol @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: MIT + +/* See contracts/COMPILERS.md */ +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.4.24 <0.9.0; + +import {ECDSA} from "./ECDSA.sol"; + + +library SignatureUtils { + /** + * @dev The selector of the ERC1271's `isValidSignature(bytes32 hash, bytes signature)` function, + * serving at the same time as the magic value that the function should return upon success. + * + * See https://eips.ethereum.org/EIPS/eip-1271. + * + * bytes4(keccak256("isValidSignature(bytes32,bytes)") + */ + bytes4 internal constant ERC1271_IS_VALID_SIGNATURE_SELECTOR = 0x1626ba7e; + + /** + * @dev Checks signature validity. + * + * If the signer address doesn't contain any code, assumes that the address is externally owned + * and the signature is a ECDSA signature generated using its private key. Otherwise, issues a + * static call to the signer address to check the signature validity using the ERC-1271 standard. + */ + function isValidSignature( + address signer, + bytes32 msgHash, + uint8 v, + bytes32 r, + bytes32 s + ) internal view returns (bool) { + if (_hasCode(signer)) { + bytes memory sig = abi.encodePacked(r, s, v); + // Solidity <0.5 generates a regular CALL instruction even if the function being called + // is marked as `view`, and the only way to perform a STATICCALL is to use assembly + bytes memory data = abi.encodeWithSelector(ERC1271_IS_VALID_SIGNATURE_SELECTOR, msgHash, sig); + bytes4 retval; + /// @solidity memory-safe-assembly + assembly { + // allocate memory for storing the return value + let outDataOffset := mload(0x40) + mstore(0x40, add(outDataOffset, 32)) + // issue a static call and load the result if the call succeeded + let success := staticcall(gas(), signer, add(data, 32), mload(data), outDataOffset, 32) + if eq(success, 1) { + retval := mload(outDataOffset) + } + } + return retval == ERC1271_IS_VALID_SIGNATURE_SELECTOR; + } else { + return ECDSA.recover(msgHash, v, r, s) == signer; + } + } + + function _hasCode(address addr) internal view returns (bool) { + uint256 size; + /// @solidity memory-safe-assembly + assembly { size := extcodesize(addr) } + return size > 0; + } +} diff --git a/contracts/common/test_helpers/ERC1271MutatingSignerMock.sol b/contracts/common/test_helpers/ERC1271MutatingSignerMock.sol new file mode 100644 index 000000000..81d32685c --- /dev/null +++ b/contracts/common/test_helpers/ERC1271MutatingSignerMock.sol @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + + +contract ERC1271MutatingSignerMock { + uint256 public callCount_isValidSignature; + + function isValidSignature(bytes32 /* hash */, bytes memory /* sig */) external returns (bytes4) { + ++callCount_isValidSignature; + return 0x1626ba7e; + } +} diff --git a/contracts/common/test_helpers/ERC1271PermitSignerMock.sol b/contracts/common/test_helpers/ERC1271PermitSignerMock.sol new file mode 100644 index 000000000..af2999616 --- /dev/null +++ b/contracts/common/test_helpers/ERC1271PermitSignerMock.sol @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + + +contract ERC1271PermitSignerMock { + bytes4 public constant ERC1271_MAGIC_VALUE = 0x1626ba7e; + + function sign(bytes32 hash) public view returns (bytes1 v, bytes32 r, bytes32 s) { + v = 0x42; + r = hash; + s = bytes32(bytes20(address(this))); + } + + function isValidSignature(bytes32 hash, bytes memory sig) external view returns (bytes4) { + (bytes1 v, bytes32 r, bytes32 s) = sign(hash); + bytes memory validSig = abi.encodePacked(r, s, v); + return keccak256(sig) == keccak256(validSig) ? ERC1271_MAGIC_VALUE : bytes4(0); + } +} diff --git a/contracts/common/test_helpers/ERC1271SignerDumbMock.sol b/contracts/common/test_helpers/ERC1271SignerDumbMock.sol new file mode 100644 index 000000000..5b02e2fa8 --- /dev/null +++ b/contracts/common/test_helpers/ERC1271SignerDumbMock.sol @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + + +contract ERC1271SignerDumbMock { + error InvalidSignature(); + + struct Config { + bytes4 retval; + bool reverts; + } + + Config internal _config; + + function configure(Config memory config) external { + _config = config; + } + + function isValidSignature(bytes32 /* hash */, bytes memory /* sig */) external view returns (bytes4) { + if (_config.reverts) revert InvalidSignature(); + return _config.retval; + } +} diff --git a/contracts/common/test_helpers/ERC1271SignerMock.sol b/contracts/common/test_helpers/ERC1271SignerMock.sol new file mode 100644 index 000000000..9e1a5b8ad --- /dev/null +++ b/contracts/common/test_helpers/ERC1271SignerMock.sol @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + + +contract ERC1271SignerMock { + struct Config { + bytes32 validHash; + bytes validSig; + bytes4 retvalOnValid; + bytes4 retvalOnInvalid; + } + + Config internal _config; + + function configure(Config memory config) external { + _config = config; + } + + function isValidSignature(bytes32 hash, bytes memory sig) external view returns (bytes4) { + Config memory cfg = _config; + + return hash == cfg.validHash && keccak256(sig) == keccak256(cfg.validSig) + ? cfg.retvalOnValid + : cfg.retvalOnInvalid; + } + +} diff --git a/contracts/common/test_helpers/SignatureUtilsConsumer_0_4_24.sol b/contracts/common/test_helpers/SignatureUtilsConsumer_0_4_24.sol new file mode 100644 index 000000000..a0bb017e1 --- /dev/null +++ b/contracts/common/test_helpers/SignatureUtilsConsumer_0_4_24.sol @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.4.24; + +import {SignatureUtils} from "../lib/SignatureUtils.sol"; + + +contract SignatureUtilsConsumer_0_4_24 { + + function isValidSignature( + address signer, + bytes32 msgHash, + uint8 v, + bytes32 r, + bytes32 s + ) external view returns (bool) { + return SignatureUtils.isValidSignature(signer, msgHash, v, r, s); + } + +} diff --git a/contracts/common/test_helpers/SignatureUtilsConsumer_0_8_9.sol b/contracts/common/test_helpers/SignatureUtilsConsumer_0_8_9.sol new file mode 100644 index 000000000..5a47d21fd --- /dev/null +++ b/contracts/common/test_helpers/SignatureUtilsConsumer_0_8_9.sol @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + +import {SignatureUtils} from "../lib/SignatureUtils.sol"; + + +contract SignatureUtilsConsumer_0_8_9 { + + function isValidSignature( + address signer, + bytes32 msgHash, + uint8 v, + bytes32 r, + bytes32 s + ) external view returns (bool) { + return SignatureUtils.isValidSignature(signer, msgHash, v, r, s); + } + +} diff --git a/lib/abi/AccountingOracle.json b/lib/abi/AccountingOracle.json index 6def379da..84fcf5043 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":[],"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":"processingRefSlot","type":"uint256"}],"name":"RefSlotCannotBeLessThanProcessingOne","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":"lastFinalizableWithdrawalRequestId","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":"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 diff --git a/lib/abi/BaseOracle.json b/lib/abi/BaseOracle.json index e667ae05c..bf2b9de68 100644 --- a/lib/abi/BaseOracle.json +++ b/lib/abi/BaseOracle.json @@ -1 +1 @@ -[{"inputs":[],"name":"AddressCannotBeSame","type":"error"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","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":"processingRefSlot","type":"uint256"}],"name":"RefSlotCannotBeLessThanProcessingOne","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":"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":"consensusRefSlot","type":"uint256"},{"internalType":"uint256","name":"dataRefSlot","type":"uint256"}],"name":"UnexpectedRefSlot","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":"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"}],"name":"WarnProcessingMissed","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"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":[{"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":"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":[{"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":[],"name":"AddressCannotBeSame","type":"error"},{"inputs":[],"name":"AddressCannotBeZero","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":"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":"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":"consensusRefSlot","type":"uint256"},{"internalType":"uint256","name":"dataRefSlot","type":"uint256"}],"name":"UnexpectedRefSlot","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":"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"}],"name":"WarnProcessingMissed","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"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":[{"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":"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":[{"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/Burner.json b/lib/abi/Burner.json index 07cd2ab43..79487afc0 100644 --- a/lib/abi/Burner.json +++ b/lib/abi/Burner.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_stETH","type":"address"},{"internalType":"uint256","name":"_totalCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_totalNonCoverSharesBurnt","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AppAuthLidoFailed","type":"error"},{"inputs":[],"name":"DirectETHTransfer","type":"error"},{"inputs":[],"name":"StETHRecoveryWrongFunc","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroBurnAmount","type":"error"},{"inputs":[],"name":"ZeroRecoveryAmount","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"ExcessStETHRecovered","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":"bool","name":"isCover","type":"bool"},{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"StETHBurnRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"StETHBurnt","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RECOVER_ASSETS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_BURN_MY_STETH_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_BURN_SHARES_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_sharesToBurnLimit","type":"uint256"}],"name":"commitSharesToBurn","outputs":[{"internalType":"uint256","name":"sharesToBurnNow","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExcessStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"getSharesRequestedToBurn","outputs":[{"internalType":"uint256","name":"coverShares","type":"uint256"},{"internalType":"uint256","name":"nonCoverShares","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":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverExcessStETH","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":"_stETHAmountToBurn","type":"uint256"}],"name":"requestBurnMyStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETHAmountToBurn","type":"uint256"}],"name":"requestBurnMyStETHForCover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_sharesAmountToBurn","type":"uint256"}],"name":"requestBurnShares","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_sharesAmountToBurn","type":"uint256"}],"name":"requestBurnSharesForCover","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":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_admin","type":"address"},{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_stETH","type":"address"},{"internalType":"uint256","name":"_totalCoverSharesBurnt","type":"uint256"},{"internalType":"uint256","name":"_totalNonCoverSharesBurnt","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AppAuthLidoFailed","type":"error"},{"inputs":[{"internalType":"uint256","name":"requestedAmount","type":"uint256"},{"internalType":"uint256","name":"actualAmount","type":"uint256"}],"name":"BurnAmountExceedsActual","type":"error"},{"inputs":[],"name":"DirectETHTransfer","type":"error"},{"inputs":[],"name":"StETHRecoveryWrongFunc","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ZeroAddress","type":"error"},{"inputs":[],"name":"ZeroBurnAmount","type":"error"},{"inputs":[],"name":"ZeroRecoveryAmount","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ERC20Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ERC721Recovered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"ExcessStETHRecovered","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":"bool","name":"isCover","type":"bool"},{"indexed":true,"internalType":"address","name":"requestedBy","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"StETHBurnRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bool","name":"isCover","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"StETHBurnt","type":"event"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RECOVER_ASSETS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_BURN_MY_STETH_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"REQUEST_BURN_SHARES_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TREASURY","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_sharesToBurn","type":"uint256"}],"name":"commitSharesToBurn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExcessStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonCoverSharesBurnt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"getSharesRequestedToBurn","outputs":[{"internalType":"uint256","name":"coverShares","type":"uint256"},{"internalType":"uint256","name":"nonCoverShares","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":"_token","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"recoverERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_tokenId","type":"uint256"}],"name":"recoverERC721","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoverExcessStETH","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":"_stETHAmountToBurn","type":"uint256"}],"name":"requestBurnMyStETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_stETHAmountToBurn","type":"uint256"}],"name":"requestBurnMyStETHForCover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_sharesAmountToBurn","type":"uint256"}],"name":"requestBurnShares","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_sharesAmountToBurn","type":"uint256"}],"name":"requestBurnSharesForCover","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":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file diff --git a/lib/abi/DepositSecurityModule.json b/lib/abi/DepositSecurityModule.json index c4a2f7758..c443f1ea6 100644 --- a/lib/abi/DepositSecurityModule.json +++ b/lib/abi/DepositSecurityModule.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_depositContract","type":"address"},{"internalType":"address","name":"_stakingRouter","type":"address"},{"internalType":"uint256","name":"_maxDepositsPerBlock","type":"uint256"},{"internalType":"uint256","name":"_minDepositBlockDistance","type":"uint256"},{"internalType":"uint256","name":"_pauseIntentValidityPeriodBlocks","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"DepositInactiveModule","type":"error"},{"inputs":[],"name":"DepositNoQuorum","type":"error"},{"inputs":[],"name":"DepositNonceChanged","type":"error"},{"inputs":[],"name":"DepositRootChanged","type":"error"},{"inputs":[],"name":"DepositTooFrequent","type":"error"},{"inputs":[],"name":"DepositUnexpectedBlockHash","type":"error"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"DuplicateAddress","type":"error"},{"inputs":[],"name":"InvalidSignature","type":"error"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"NotAGuardian","type":"error"},{"inputs":[{"internalType":"address","name":"caller","type":"address"}],"name":"NotAnOwner","type":"error"},{"inputs":[],"name":"PauseIntentExpired","type":"error"},{"inputs":[],"name":"SignatureNotSorted","type":"error"},{"inputs":[],"name":"StakingModuleIdTooLarge","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ZeroAddress","type":"error"},{"inputs":[{"internalType":"string","name":"parameter","type":"string"}],"name":"ZeroParameter","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"guardian","type":"address"},{"indexed":true,"internalType":"uint24","name":"stakingModuleId","type":"uint24"}],"name":"DepositsPaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint24","name":"stakingModuleId","type":"uint24"}],"name":"DepositsUnpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"guardian","type":"address"}],"name":"GuardianAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"GuardianQuorumChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"guardian","type":"address"}],"name":"GuardianRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"MaxDepositsChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"MinDepositBlockDistanceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"newValue","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"PauseIntentValidityPeriodBlocksChanged","type":"event"},{"inputs":[],"name":"ATTEST_MESSAGE_PREFIX","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":"LIDO","outputs":[{"internalType":"contract ILido","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_MESSAGE_PREFIX","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_ROUTER","outputs":[{"internalType":"contract IStakingRouter","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"addGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"addGuardians","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"}],"name":"canDeposit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"internalType":"bytes32","name":"depositRoot","type":"bytes32"},{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"depositCalldata","type":"bytes"},{"components":[{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"vs","type":"bytes32"}],"internalType":"struct DepositSecurityModule.Signature[]","name":"sortedGuardianSignatures","type":"tuple[]"}],"name":"depositBufferedEther","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getGuardianIndex","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getGuardianQuorum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getGuardians","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaxDeposits","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMinDepositBlockDistance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getPauseIntentValidityPeriodBlocks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"isGuardian","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"components":[{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"vs","type":"bytes32"}],"internalType":"struct DepositSecurityModule.Signature","name":"sig","type":"tuple"}],"name":"pauseDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"removeGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setGuardianQuorum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setMaxDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setMinDepositBlockDistance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newValue","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setPauseIntentValidityPeriodBlocks","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"}],"name":"unpauseDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_lido","type":"address"},{"internalType":"address","name":"_depositContract","type":"address"},{"internalType":"address","name":"_stakingRouter","type":"address"},{"internalType":"uint256","name":"_maxDepositsPerBlock","type":"uint256"},{"internalType":"uint256","name":"_minDepositBlockDistance","type":"uint256"},{"internalType":"uint256","name":"_pauseIntentValidityPeriodBlocks","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"DepositInactiveModule","type":"error"},{"inputs":[],"name":"DepositNoQuorum","type":"error"},{"inputs":[],"name":"DepositNonceChanged","type":"error"},{"inputs":[],"name":"DepositRootChanged","type":"error"},{"inputs":[],"name":"DepositTooFrequent","type":"error"},{"inputs":[],"name":"DepositUnexpectedBlockHash","type":"error"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"DuplicateAddress","type":"error"},{"inputs":[],"name":"InvalidSignature","type":"error"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"NotAGuardian","type":"error"},{"inputs":[{"internalType":"address","name":"caller","type":"address"}],"name":"NotAnOwner","type":"error"},{"inputs":[],"name":"PauseIntentExpired","type":"error"},{"inputs":[],"name":"SignatureNotSorted","type":"error"},{"inputs":[{"internalType":"string","name":"field","type":"string"}],"name":"ZeroAddress","type":"error"},{"inputs":[{"internalType":"string","name":"parameter","type":"string"}],"name":"ZeroParameter","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"guardian","type":"address"},{"indexed":true,"internalType":"uint24","name":"stakingModuleId","type":"uint24"}],"name":"DepositsPaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint24","name":"stakingModuleId","type":"uint24"}],"name":"DepositsUnpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"guardian","type":"address"}],"name":"GuardianAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"GuardianQuorumChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"guardian","type":"address"}],"name":"GuardianRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"MaxDepositsChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"MinDepositBlockDistanceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"newValue","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"PauseIntentValidityPeriodBlocksChanged","type":"event"},{"inputs":[],"name":"ATTEST_MESSAGE_PREFIX","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":"LIDO","outputs":[{"internalType":"contract ILido","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_MESSAGE_PREFIX","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STAKING_ROUTER","outputs":[{"internalType":"contract IStakingRouter","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"addGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"addGuardians","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"}],"name":"canDeposit","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"internalType":"bytes32","name":"depositRoot","type":"bytes32"},{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"depositCalldata","type":"bytes"},{"components":[{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"vs","type":"bytes32"}],"internalType":"struct DepositSecurityModule.Signature[]","name":"sortedGuardianSignatures","type":"tuple[]"}],"name":"depositBufferedEther","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getGuardianIndex","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getGuardianQuorum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getGuardians","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMaxDeposits","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMinDepositBlockDistance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getPauseIntentValidityPeriodBlocks","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"isGuardian","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"uint256","name":"stakingModuleId","type":"uint256"},{"components":[{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"vs","type":"bytes32"}],"internalType":"struct DepositSecurityModule.Signature","name":"sig","type":"tuple"}],"name":"pauseDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"removeGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setGuardianQuorum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setMaxDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setMinDepositBlockDistance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newValue","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newValue","type":"uint256"}],"name":"setPauseIntentValidityPeriodBlocks","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"stakingModuleId","type":"uint256"}],"name":"unpauseDeposits","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/lib/abi/HashConsensus.json b/lib/abi/HashConsensus.json index 9704d5c43..d286ffdf6 100644 --- a/lib/abi/HashConsensus.json +++ b/lib/abi/HashConsensus.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"uint256","name":"slotsPerEpoch","type":"uint256"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"initialEpoch","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"},{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"reportProcessor","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"AdminCannotBeZero","type":"error"},{"inputs":[],"name":"ConsensusReportAlreadyProcessing","type":"error"},{"inputs":[],"name":"DuplicateMember","type":"error"},{"inputs":[],"name":"DuplicateReport","type":"error"},{"inputs":[],"name":"EmptyReport","type":"error"},{"inputs":[],"name":"EpochsPerFrameCannotBeZero","type":"error"},{"inputs":[],"name":"FastLanePeriodCannotBeLongerThanFrame","type":"error"},{"inputs":[],"name":"InitialEpochIsYetToArrive","type":"error"},{"inputs":[],"name":"InvalidSlot","type":"error"},{"inputs":[],"name":"NewProcessorCannotBeTheSame","type":"error"},{"inputs":[],"name":"NonFastLaneMemberCannotReportWithinFastLaneInterval","type":"error"},{"inputs":[],"name":"NonMember","type":"error"},{"inputs":[],"name":"NumericOverflow","type":"error"},{"inputs":[{"internalType":"uint256","name":"minQuorum","type":"uint256"},{"internalType":"uint256","name":"receivedQuorum","type":"uint256"}],"name":"QuorumTooSmall","type":"error"},{"inputs":[],"name":"ReportProcessorCannotBeZero","type":"error"},{"inputs":[],"name":"StaleReport","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedConsensusVersion","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"report","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"support","type":"uint256"}],"name":"ConsensusReached","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"name":"FastLaneConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newInitialEpoch","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newEpochsPerFrame","type":"uint256"}],"name":"FrameConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"uint256","name":"newTotalMembers","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"MemberAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"uint256","name":"newTotalMembers","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"MemberRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newQuorum","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"totalMembers","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"prevQuorum","type":"uint256"}],"name":"QuorumSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"processor","type":"address"},{"indexed":true,"internalType":"address","name":"prevProcessor","type":"address"}],"name":"ReportProcessorSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":true,"internalType":"address","name":"member","type":"address"},{"indexed":false,"internalType":"bytes32","name":"report","type":"bytes32"}],"name":"ReportReceived","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"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DISABLE_CONSENSUS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_FAST_LANE_CONFIG_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_FRAME_CONFIG_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_MEMBERS_AND_QUORUM_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_REPORT_PROCESSOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"quorum","type":"uint256"}],"name":"addMember","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disableConsensus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getChainConfig","outputs":[{"internalType":"uint256","name":"slotsPerEpoch","type":"uint256"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusState","outputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"bytes32","name":"consensusReport","type":"bytes32"},{"internalType":"bool","name":"isReportProcessing","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getConsensusStateForMember","outputs":[{"components":[{"internalType":"uint256","name":"currentFrameRefSlot","type":"uint256"},{"internalType":"bytes32","name":"currentFrameConsensusReport","type":"bytes32"},{"internalType":"bool","name":"isMember","type":"bool"},{"internalType":"bool","name":"isFastLane","type":"bool"},{"internalType":"bool","name":"canReport","type":"bool"},{"internalType":"uint256","name":"lastMemberReportRefSlot","type":"uint256"},{"internalType":"bytes32","name":"currentFrameMemberReport","type":"bytes32"}],"internalType":"struct HashConsensus.MemberConsensusState","name":"result","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentFrame","outputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"reportProcessingDeadlineSlot","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFastLaneMembers","outputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"uint256[]","name":"lastReportedRefSlots","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFrameConfig","outputs":[{"internalType":"uint256","name":"initialEpoch","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getIsFastLaneMember","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getIsMember","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMembers","outputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"uint256[]","name":"lastReportedRefSlots","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getQuorum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getReportVariants","outputs":[{"internalType":"bytes32[]","name":"variants","type":"bytes32[]"},{"internalType":"uint256[]","name":"support","type":"uint256[]"}],"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":"addr","type":"address"},{"internalType":"uint256","name":"quorum","type":"uint256"}],"name":"removeMember","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":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"name":"setFastLaneLengthSlots","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"name":"setFrameConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"quorum","type":"uint256"}],"name":"setQuorum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newProcessor","type":"address"}],"name":"setReportProcessor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"slot","type":"uint256"},{"internalType":"bytes32","name":"report","type":"bytes32"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"}],"name":"submitReport","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":"uint256","name":"slotsPerEpoch","type":"uint256"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"},{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"reportProcessor","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"AdminCannotBeZero","type":"error"},{"inputs":[],"name":"ConsensusReportAlreadyProcessing","type":"error"},{"inputs":[],"name":"DuplicateMember","type":"error"},{"inputs":[],"name":"DuplicateReport","type":"error"},{"inputs":[],"name":"EmptyReport","type":"error"},{"inputs":[],"name":"EpochsPerFrameCannotBeZero","type":"error"},{"inputs":[],"name":"FastLanePeriodCannotBeLongerThanFrame","type":"error"},{"inputs":[],"name":"InitialEpochAlreadyArrived","type":"error"},{"inputs":[],"name":"InitialEpochIsYetToArrive","type":"error"},{"inputs":[],"name":"InitialEpochRefSlotCannotBeEarlierThanProcessingSlot","type":"error"},{"inputs":[],"name":"InvalidSlot","type":"error"},{"inputs":[],"name":"NewProcessorCannotBeTheSame","type":"error"},{"inputs":[],"name":"NonFastLaneMemberCannotReportWithinFastLaneInterval","type":"error"},{"inputs":[],"name":"NonMember","type":"error"},{"inputs":[],"name":"NumericOverflow","type":"error"},{"inputs":[{"internalType":"uint256","name":"minQuorum","type":"uint256"},{"internalType":"uint256","name":"receivedQuorum","type":"uint256"}],"name":"QuorumTooSmall","type":"error"},{"inputs":[],"name":"ReportProcessorCannotBeZero","type":"error"},{"inputs":[],"name":"StaleReport","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedConsensusVersion","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"report","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"support","type":"uint256"}],"name":"ConsensusReached","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"name":"FastLaneConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newInitialEpoch","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newEpochsPerFrame","type":"uint256"}],"name":"FrameConfigSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"uint256","name":"newTotalMembers","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"MemberAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"uint256","name":"newTotalMembers","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"newQuorum","type":"uint256"}],"name":"MemberRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"newQuorum","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"totalMembers","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"prevQuorum","type":"uint256"}],"name":"QuorumSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"processor","type":"address"},{"indexed":true,"internalType":"address","name":"prevProcessor","type":"address"}],"name":"ReportProcessorSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":true,"internalType":"address","name":"member","type":"address"},{"indexed":false,"internalType":"bytes32","name":"report","type":"bytes32"}],"name":"ReportReceived","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"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DISABLE_CONSENSUS_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_FAST_LANE_CONFIG_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_FRAME_CONFIG_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_MEMBERS_AND_QUORUM_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_REPORT_PROCESSOR_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"quorum","type":"uint256"}],"name":"addMember","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"disableConsensus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getChainConfig","outputs":[{"internalType":"uint256","name":"slotsPerEpoch","type":"uint256"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getConsensusState","outputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"bytes32","name":"consensusReport","type":"bytes32"},{"internalType":"bool","name":"isReportProcessing","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getConsensusStateForMember","outputs":[{"components":[{"internalType":"uint256","name":"currentFrameRefSlot","type":"uint256"},{"internalType":"bytes32","name":"currentFrameConsensusReport","type":"bytes32"},{"internalType":"bool","name":"isMember","type":"bool"},{"internalType":"bool","name":"isFastLane","type":"bool"},{"internalType":"bool","name":"canReport","type":"bool"},{"internalType":"uint256","name":"lastMemberReportRefSlot","type":"uint256"},{"internalType":"bytes32","name":"currentFrameMemberReport","type":"bytes32"}],"internalType":"struct HashConsensus.MemberConsensusState","name":"result","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentFrame","outputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"reportProcessingDeadlineSlot","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFastLaneMembers","outputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"uint256[]","name":"lastReportedRefSlots","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFrameConfig","outputs":[{"internalType":"uint256","name":"initialEpoch","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getInitialRefSlot","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getIsFastLaneMember","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getIsMember","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getMembers","outputs":[{"internalType":"address[]","name":"addresses","type":"address[]"},{"internalType":"uint256[]","name":"lastReportedRefSlots","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getQuorum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getReportVariants","outputs":[{"internalType":"bytes32[]","name":"variants","type":"bytes32[]"},{"internalType":"uint256[]","name":"support","type":"uint256[]"}],"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":"addr","type":"address"},{"internalType":"uint256","name":"quorum","type":"uint256"}],"name":"removeMember","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":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"name":"setFastLaneLengthSlots","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"},{"internalType":"uint256","name":"fastLaneLengthSlots","type":"uint256"}],"name":"setFrameConfig","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"quorum","type":"uint256"}],"name":"setQuorum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newProcessor","type":"address"}],"name":"setReportProcessor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"slot","type":"uint256"},{"internalType":"bytes32","name":"report","type":"bytes32"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"}],"name":"submitReport","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":"initialEpoch","type":"uint256"}],"name":"updateInitialEpoch","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/lib/abi/IConsensusContract.json b/lib/abi/IConsensusContract.json index fc4dd8feb..f579c86d0 100644 --- a/lib/abi/IConsensusContract.json +++ b/lib/abi/IConsensusContract.json @@ -1 +1 @@ -[{"inputs":[],"name":"getChainConfig","outputs":[{"internalType":"uint256","name":"slotsPerEpoch","type":"uint256"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentFrame","outputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"reportProcessingDeadlineSlot","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFrameConfig","outputs":[{"internalType":"uint256","name":"initialEpoch","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getIsMember","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[],"name":"getChainConfig","outputs":[{"internalType":"uint256","name":"slotsPerEpoch","type":"uint256"},{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentFrame","outputs":[{"internalType":"uint256","name":"refSlot","type":"uint256"},{"internalType":"uint256","name":"reportProcessingDeadlineSlot","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFrameConfig","outputs":[{"internalType":"uint256","name":"initialEpoch","type":"uint256"},{"internalType":"uint256","name":"epochsPerFrame","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getInitialRefSlot","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getIsMember","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/ILido.json b/lib/abi/ILido.json index e79a4bf07..f79f48b65 100644 --- a/lib/abi/ILido.json +++ b/lib/abi/ILido.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"uint256","name":"_currentReportTimestamp","type":"uint256"},{"internalType":"uint256","name":"_timeElapsedSeconds","type":"uint256"},{"internalType":"uint256","name":"_clValidators","type":"uint256"},{"internalType":"uint256","name":"_clBalance","type":"uint256"},{"internalType":"uint256","name":"_withdrawalVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_elRewardsVaultBalance","type":"uint256"},{"internalType":"uint256","name":"_lastFinalizableRequestId","type":"uint256"},{"internalType":"uint256","name":"_simulatedShareRate","type":"uint256"}],"name":"handleOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"uint256","name":"_currentReportTimestamp","type":"uint256"},{"internalType":"uint256","name":"_timeElapsedSeconds","type":"uint256"},{"internalType":"uint256","name":"_clValidators","type":"uint256"},{"internalType":"uint256","name":"_clBalance","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"}],"name":"handleOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/lib/abi/IWithdrawalQueue.json b/lib/abi/IWithdrawalQueue.json index b70a929f2..4c65f3257 100644 --- a/lib/abi/IWithdrawalQueue.json +++ b/lib/abi/IWithdrawalQueue.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"getWithdrawalRequestStatus","outputs":[{"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"internalType":"uint256","name":"amountOfShares","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bool","name":"isFinalized","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"getWithdrawalStatus","outputs":[{"components":[{"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"internalType":"uint256","name":"amountOfShares","type":"uint256"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bool","name":"isFinalized","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"internalType":"struct IWithdrawalQueue.WithdrawalRequestStatus[]","name":"statuses","type":"tuple[]"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/LegacyOracle.json b/lib/abi/LegacyOracle.json index f43a2461f..6fd74b812 100644 --- a/lib/abi/LegacyOracle.json +++ b/lib/abi/LegacyOracle.json @@ -1 +1 @@ -[{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lidoLocator","type":"address"},{"name":"_accountingOracleConsensusContract","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_accountingOracle","type":"address"}],"name":"finalizeUpgrade_v4","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedReportDelta","outputs":[{"name":"postTotalPooledEther","type":"uint256"},{"name":"preTotalPooledEther","type":"uint256"},{"name":"timeElapsed","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLido","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentFrame","outputs":[{"name":"frameEpochId","type":"uint256"},{"name":"frameStartTime","type":"uint256"},{"name":"frameEndTime","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"","type":"uint256"},{"name":"timeElapsed","type":"uint256"},{"name":"","type":"uint256"},{"name":"preTotalEther","type":"uint256"},{"name":"postTotalShares","type":"uint256"},{"name":"postTotalEther","type":"uint256"},{"name":"","type":"uint256"}],"name":"handlePostTokenRebase","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getContractVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_refSlot","type":"uint256"},{"name":"_clBalance","type":"uint256"},{"name":"_clValidators","type":"uint256"}],"name":"handleConsensusLayerReport","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getAccountingOracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentEpochId","outputs":[{"name":"epochId","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconSpec","outputs":[{"name":"epochsPerFrame","type":"uint64"},{"name":"slotsPerEpoch","type":"uint64"},{"name":"secondsPerSlot","type":"uint64"},{"name":"genesisTime","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"},{"indexed":false,"name":"beaconBalance","type":"uint128"},{"indexed":false,"name":"beaconValidators","type":"uint128"}],"name":"Completed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"postTotalPooledEther","type":"uint256"},{"indexed":false,"name":"preTotalPooledEther","type":"uint256"},{"indexed":false,"name":"timeElapsed","type":"uint256"},{"indexed":false,"name":"totalShares","type":"uint256"}],"name":"PostTotalShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"}] \ No newline at end of file +[{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lidoLocator","type":"address"},{"name":"_accountingOracleConsensusContract","type":"address"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_accountingOracle","type":"address"}],"name":"finalizeUpgrade_v4","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedReportDelta","outputs":[{"name":"postTotalPooledEther","type":"uint256"},{"name":"preTotalPooledEther","type":"uint256"},{"name":"timeElapsed","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLido","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentFrame","outputs":[{"name":"frameEpochId","type":"uint256"},{"name":"frameStartTime","type":"uint256"},{"name":"frameEndTime","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"","type":"uint256"},{"name":"timeElapsed","type":"uint256"},{"name":"","type":"uint256"},{"name":"preTotalEther","type":"uint256"},{"name":"postTotalShares","type":"uint256"},{"name":"postTotalEther","type":"uint256"},{"name":"","type":"uint256"}],"name":"handlePostTokenRebase","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getLastCompletedEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getContractVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_refSlot","type":"uint256"},{"name":"_clBalance","type":"uint256"},{"name":"_clValidators","type":"uint256"}],"name":"handleConsensusLayerReport","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getAccountingOracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentEpochId","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconSpec","outputs":[{"name":"epochsPerFrame","type":"uint64"},{"name":"slotsPerEpoch","type":"uint64"},{"name":"secondsPerSlot","type":"uint64"},{"name":"genesisTime","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"epochId","type":"uint256"},{"indexed":false,"name":"beaconBalance","type":"uint128"},{"indexed":false,"name":"beaconValidators","type":"uint128"}],"name":"Completed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"postTotalPooledEther","type":"uint256"},{"indexed":false,"name":"preTotalPooledEther","type":"uint256"},{"indexed":false,"name":"timeElapsed","type":"uint256"},{"indexed":false,"name":"totalShares","type":"uint256"}],"name":"PostTotalShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"}] \ No newline at end of file diff --git a/lib/abi/Lido.json b/lib/abi/Lido.json index 58ee3388a..c28e25b8b 100644 --- a/lib/abi/Lido.json +++ b/lib/abi/Lido.json @@ -1 +1 @@ -[{"constant":false,"inputs":[],"name":"resume","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_CONTROL_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_ethAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStakingPaused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"TOTAL_BASIS_POINTS","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_maxStakeLimit","type":"uint256"},{"name":"_stakeLimitIncreasePerBlock","type":"uint256"}],"name":"setStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"RESUME_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lidoLocator","type":"address"},{"name":"_eip712StETH","type":"address"}],"name":"finalizeUpgrade_v2","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalPooledEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newDepositedValidators","type":"uint256"}],"name":"unsafeChangeDepositedValidators","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTreasury","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBufferedEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lidoLocator","type":"address"},{"name":"_eip712StETH","type":"address"}],"name":"initialize","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[],"name":"receiveELRewards","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"getWithdrawalCredentials","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentStakeLimit","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getStakeLimitFullInfo","outputs":[{"name":"isStakingPaused","type":"bool"},{"name":"isStakingLimitSet","type":"bool"},{"name":"currentStakeLimit","type":"uint256"},{"name":"maxStakeLimit","type":"uint256"},{"name":"maxStakeLimitGrowthBlocks","type":"uint256"},{"name":"prevStakeLimit","type":"uint256"},{"name":"prevStakeBlockNumber","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"transferSharesFrom","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"resumeStaking","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getFeeDistribution","outputs":[{"name":"treasuryFeeBasisPoints","type":"uint16"},{"name":"insuranceFeeBasisPoints","type":"uint16"},{"name":"operatorsFeeBasisPoints","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"receiveWithdrawals","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"nonces","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"eip712Domain","outputs":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getContractVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"transferShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getEIP712StETH","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_referral","type":"address"}],"name":"submit","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_maxDepositsCount","type":"uint256"},{"name":"_stakingModuleId","type":"uint256"},{"name":"_depositCalldata","type":"bytes"}],"name":"deposit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconStat","outputs":[{"name":"depositedValidators","type":"uint256"},{"name":"beaconValidators","type":"uint256"},{"name":"beaconBalance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_reportTimestamp","type":"uint256"},{"name":"_timeElapsed","type":"uint256"},{"name":"_clValidators","type":"uint256"},{"name":"_clBalance","type":"uint256"},{"name":"_withdrawalVaultBalance","type":"uint256"},{"name":"_elRewardsVaultBalance","type":"uint256"},{"name":"_lastFinalizableRequestId","type":"uint256"},{"name":"_simulatedShareRate","type":"uint256"}],"name":"handleOracleReport","outputs":[{"name":"postTotalPooledEther","type":"uint256"},{"name":"postTotalShares","type":"uint256"},{"name":"withdrawals","type":"uint256"},{"name":"elRewards","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"removeStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getFee","outputs":[{"name":"totalFee","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_deadline","type":"uint256"},{"name":"_v","type":"uint8"},{"name":"_r","type":"bytes32"},{"name":"_s","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLidoLocator","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"canDeposit","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getDepositableEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pauseStaking","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTotalELRewardsCollected","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[],"name":"StakingPaused","type":"event"},{"anonymous":false,"inputs":[],"name":"StakingResumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"maxStakeLimit","type":"uint256"},{"indexed":false,"name":"stakeLimitIncreasePerBlock","type":"uint256"}],"name":"StakingLimitSet","type":"event"},{"anonymous":false,"inputs":[],"name":"StakingLimitRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"reportTimestamp","type":"uint256"},{"indexed":false,"name":"preCLValidators","type":"uint256"},{"indexed":false,"name":"postCLValidators","type":"uint256"}],"name":"CLValidatorsUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"depositedValidators","type":"uint256"}],"name":"DepositedValidatorsChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"reportTimestamp","type":"uint256"},{"indexed":false,"name":"preCLBalance","type":"uint256"},{"indexed":false,"name":"postCLBalance","type":"uint256"},{"indexed":false,"name":"withdrawalsWithdrawn","type":"uint256"},{"indexed":false,"name":"executionLayerRewardsWithdrawn","type":"uint256"},{"indexed":false,"name":"postBufferedEther","type":"uint256"}],"name":"ETHDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"reportTimestamp","type":"uint256"},{"indexed":false,"name":"timeElapsed","type":"uint256"},{"indexed":false,"name":"preTotalShares","type":"uint256"},{"indexed":false,"name":"preTotalEther","type":"uint256"},{"indexed":false,"name":"postTotalShares","type":"uint256"},{"indexed":false,"name":"postTotalEther","type":"uint256"},{"indexed":false,"name":"sharesMintedAsFees","type":"uint256"}],"name":"TokenRebased","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"lidoLocator","type":"address"}],"name":"LidoLocatorSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"ELRewardsReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"WithdrawalsReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"referral","type":"address"}],"name":"Submitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Unbuffered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"eip712StETH","type":"address"}],"name":"EIP712StETHInitialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"sharesValue","type":"uint256"}],"name":"TransferShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"},{"indexed":false,"name":"preRebaseTokenAmount","type":"uint256"},{"indexed":false,"name":"postRebaseTokenAmount","type":"uint256"},{"indexed":false,"name":"sharesAmount","type":"uint256"}],"name":"SharesBurnt","type":"event"},{"anonymous":false,"inputs":[],"name":"Stopped","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"}] \ No newline at end of file +[{"constant":false,"inputs":[],"name":"resume","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[],"name":"stop","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_CONTROL_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_ethAmount","type":"uint256"}],"name":"getSharesByPooledEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStakingPaused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_maxStakeLimit","type":"uint256"},{"name":"_stakeLimitIncreasePerBlock","type":"uint256"}],"name":"setStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"RESUME_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lidoLocator","type":"address"},{"name":"_eip712StETH","type":"address"}],"name":"finalizeUpgrade_v2","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalPooledEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newDepositedValidators","type":"uint256"}],"name":"unsafeChangeDepositedValidators","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTreasury","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isStopped","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBufferedEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_lidoLocator","type":"address"},{"name":"_eip712StETH","type":"address"}],"name":"initialize","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[],"name":"receiveELRewards","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"getWithdrawalCredentials","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentStakeLimit","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getStakeLimitFullInfo","outputs":[{"name":"isStakingPaused","type":"bool"},{"name":"isStakingLimitSet","type":"bool"},{"name":"currentStakeLimit","type":"uint256"},{"name":"maxStakeLimit","type":"uint256"},{"name":"maxStakeLimitGrowthBlocks","type":"uint256"},{"name":"prevStakeLimit","type":"uint256"},{"name":"prevStakeBlockNumber","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"transferSharesFrom","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"resumeStaking","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getFeeDistribution","outputs":[{"name":"treasuryFeeBasisPoints","type":"uint16"},{"name":"insuranceFeeBasisPoints","type":"uint16"},{"name":"operatorsFeeBasisPoints","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"receiveWithdrawals","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"_sharesAmount","type":"uint256"}],"name":"getPooledEthByShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"nonces","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getOracle","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"eip712Domain","outputs":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getContractVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_sharesAmount","type":"uint256"}],"name":"transferShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[],"name":"getEIP712StETH","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_referral","type":"address"}],"name":"submit","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_maxDepositsCount","type":"uint256"},{"name":"_stakingModuleId","type":"uint256"},{"name":"_depositCalldata","type":"bytes"}],"name":"deposit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getBeaconStat","outputs":[{"name":"depositedValidators","type":"uint256"},{"name":"beaconValidators","type":"uint256"},{"name":"beaconBalance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"removeStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_reportTimestamp","type":"uint256"},{"name":"_timeElapsed","type":"uint256"},{"name":"_clValidators","type":"uint256"},{"name":"_clBalance","type":"uint256"},{"name":"_withdrawalVaultBalance","type":"uint256"},{"name":"_elRewardsVaultBalance","type":"uint256"},{"name":"_sharesRequestedToBurn","type":"uint256"},{"name":"_withdrawalFinalizationBatches","type":"uint256[]"},{"name":"_simulatedShareRate","type":"uint256"}],"name":"handleOracleReport","outputs":[{"name":"postRebaseAmounts","type":"uint256[4]"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getFee","outputs":[{"name":"totalFee","type":"uint16"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTotalShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_deadline","type":"uint256"},{"name":"_v","type":"uint8"},{"name":"_r","type":"bytes32"},{"name":"_s","type":"bytes32"}],"name":"permit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLidoLocator","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"canDeposit","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_PAUSE_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getDepositableEther","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_account","type":"address"}],"name":"sharesOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pauseStaking","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getTotalELRewardsCollected","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[],"name":"StakingPaused","type":"event"},{"anonymous":false,"inputs":[],"name":"StakingResumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"maxStakeLimit","type":"uint256"},{"indexed":false,"name":"stakeLimitIncreasePerBlock","type":"uint256"}],"name":"StakingLimitSet","type":"event"},{"anonymous":false,"inputs":[],"name":"StakingLimitRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"reportTimestamp","type":"uint256"},{"indexed":false,"name":"preCLValidators","type":"uint256"},{"indexed":false,"name":"postCLValidators","type":"uint256"}],"name":"CLValidatorsUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"depositedValidators","type":"uint256"}],"name":"DepositedValidatorsChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"reportTimestamp","type":"uint256"},{"indexed":false,"name":"preCLBalance","type":"uint256"},{"indexed":false,"name":"postCLBalance","type":"uint256"},{"indexed":false,"name":"withdrawalsWithdrawn","type":"uint256"},{"indexed":false,"name":"executionLayerRewardsWithdrawn","type":"uint256"},{"indexed":false,"name":"postBufferedEther","type":"uint256"}],"name":"ETHDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"reportTimestamp","type":"uint256"},{"indexed":false,"name":"timeElapsed","type":"uint256"},{"indexed":false,"name":"preTotalShares","type":"uint256"},{"indexed":false,"name":"preTotalEther","type":"uint256"},{"indexed":false,"name":"postTotalShares","type":"uint256"},{"indexed":false,"name":"postTotalEther","type":"uint256"},{"indexed":false,"name":"sharesMintedAsFees","type":"uint256"}],"name":"TokenRebased","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"lidoLocator","type":"address"}],"name":"LidoLocatorSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"ELRewardsReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"WithdrawalsReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"referral","type":"address"}],"name":"Submitted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Unbuffered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"eip712StETH","type":"address"}],"name":"EIP712StETHInitialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"sharesValue","type":"uint256"}],"name":"TransferShares","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"},{"indexed":false,"name":"preRebaseTokenAmount","type":"uint256"},{"indexed":false,"name":"postRebaseTokenAmount","type":"uint256"},{"indexed":false,"name":"sharesAmount","type":"uint256"}],"name":"SharesBurnt","type":"event"},{"anonymous":false,"inputs":[],"name":"Stopped","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"}] \ No newline at end of file diff --git a/lib/abi/NodeOperatorsRegistry.json b/lib/abi/NodeOperatorsRegistry.json index 6efc49363..846e58f1a 100644 --- a/lib/abi/NodeOperatorsRegistry.json +++ b/lib/abi/NodeOperatorsRegistry.json @@ -1 +1 @@ -[{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_keysCount","type":"uint256"},{"name":"_publicKeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getType","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"clearNodeOperatorPenalty","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_offset","type":"uint256"},{"name":"_limit","type":"uint256"}],"name":"getNodeOperatorIds","outputs":[{"name":"nodeOperatorIds","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_offset","type":"uint256"},{"name":"_limit","type":"uint256"}],"name":"getSigningKeys","outputs":[{"name":"pubkeys","type":"bytes"},{"name":"signatures","type":"bytes"},{"name":"used","type":"bool[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_fromIndex","type":"uint256"},{"name":"_keysCount","type":"uint256"}],"name":"removeSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getNodeOperatorIsActive","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_name","type":"string"}],"name":"setNodeOperatorName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_totalRewardShares","type":"uint256"}],"name":"getRewardsDistribution","outputs":[{"name":"recipients","type":"address[]"},{"name":"shares","type":"uint256[]"},{"name":"penalized","type":"bool[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_indexFrom","type":"uint256"},{"name":"_indexTo","type":"uint256"}],"name":"invalidateReadyToDepositKeysRange","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_locator","type":"address"},{"name":"_type","type":"bytes32"},{"name":"_stuckPenaltyDelay","type":"uint256"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_isTargetLimitActive","type":"bool"},{"name":"_targetLimit","type":"uint64"}],"name":"updateTargetValidatorsLimits","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_delay","type":"uint256"}],"name":"setStuckPenaltyDelay","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getStuckPenaltyDelay","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_fromIndex","type":"uint256"},{"name":"_keysCount","type":"uint256"}],"name":"removeSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"isOperatorPenalized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"deactivateNodeOperator","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_ROUTER_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_keysCount","type":"uint256"},{"name":"_publicKeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getActiveNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_rewardAddress","type":"address"}],"name":"addNodeOperator","outputs":[{"name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getContractVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getUnusedSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"onRewardsMinted","outputs":[],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_NODE_OPERATOR_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"onWithdrawalCredentialsChanged","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"activateNodeOperator","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_rewardAddress","type":"address"}],"name":"setNodeOperatorRewardAddress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_fullInfo","type":"bool"}],"name":"getNodeOperator","outputs":[{"name":"active","type":"bool"},{"name":"name","type":"string"},{"name":"rewardAddress","type":"address"},{"name":"stakingLimit","type":"uint64"},{"name":"stoppedValidators","type":"uint64"},{"name":"totalSigningKeys","type":"uint64"},{"name":"usedSigningKeys","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_locator","type":"address"},{"name":"_type","type":"bytes32"},{"name":"_stuckPenaltyDelay","type":"uint256"}],"name":"finalizeUpgrade_v2","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getStakingModuleSummary","outputs":[{"name":"totalExitedValidators","type":"uint256"},{"name":"totalDepositedValidators","type":"uint256"},{"name":"depositableValidatorsCount","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorIds","type":"bytes"},{"name":"_exitedValidatorsCounts","type":"bytes"}],"name":"updateExitedValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorIds","type":"bytes"},{"name":"_stuckValidatorsCounts","type":"bytes"}],"name":"updateStuckValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_refundedValidatorsCount","type":"uint256"}],"name":"updateRefundedValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_vettedSigningKeysCount","type":"uint64"}],"name":"setNodeOperatorStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getNodeOperatorSummary","outputs":[{"name":"isTargetLimitActive","type":"bool"},{"name":"targetValidatorsCount","type":"uint256"},{"name":"stuckValidatorsCount","type":"uint256"},{"name":"refundedValidatorsCount","type":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256"},{"name":"totalExitedValidators","type":"uint256"},{"name":"totalDepositedValidators","type":"uint256"},{"name":"depositableValidatorsCount","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"getSigningKey","outputs":[{"name":"key","type":"bytes"},{"name":"depositSignature","type":"bytes"},{"name":"used","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_NODE_OPERATOR_NAME_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_depositsCount","type":"uint256"},{"name":"","type":"bytes"}],"name":"obtainDepositData","outputs":[{"name":"publicKeys","type":"bytes"},{"name":"signatures","type":"bytes"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getKeysOpIndex","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNonce","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLocator","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_LIMIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getTotalSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"onExitedAndStuckValidatorsCountsUpdated","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_NODE_OPERATORS_COUNT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKeyOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_exitedValidatorsCount","type":"uint256"},{"name":"_stuckValidatorsCount","type":"uint256"}],"name":"unsafeUpdateValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_SIGNING_KEYS","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"isOperatorPenaltyCleared","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"rewardAddress","type":"address"},{"indexed":false,"name":"stakingLimit","type":"uint64"}],"name":"NodeOperatorAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"active","type":"bool"}],"name":"NodeOperatorActiveSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"name","type":"string"}],"name":"NodeOperatorNameSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"rewardAddress","type":"address"}],"name":"NodeOperatorRewardAddressSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"totalKeysTrimmed","type":"uint64"}],"name":"NodeOperatorTotalKeysTrimmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"keysOpIndex","type":"uint256"}],"name":"KeysOpIndexSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"moduleType","type":"bytes32"}],"name":"StakingModuleTypeSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"rewardAddress","type":"address"},{"indexed":false,"name":"sharesAmount","type":"uint256"}],"name":"RewardsDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"locatorAddress","type":"address"}],"name":"LocatorContractSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"approvedValidatorsCount","type":"uint256"}],"name":"VettedSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"depositedValidatorsCount","type":"uint256"}],"name":"DepositedSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"exitedValidatorsCount","type":"uint256"}],"name":"ExitedSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"totalValidatorsCount","type":"uint256"}],"name":"TotalSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"nonce","type":"uint256"}],"name":"NonceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"stuckPenaltyDelay","type":"uint256"}],"name":"StuckPenaltyDelayChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"stuckValidatorsCount","type":"uint256"},{"indexed":false,"name":"refundedValidatorsCount","type":"uint256"},{"indexed":false,"name":"stuckPenaltyEndTimestamp","type":"uint256"}],"name":"StuckPenaltyStateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"targetValidatorsCount","type":"uint256"}],"name":"TargetValidatorsCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"recipientAddress","type":"address"},{"indexed":false,"name":"sharesPenalizedAmount","type":"uint256"}],"name":"NodeOperatorPenalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"}] \ No newline at end of file +[{"constant":true,"inputs":[],"name":"hasInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_keysCount","type":"uint256"},{"name":"_publicKeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getType","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_script","type":"bytes"}],"name":"getEVMScriptExecutor","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"clearNodeOperatorPenalty","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getRecoveryVault","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_offset","type":"uint256"},{"name":"_limit","type":"uint256"}],"name":"getNodeOperatorIds","outputs":[{"name":"nodeOperatorIds","type":"uint256[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_offset","type":"uint256"},{"name":"_limit","type":"uint256"}],"name":"getSigningKeys","outputs":[{"name":"pubkeys","type":"bytes"},{"name":"signatures","type":"bytes"},{"name":"used","type":"bool[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_fromIndex","type":"uint256"},{"name":"_keysCount","type":"uint256"}],"name":"removeSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getNodeOperatorIsActive","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_name","type":"string"}],"name":"setNodeOperatorName","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_totalRewardShares","type":"uint256"}],"name":"getRewardsDistribution","outputs":[{"name":"recipients","type":"address[]"},{"name":"shares","type":"uint256[]"},{"name":"penalized","type":"bool[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_indexFrom","type":"uint256"},{"name":"_indexTo","type":"uint256"}],"name":"invalidateReadyToDepositKeysRange","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_locator","type":"address"},{"name":"_type","type":"bytes32"},{"name":"_stuckPenaltyDelay","type":"uint256"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_delay","type":"uint256"}],"name":"setStuckPenaltyDelay","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getStuckPenaltyDelay","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_fromIndex","type":"uint256"},{"name":"_keysCount","type":"uint256"}],"name":"removeSigningKeys","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"isOperatorPenalized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"deactivateNodeOperator","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"token","type":"address"}],"name":"allowRecoverability","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"STAKING_ROUTER_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_keysCount","type":"uint256"},{"name":"_publicKeys","type":"bytes"},{"name":"_signatures","type":"bytes"}],"name":"addSigningKeysOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"appId","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getActiveNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_name","type":"string"},{"name":"_rewardAddress","type":"address"}],"name":"addNodeOperator","outputs":[{"name":"id","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getContractVersion","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getInitializationBlock","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getUnusedSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"onRewardsMinted","outputs":[],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_NODE_OPERATOR_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"onWithdrawalCredentialsChanged","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"activateNodeOperator","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_rewardAddress","type":"address"}],"name":"setNodeOperatorRewardAddress","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_fullInfo","type":"bool"}],"name":"getNodeOperator","outputs":[{"name":"active","type":"bool"},{"name":"name","type":"string"},{"name":"rewardAddress","type":"address"},{"name":"stakingLimit","type":"uint64"},{"name":"stoppedValidators","type":"uint64"},{"name":"totalSigningKeys","type":"uint64"},{"name":"usedSigningKeys","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_locator","type":"address"},{"name":"_type","type":"bytes32"},{"name":"_stuckPenaltyDelay","type":"uint256"}],"name":"finalizeUpgrade_v2","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getStakingModuleSummary","outputs":[{"name":"totalExitedValidators","type":"uint256"},{"name":"totalDepositedValidators","type":"uint256"},{"name":"depositableValidatorsCount","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorIds","type":"bytes"},{"name":"_exitedValidatorsCounts","type":"bytes"}],"name":"updateExitedValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorIds","type":"bytes"},{"name":"_stuckValidatorsCounts","type":"bytes"}],"name":"updateStuckValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_token","type":"address"}],"name":"transferToVault","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_sender","type":"address"},{"name":"_role","type":"bytes32"},{"name":"_params","type":"uint256[]"}],"name":"canPerform","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_refundedValidatorsCount","type":"uint256"}],"name":"updateRefundedValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getEVMScriptRegistry","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNodeOperatorsCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_isTargetLimitActive","type":"bool"},{"name":"_targetLimit","type":"uint256"}],"name":"updateTargetValidatorsLimits","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_vettedSigningKeysCount","type":"uint64"}],"name":"setNodeOperatorStakingLimit","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getNodeOperatorSummary","outputs":[{"name":"isTargetLimitActive","type":"bool"},{"name":"targetValidatorsCount","type":"uint256"},{"name":"stuckValidatorsCount","type":"uint256"},{"name":"refundedValidatorsCount","type":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256"},{"name":"totalExitedValidators","type":"uint256"},{"name":"totalDepositedValidators","type":"uint256"},{"name":"depositableValidatorsCount","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"getSigningKey","outputs":[{"name":"key","type":"bytes"},{"name":"depositSignature","type":"bytes"},{"name":"used","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_NODE_OPERATOR_NAME_LENGTH","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_depositsCount","type":"uint256"},{"name":"","type":"bytes"}],"name":"obtainDepositData","outputs":[{"name":"publicKeys","type":"bytes"},{"name":"signatures","type":"bytes"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getKeysOpIndex","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getNonce","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"kernel","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getLocator","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"SET_NODE_OPERATOR_LIMIT_ROLE","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"getTotalSigningKeyCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isPetrified","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"MAX_STUCK_PENALTY_DELAY","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"onExitedAndStuckValidatorsCountsUpdated","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_NODE_OPERATORS_COUNT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_index","type":"uint256"}],"name":"removeSigningKeyOperatorBH","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_nodeOperatorId","type":"uint256"},{"name":"_exitedValidatorsCount","type":"uint256"},{"name":"_stuckValidatorsCount","type":"uint256"}],"name":"unsafeUpdateValidatorsCount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MANAGE_SIGNING_KEYS","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_nodeOperatorId","type":"uint256"}],"name":"isOperatorPenaltyCleared","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"name","type":"string"},{"indexed":false,"name":"rewardAddress","type":"address"},{"indexed":false,"name":"stakingLimit","type":"uint64"}],"name":"NodeOperatorAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"active","type":"bool"}],"name":"NodeOperatorActiveSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"name","type":"string"}],"name":"NodeOperatorNameSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"rewardAddress","type":"address"}],"name":"NodeOperatorRewardAddressSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"totalKeysTrimmed","type":"uint64"}],"name":"NodeOperatorTotalKeysTrimmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"keysOpIndex","type":"uint256"}],"name":"KeysOpIndexSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"moduleType","type":"bytes32"}],"name":"StakingModuleTypeSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"rewardAddress","type":"address"},{"indexed":false,"name":"sharesAmount","type":"uint256"}],"name":"RewardsDistributed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"locatorAddress","type":"address"}],"name":"LocatorContractSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"approvedValidatorsCount","type":"uint256"}],"name":"VettedSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"depositedValidatorsCount","type":"uint256"}],"name":"DepositedSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"exitedValidatorsCount","type":"uint256"}],"name":"ExitedSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"totalValidatorsCount","type":"uint256"}],"name":"TotalSigningKeysCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"nonce","type":"uint256"}],"name":"NonceChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"stuckPenaltyDelay","type":"uint256"}],"name":"StuckPenaltyDelayChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"stuckValidatorsCount","type":"uint256"},{"indexed":false,"name":"refundedValidatorsCount","type":"uint256"},{"indexed":false,"name":"stuckPenaltyEndTimestamp","type":"uint256"}],"name":"StuckPenaltyStateChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"nodeOperatorId","type":"uint256"},{"indexed":false,"name":"targetValidatorsCount","type":"uint256"}],"name":"TargetValidatorsCountChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"recipientAddress","type":"address"},{"indexed":false,"name":"sharesPenalizedAmount","type":"uint256"}],"name":"NodeOperatorPenalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"executor","type":"address"},{"indexed":false,"name":"script","type":"bytes"},{"indexed":false,"name":"input","type":"bytes"},{"indexed":false,"name":"returnData","type":"bytes"}],"name":"ScriptResult","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"vault","type":"address"},{"indexed":true,"name":"token","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"RecoverToVault","type":"event"}] \ No newline at end of file diff --git a/lib/abi/OracleReportSanityChecker.json b/lib/abi/OracleReportSanityChecker.json index 17a81a848..e92dbaba8 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":[{"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":"simulatedShareDeviation","type":"uint256"}],"name":"IncorrectSimulatedShareRate","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":"TooHighTokenRebaseLimit","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":"_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":"_sharesBurntFromWithdrawalQueue","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":"_etherToLockForWithdrawals","type":"uint256"}],"name":"smoothenTokenRebase","outputs":[{"internalType":"uint256","name":"withdrawals","type":"uint256"},{"internalType":"uint256","name":"elRewards","type":"uint256"},{"internalType":"uint256","name":"sharesToBurnLimit","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":[{"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/StakingRouter.json b/lib/abi/StakingRouter.json index 756ae6ed7..4d4b74251 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":[],"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"}],"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":"StakingModuleIdTooLarge","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":"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"},{"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":"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"},{"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":[{"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 diff --git a/lib/abi/ValidatorsExitBusOracle.json b/lib/abi/ValidatorsExitBusOracle.json index d8dfe9d41..580e95171 100644 --- a/lib/abi/ValidatorsExitBusOracle.json +++ b/lib/abi/ValidatorsExitBusOracle.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"},{"internalType":"address","name":"lidoLocator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressCannotBeSame","type":"error"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"AdminCannotBeZero","type":"error"},{"inputs":[],"name":"ArgumentOutOfBounds","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[],"name":"InvalidRequestsData","type":"error"},{"inputs":[],"name":"InvalidRequestsDataLength","type":"error"},{"inputs":[],"name":"InvalidRequestsDataSortOrder","type":"error"},{"inputs":[{"internalType":"uint256","name":"moduleId","type":"uint256"},{"internalType":"uint256","name":"nodeOpId","type":"uint256"},{"internalType":"uint256","name":"prevRequestedValidatorIndex","type":"uint256"},{"internalType":"uint256","name":"requestedValidatorIndex","type":"uint256"}],"name":"NodeOpValidatorIndexMustIncrease","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"OnlyConsensusContractCanSubmitReport","type":"error"},{"inputs":[],"name":"PausedExpected","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":"processingRefSlot","type":"uint256"}],"name":"RefSlotCannotBeLessThanProcessingOne","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":"ResumedExpected","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":"consensusRefSlot","type":"uint256"},{"internalType":"uint256","name":"dataRefSlot","type":"uint256"}],"name":"UnexpectedRefSlot","type":"error"},{"inputs":[],"name":"UnexpectedRequestsDataLength","type":"error"},{"inputs":[{"internalType":"uint256","name":"format","type":"uint256"}],"name":"UnsupportedRequestsDataFormat","type":"error"},{"inputs":[],"name":"VersionCannotBeSame","type":"error"},{"inputs":[],"name":"ZeroPauseDuration","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":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Paused","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":[],"name":"Resumed","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":true,"internalType":"uint256","name":"nodeOperatorId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"validatorIndex","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"validatorPubkey","type":"bytes"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ValidatorExitRequest","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"requestsProcessed","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"requestsCount","type":"uint256"}],"name":"WarnDataIncompleteProcessing","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"}],"name":"WarnProcessingMissed","type":"event"},{"inputs":[],"name":"DATA_FORMAT_LIST","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"PAUSE_INFINITELY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_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":[{"internalType":"uint256","name":"moduleId","type":"uint256"},{"internalType":"uint256[]","name":"nodeOpIds","type":"uint256[]"}],"name":"getLastRequestedValidatorIndices","outputs":[{"internalType":"int256[]","name":"","type":"int256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getProcessingState","outputs":[{"components":[{"internalType":"uint256","name":"currentFrameRefSlot","type":"uint256"},{"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"},{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bool","name":"dataSubmitted","type":"bool"},{"internalType":"uint256","name":"dataFormat","type":"uint256"},{"internalType":"uint256","name":"requestsCount","type":"uint256"},{"internalType":"uint256","name":"requestsSubmitted","type":"uint256"}],"internalType":"struct ValidatorsExitBusOracle.ProcessingState","name":"result","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResumeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"getTotalRequestsProcessed","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":"pauser","type":"address"},{"internalType":"address","name":"resumer","type":"address"},{"internalType":"address","name":"consensusContract","type":"address"},{"internalType":"uint256","name":"consensusVersion","type":"uint256"},{"internalType":"uint256","name":"lastProcessingRefSlot","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_duration","type":"uint256"}],"name":"pause","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":[],"name":"resume","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":"requestsCount","type":"uint256"},{"internalType":"uint256","name":"dataFormat","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct ValidatorsExitBusOracle.ReportData","name":"data","type":"tuple"},{"internalType":"uint256","name":"contractVersion","type":"uint256"}],"name":"submitReportData","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":"uint256","name":"secondsPerSlot","type":"uint256"},{"internalType":"uint256","name":"genesisTime","type":"uint256"},{"internalType":"address","name":"lidoLocator","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AddressCannotBeSame","type":"error"},{"inputs":[],"name":"AddressCannotBeZero","type":"error"},{"inputs":[],"name":"AdminCannotBeZero","type":"error"},{"inputs":[],"name":"ArgumentOutOfBounds","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":"InvalidRequestsData","type":"error"},{"inputs":[],"name":"InvalidRequestsDataLength","type":"error"},{"inputs":[],"name":"InvalidRequestsDataSortOrder","type":"error"},{"inputs":[{"internalType":"uint256","name":"moduleId","type":"uint256"},{"internalType":"uint256","name":"nodeOpId","type":"uint256"},{"internalType":"uint256","name":"prevRequestedValidatorIndex","type":"uint256"},{"internalType":"uint256","name":"requestedValidatorIndex","type":"uint256"}],"name":"NodeOpValidatorIndexMustIncrease","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"OnlyConsensusContractCanSubmitReport","type":"error"},{"inputs":[],"name":"PauseUntilMustBeInFuture","type":"error"},{"inputs":[],"name":"PausedExpected","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":"ResumedExpected","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":"consensusRefSlot","type":"uint256"},{"internalType":"uint256","name":"dataRefSlot","type":"uint256"}],"name":"UnexpectedRefSlot","type":"error"},{"inputs":[],"name":"UnexpectedRequestsDataLength","type":"error"},{"inputs":[{"internalType":"uint256","name":"format","type":"uint256"}],"name":"UnsupportedRequestsDataFormat","type":"error"},{"inputs":[],"name":"VersionCannotBeSame","type":"error"},{"inputs":[],"name":"ZeroPauseDuration","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":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Paused","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":[],"name":"Resumed","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":true,"internalType":"uint256","name":"nodeOperatorId","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"validatorIndex","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"validatorPubkey","type":"bytes"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ValidatorExitRequest","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"requestsProcessed","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"requestsCount","type":"uint256"}],"name":"WarnDataIncompleteProcessing","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"refSlot","type":"uint256"}],"name":"WarnProcessingMissed","type":"event"},{"inputs":[],"name":"DATA_FORMAT_LIST","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"GENESIS_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"PAUSE_INFINITELY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_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":[{"internalType":"uint256","name":"moduleId","type":"uint256"},{"internalType":"uint256[]","name":"nodeOpIds","type":"uint256[]"}],"name":"getLastRequestedValidatorIndices","outputs":[{"internalType":"int256[]","name":"","type":"int256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getProcessingState","outputs":[{"components":[{"internalType":"uint256","name":"currentFrameRefSlot","type":"uint256"},{"internalType":"uint256","name":"processingDeadlineTime","type":"uint256"},{"internalType":"bytes32","name":"dataHash","type":"bytes32"},{"internalType":"bool","name":"dataSubmitted","type":"bool"},{"internalType":"uint256","name":"dataFormat","type":"uint256"},{"internalType":"uint256","name":"requestsCount","type":"uint256"},{"internalType":"uint256","name":"requestsSubmitted","type":"uint256"}],"internalType":"struct ValidatorsExitBusOracle.ProcessingState","name":"result","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResumeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"getTotalRequestsProcessed","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"},{"internalType":"uint256","name":"lastProcessingRefSlot","type":"uint256"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_duration","type":"uint256"}],"name":"pauseFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pauseUntilInclusive","type":"uint256"}],"name":"pauseUntil","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":[],"name":"resume","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":"requestsCount","type":"uint256"},{"internalType":"uint256","name":"dataFormat","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct ValidatorsExitBusOracle.ReportData","name":"data","type":"tuple"},{"internalType":"uint256","name":"contractVersion","type":"uint256"}],"name":"submitReportData","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/WithdrawalQueue.json b/lib/abi/WithdrawalQueue.json index 630bba300..ea968038c 100644 --- a/lib/abi/WithdrawalQueue.json +++ b/lib/abi/WithdrawalQueue.json @@ -1 +1 @@ -[{"inputs":[],"name":"AdminZeroAddress","type":"error"},{"inputs":[],"name":"CantSendValueRecipientMayHaveReverted","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[{"internalType":"uint256","name":"_hint","type":"uint256"}],"name":"InvalidHint","type":"error"},{"inputs":[],"name":"InvalidReportTimestamp","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"InvalidRequestId","type":"error"},{"inputs":[{"internalType":"uint256","name":"startId","type":"uint256"},{"internalType":"uint256","name":"endId","type":"uint256"}],"name":"InvalidRequestIdRange","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"NotEnoughEther","type":"error"},{"inputs":[{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"name":"NotOwner","type":"error"},{"inputs":[],"name":"PausedExpected","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestAlreadyClaimed","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooLarge","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooSmall","type":"error"},{"inputs":[],"name":"RequestIdsNotSorted","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestNotFoundOrNotFinalized","type":"error"},{"inputs":[],"name":"ResumedExpected","type":"error"},{"inputs":[{"internalType":"uint256","name":"sent","type":"uint256"},{"internalType":"uint256","name":"maxExpected","type":"uint256"}],"name":"TooMuchEtherToFinalize","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[],"name":"ZeroAmountOfETH","type":"error"},{"inputs":[],"name":"ZeroPauseDuration","type":"error"},{"inputs":[],"name":"ZeroRecipient","type":"error"},{"inputs":[],"name":"ZeroShareRate","type":"error"},{"inputs":[],"name":"ZeroTimestamp","type":"error"},{"anonymous":false,"inputs":[],"name":"BunkerModeDisabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_sinceTimestamp","type":"uint256"}],"name":"BunkerModeEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_admin","type":"address"},{"indexed":false,"internalType":"address","name":"_pauser","type":"address"},{"indexed":false,"internalType":"address","name":"_resumer","type":"address"},{"indexed":false,"internalType":"address","name":"_finalizer","type":"address"},{"indexed":false,"internalType":"address","name":"_bunkerReporter","type":"address"}],"name":"InitializedV1","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","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":"from","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"to","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfETHLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"WithdrawalBatchFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfETH","type":"uint256"}],"name":"WithdrawalClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"requestor","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"WithdrawalRequested","type":"event"},{"inputs":[],"name":"BUNKER_MODE_DISABLED_TIMESTAMP","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"BUNKER_MODE_REPORT_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":"E27_PRECISION_BASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FINALIZE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_INFINITELY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"contract IStETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WSTETH","outputs":[{"internalType":"contract IWstETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"bunkerModeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"claimWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"claimWithdrawals","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"claimWithdrawalsTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_nextFinalizedRequestId","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"}],"name":"finalizationBatch","outputs":[{"internalType":"uint256","name":"ethToLock","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_nextFinalizedRequestId","type":"uint256"}],"name":"finalize","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256","name":"_firstIndex","type":"uint256"},{"internalType":"uint256","name":"_lastIndex","type":"uint256"}],"name":"findCheckpointHints","outputs":[{"internalType":"uint256[]","name":"hintIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"findCheckpointHintsUnbounded","outputs":[{"internalType":"uint256[]","name":"hintIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ethBudget","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"},{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"}],"name":"findLastFinalizableRequestId","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ethBudget","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"},{"internalType":"uint256","name":"_startId","type":"uint256"},{"internalType":"uint256","name":"_endId","type":"uint256"}],"name":"findLastFinalizableRequestIdByBudget","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"},{"internalType":"uint256","name":"_startId","type":"uint256"},{"internalType":"uint256","name":"_endId","type":"uint256"}],"name":"findLastFinalizableRequestIdByTimestamp","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"getClaimableEther","outputs":[{"internalType":"uint256[]","name":"claimableEthValues","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastCheckpointIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastFinalizedRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedEtherAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResumeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"address","name":"_owner","type":"address"}],"name":"getWithdrawalRequests","outputs":[{"internalType":"uint256[]","name":"requestsIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"getWithdrawalStatus","outputs":[{"components":[{"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"internalType":"uint256","name":"amountOfShares","type":"uint256"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bool","name":"isFinalized","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"internalType":"struct WithdrawalQueueBase.WithdrawalRequestStatus[]","name":"statuses","type":"tuple[]"}],"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":"_pauser","type":"address"},{"internalType":"address","name":"_resumer","type":"address"},{"internalType":"address","name":"_finalizer","type":"address"},{"internalType":"address","name":"_bunkerReporter","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isBunkerModeActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_duration","type":"uint256"}],"name":"pause","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":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawals","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawalsWstETH","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWstETHWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resume","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":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedRequestNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"_isBunkerModeNow","type":"bool"},{"internalType":"uint256","name":"_sinceTimestamp","type":"uint256"}],"name":"updateBunkerMode","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[],"name":"AdminZeroAddress","type":"error"},{"inputs":[],"name":"BatchesAreNotSorted","type":"error"},{"inputs":[],"name":"CantSendValueRecipientMayHaveReverted","type":"error"},{"inputs":[],"name":"EmptyBatches","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[{"internalType":"uint256","name":"_hint","type":"uint256"}],"name":"InvalidHint","type":"error"},{"inputs":[],"name":"InvalidReportTimestamp","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"InvalidRequestId","type":"error"},{"inputs":[{"internalType":"uint256","name":"startId","type":"uint256"},{"internalType":"uint256","name":"endId","type":"uint256"}],"name":"InvalidRequestIdRange","type":"error"},{"inputs":[],"name":"InvalidState","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"NotEnoughEther","type":"error"},{"inputs":[{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"name":"NotOwner","type":"error"},{"inputs":[],"name":"PauseUntilMustBeInFuture","type":"error"},{"inputs":[],"name":"PausedExpected","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestAlreadyClaimed","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooLarge","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooSmall","type":"error"},{"inputs":[],"name":"RequestIdsNotSorted","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestNotFoundOrNotFinalized","type":"error"},{"inputs":[],"name":"ResumedExpected","type":"error"},{"inputs":[{"internalType":"uint256","name":"sent","type":"uint256"},{"internalType":"uint256","name":"maxExpected","type":"uint256"}],"name":"TooMuchEtherToFinalize","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[],"name":"ZeroAmountOfETH","type":"error"},{"inputs":[],"name":"ZeroPauseDuration","type":"error"},{"inputs":[],"name":"ZeroRecipient","type":"error"},{"inputs":[],"name":"ZeroShareRate","type":"error"},{"inputs":[],"name":"ZeroTimestamp","type":"error"},{"anonymous":false,"inputs":[],"name":"BunkerModeDisabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_sinceTimestamp","type":"uint256"}],"name":"BunkerModeEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_admin","type":"address"}],"name":"InitializedV1","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","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":"from","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"to","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfETHLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"WithdrawalBatchFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfETH","type":"uint256"}],"name":"WithdrawalClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"requestor","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"WithdrawalRequested","type":"event"},{"inputs":[],"name":"BUNKER_MODE_DISABLED_TIMESTAMP","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FINALIZE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_BATCHES_LENGTH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ORACLE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_INFINITELY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"contract IStETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WSTETH","outputs":[{"internalType":"contract IWstETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"bunkerModeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxShareRate","type":"uint256"},{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"},{"internalType":"uint256","name":"_maxRequestsPerCall","type":"uint256"},{"components":[{"internalType":"uint256","name":"remainingEthBudget","type":"uint256"},{"internalType":"bool","name":"finished","type":"bool"},{"internalType":"uint256[36]","name":"batches","type":"uint256[36]"},{"internalType":"uint256","name":"batchesLength","type":"uint256"}],"internalType":"struct WithdrawalQueueBase.BatchesCalculationState","name":"_state","type":"tuple"}],"name":"calculateFinalizationBatches","outputs":[{"components":[{"internalType":"uint256","name":"remainingEthBudget","type":"uint256"},{"internalType":"bool","name":"finished","type":"bool"},{"internalType":"uint256[36]","name":"batches","type":"uint256[36]"},{"internalType":"uint256","name":"batchesLength","type":"uint256"}],"internalType":"struct WithdrawalQueueBase.BatchesCalculationState","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"claimWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"claimWithdrawals","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"claimWithdrawalsTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_batches","type":"uint256[]"},{"internalType":"uint256","name":"_maxShareRate","type":"uint256"}],"name":"finalize","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256","name":"_firstIndex","type":"uint256"},{"internalType":"uint256","name":"_lastIndex","type":"uint256"}],"name":"findCheckpointHints","outputs":[{"internalType":"uint256[]","name":"hintIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"getClaimableEther","outputs":[{"internalType":"uint256[]","name":"claimableEthValues","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastCheckpointIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastFinalizedRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedEtherAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResumeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"address","name":"_owner","type":"address"}],"name":"getWithdrawalRequests","outputs":[{"internalType":"uint256[]","name":"requestsIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"getWithdrawalStatus","outputs":[{"components":[{"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"internalType":"uint256","name":"amountOfShares","type":"uint256"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bool","name":"isFinalized","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"internalType":"struct WithdrawalQueueBase.WithdrawalRequestStatus[]","name":"statuses","type":"tuple[]"}],"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"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isBunkerModeActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"_isBunkerModeNow","type":"bool"},{"internalType":"uint256","name":"_bunkerStartTimestamp","type":"uint256"},{"internalType":"uint256","name":"_currentReportTimestamp","type":"uint256"}],"name":"onOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_duration","type":"uint256"}],"name":"pauseFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pauseUntilInclusive","type":"uint256"}],"name":"pauseUntil","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_batches","type":"uint256[]"},{"internalType":"uint256","name":"_maxShareRate","type":"uint256"}],"name":"prefinalize","outputs":[{"internalType":"uint256","name":"ethToLock","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"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":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawals","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawalsWstETH","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWstETHWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resume","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":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedRequestNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/WithdrawalQueueBase.json b/lib/abi/WithdrawalQueueBase.json index bb0aec71d..952b59cc8 100644 --- a/lib/abi/WithdrawalQueueBase.json +++ b/lib/abi/WithdrawalQueueBase.json @@ -1 +1 @@ -[{"inputs":[],"name":"CantSendValueRecipientMayHaveReverted","type":"error"},{"inputs":[{"internalType":"uint256","name":"_hint","type":"uint256"}],"name":"InvalidHint","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"InvalidRequestId","type":"error"},{"inputs":[{"internalType":"uint256","name":"startId","type":"uint256"},{"internalType":"uint256","name":"endId","type":"uint256"}],"name":"InvalidRequestIdRange","type":"error"},{"inputs":[],"name":"NotEnoughEther","type":"error"},{"inputs":[{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"name":"NotOwner","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestAlreadyClaimed","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestNotFoundOrNotFinalized","type":"error"},{"inputs":[{"internalType":"uint256","name":"sent","type":"uint256"},{"internalType":"uint256","name":"maxExpected","type":"uint256"}],"name":"TooMuchEtherToFinalize","type":"error"},{"inputs":[],"name":"ZeroAmountOfETH","type":"error"},{"inputs":[],"name":"ZeroShareRate","type":"error"},{"inputs":[],"name":"ZeroTimestamp","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"from","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"to","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfETHLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"WithdrawalBatchFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfETH","type":"uint256"}],"name":"WithdrawalClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"requestor","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"WithdrawalRequested","type":"event"},{"inputs":[],"name":"E27_PRECISION_BASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_nextFinalizedRequestId","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"}],"name":"finalizationBatch","outputs":[{"internalType":"uint256","name":"ethToLock","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ethBudget","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"},{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"}],"name":"findLastFinalizableRequestId","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ethBudget","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"},{"internalType":"uint256","name":"_startId","type":"uint256"},{"internalType":"uint256","name":"_endId","type":"uint256"}],"name":"findLastFinalizableRequestIdByBudget","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"},{"internalType":"uint256","name":"_startId","type":"uint256"},{"internalType":"uint256","name":"_endId","type":"uint256"}],"name":"findLastFinalizableRequestIdByTimestamp","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastCheckpointIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastFinalizedRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedEtherAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedRequestNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file +[{"inputs":[],"name":"BatchesAreNotSorted","type":"error"},{"inputs":[],"name":"CantSendValueRecipientMayHaveReverted","type":"error"},{"inputs":[],"name":"EmptyBatches","type":"error"},{"inputs":[{"internalType":"uint256","name":"_hint","type":"uint256"}],"name":"InvalidHint","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"InvalidRequestId","type":"error"},{"inputs":[{"internalType":"uint256","name":"startId","type":"uint256"},{"internalType":"uint256","name":"endId","type":"uint256"}],"name":"InvalidRequestIdRange","type":"error"},{"inputs":[],"name":"InvalidState","type":"error"},{"inputs":[],"name":"NotEnoughEther","type":"error"},{"inputs":[{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"name":"NotOwner","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestAlreadyClaimed","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestNotFoundOrNotFinalized","type":"error"},{"inputs":[{"internalType":"uint256","name":"sent","type":"uint256"},{"internalType":"uint256","name":"maxExpected","type":"uint256"}],"name":"TooMuchEtherToFinalize","type":"error"},{"inputs":[],"name":"ZeroAmountOfETH","type":"error"},{"inputs":[],"name":"ZeroShareRate","type":"error"},{"inputs":[],"name":"ZeroTimestamp","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"from","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"to","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfETHLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"WithdrawalBatchFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfETH","type":"uint256"}],"name":"WithdrawalClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"requestor","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"WithdrawalRequested","type":"event"},{"inputs":[],"name":"MAX_BATCHES_LENGTH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxShareRate","type":"uint256"},{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"},{"internalType":"uint256","name":"_maxRequestsPerCall","type":"uint256"},{"components":[{"internalType":"uint256","name":"remainingEthBudget","type":"uint256"},{"internalType":"bool","name":"finished","type":"bool"},{"internalType":"uint256[36]","name":"batches","type":"uint256[36]"},{"internalType":"uint256","name":"batchesLength","type":"uint256"}],"internalType":"struct WithdrawalQueueBase.BatchesCalculationState","name":"_state","type":"tuple"}],"name":"calculateFinalizationBatches","outputs":[{"components":[{"internalType":"uint256","name":"remainingEthBudget","type":"uint256"},{"internalType":"bool","name":"finished","type":"bool"},{"internalType":"uint256[36]","name":"batches","type":"uint256[36]"},{"internalType":"uint256","name":"batchesLength","type":"uint256"}],"internalType":"struct WithdrawalQueueBase.BatchesCalculationState","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastCheckpointIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastFinalizedRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedEtherAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_batches","type":"uint256[]"},{"internalType":"uint256","name":"_maxShareRate","type":"uint256"}],"name":"prefinalize","outputs":[{"internalType":"uint256","name":"ethToLock","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedRequestNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/lib/abi/WithdrawalQueueERC721.json b/lib/abi/WithdrawalQueueERC721.json index 728ac3391..61bdc2836 100644 --- a/lib/abi/WithdrawalQueueERC721.json +++ b/lib/abi/WithdrawalQueueERC721.json @@ -1 +1 @@ -[{"inputs":[{"internalType":"address","name":"_wstETH","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AdminZeroAddress","type":"error"},{"inputs":[],"name":"ApprovalToOwner","type":"error"},{"inputs":[],"name":"ApproveToCaller","type":"error"},{"inputs":[],"name":"CantSendValueRecipientMayHaveReverted","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[{"internalType":"uint256","name":"_hint","type":"uint256"}],"name":"InvalidHint","type":"error"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"InvalidOwnerAddress","type":"error"},{"inputs":[],"name":"InvalidReportTimestamp","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"InvalidRequestId","type":"error"},{"inputs":[{"internalType":"uint256","name":"startId","type":"uint256"},{"internalType":"uint256","name":"endId","type":"uint256"}],"name":"InvalidRequestIdRange","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"NotEnoughEther","type":"error"},{"inputs":[{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"name":"NotOwner","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"NotOwnerOrApproved","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"NotOwnerOrApprovedForAll","type":"error"},{"inputs":[],"name":"PausedExpected","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestAlreadyClaimed","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooLarge","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooSmall","type":"error"},{"inputs":[],"name":"RequestIdsNotSorted","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestNotFoundOrNotFinalized","type":"error"},{"inputs":[],"name":"ResumedExpected","type":"error"},{"inputs":[{"internalType":"string","name":"str","type":"string"}],"name":"StringTooLong","type":"error"},{"inputs":[{"internalType":"uint256","name":"sent","type":"uint256"},{"internalType":"uint256","name":"maxExpected","type":"uint256"}],"name":"TooMuchEtherToFinalize","type":"error"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"realOwner","type":"address"}],"name":"TransferFromIncorrectOwner","type":"error"},{"inputs":[],"name":"TransferFromZeroAddress","type":"error"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"TransferToNonIERC721Receiver","type":"error"},{"inputs":[],"name":"TransferToThemselves","type":"error"},{"inputs":[],"name":"TransferToZeroAddress","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[],"name":"ZeroAmountOfETH","type":"error"},{"inputs":[],"name":"ZeroMetadata","type":"error"},{"inputs":[],"name":"ZeroPauseDuration","type":"error"},{"inputs":[],"name":"ZeroRecipient","type":"error"},{"inputs":[],"name":"ZeroShareRate","type":"error"},{"inputs":[],"name":"ZeroTimestamp","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"baseURI","type":"string"}],"name":"BaseURISet","type":"event"},{"anonymous":false,"inputs":[],"name":"BunkerModeDisabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_sinceTimestamp","type":"uint256"}],"name":"BunkerModeEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_admin","type":"address"},{"indexed":false,"internalType":"address","name":"_pauser","type":"address"},{"indexed":false,"internalType":"address","name":"_resumer","type":"address"},{"indexed":false,"internalType":"address","name":"_finalizer","type":"address"},{"indexed":false,"internalType":"address","name":"_bunkerReporter","type":"address"}],"name":"InitializedV1","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"nftDescriptorAddress","type":"address"}],"name":"NftDescriptorAddressSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","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":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"from","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"to","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfETHLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"WithdrawalBatchFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfETH","type":"uint256"}],"name":"WithdrawalClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"requestor","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"WithdrawalRequested","type":"event"},{"inputs":[],"name":"BUNKER_MODE_DISABLED_TIMESTAMP","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"BUNKER_MODE_REPORT_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":"E27_PRECISION_BASE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FINALIZE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_TOKEN_URI_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_INFINITELY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"contract IStETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WSTETH","outputs":[{"internalType":"contract IWstETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"bunkerModeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"claimWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"claimWithdrawals","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"claimWithdrawalsTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_nextFinalizedRequestId","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"}],"name":"finalizationBatch","outputs":[{"internalType":"uint256","name":"ethToLock","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_nextFinalizedRequestId","type":"uint256"}],"name":"finalize","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256","name":"_firstIndex","type":"uint256"},{"internalType":"uint256","name":"_lastIndex","type":"uint256"}],"name":"findCheckpointHints","outputs":[{"internalType":"uint256[]","name":"hintIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"findCheckpointHintsUnbounded","outputs":[{"internalType":"uint256[]","name":"hintIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ethBudget","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"},{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"}],"name":"findLastFinalizableRequestId","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ethBudget","type":"uint256"},{"internalType":"uint256","name":"_shareRate","type":"uint256"},{"internalType":"uint256","name":"_startId","type":"uint256"},{"internalType":"uint256","name":"_endId","type":"uint256"}],"name":"findLastFinalizableRequestIdByBudget","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"},{"internalType":"uint256","name":"_startId","type":"uint256"},{"internalType":"uint256","name":"_endId","type":"uint256"}],"name":"findLastFinalizableRequestIdByTimestamp","outputs":[{"internalType":"uint256","name":"finalizableRequestId","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBaseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"getClaimableEther","outputs":[{"internalType":"uint256[]","name":"claimableEthValues","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastCheckpointIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastFinalizedRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedEtherAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNFTDescriptorAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResumeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"address","name":"_owner","type":"address"}],"name":"getWithdrawalRequests","outputs":[{"internalType":"uint256[]","name":"requestsIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"getWithdrawalStatus","outputs":[{"components":[{"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"internalType":"uint256","name":"amountOfShares","type":"uint256"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bool","name":"isFinalized","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"internalType":"struct WithdrawalQueueBase.WithdrawalRequestStatus[]","name":"statuses","type":"tuple[]"}],"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":"_pauser","type":"address"},{"internalType":"address","name":"_resumer","type":"address"},{"internalType":"address","name":"_finalizer","type":"address"},{"internalType":"address","name":"_bunkerReporter","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isBunkerModeActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_duration","type":"uint256"}],"name":"pause","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":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawals","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawalsWstETH","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWstETHWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resume","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":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_operator","type":"address"},{"internalType":"bool","name":"_approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_baseURI","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_nftDescriptorAddress","type":"address"}],"name":"setNFTDescriptorAddress","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":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unfinalizedRequestNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"_isBunkerModeNow","type":"bool"},{"internalType":"uint256","name":"_sinceTimestamp","type":"uint256"}],"name":"updateBunkerMode","outputs":[],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file +[{"inputs":[{"internalType":"address","name":"_wstETH","type":"address"},{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AdminZeroAddress","type":"error"},{"inputs":[],"name":"ApprovalToOwner","type":"error"},{"inputs":[],"name":"ApproveToCaller","type":"error"},{"inputs":[],"name":"BatchesAreNotSorted","type":"error"},{"inputs":[],"name":"CantSendValueRecipientMayHaveReverted","type":"error"},{"inputs":[],"name":"EmptyBatches","type":"error"},{"inputs":[],"name":"InvalidContractVersionIncrement","type":"error"},{"inputs":[{"internalType":"uint256","name":"_hint","type":"uint256"}],"name":"InvalidHint","type":"error"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"InvalidOwnerAddress","type":"error"},{"inputs":[],"name":"InvalidReportTimestamp","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"InvalidRequestId","type":"error"},{"inputs":[{"internalType":"uint256","name":"startId","type":"uint256"},{"internalType":"uint256","name":"endId","type":"uint256"}],"name":"InvalidRequestIdRange","type":"error"},{"inputs":[],"name":"InvalidState","type":"error"},{"inputs":[],"name":"NonZeroContractVersionOnInit","type":"error"},{"inputs":[],"name":"NotEnoughEther","type":"error"},{"inputs":[{"internalType":"address","name":"_sender","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"name":"NotOwner","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"NotOwnerOrApproved","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"NotOwnerOrApprovedForAll","type":"error"},{"inputs":[],"name":"PauseUntilMustBeInFuture","type":"error"},{"inputs":[],"name":"PausedExpected","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestAlreadyClaimed","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooLarge","type":"error"},{"inputs":[{"internalType":"uint256","name":"_amountOfStETH","type":"uint256"}],"name":"RequestAmountTooSmall","type":"error"},{"inputs":[],"name":"RequestIdsNotSorted","type":"error"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"RequestNotFoundOrNotFinalized","type":"error"},{"inputs":[],"name":"ResumedExpected","type":"error"},{"inputs":[{"internalType":"string","name":"str","type":"string"}],"name":"StringTooLong","type":"error"},{"inputs":[{"internalType":"uint256","name":"sent","type":"uint256"},{"internalType":"uint256","name":"maxExpected","type":"uint256"}],"name":"TooMuchEtherToFinalize","type":"error"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"realOwner","type":"address"}],"name":"TransferFromIncorrectOwner","type":"error"},{"inputs":[],"name":"TransferFromZeroAddress","type":"error"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"TransferToNonIERC721Receiver","type":"error"},{"inputs":[],"name":"TransferToThemselves","type":"error"},{"inputs":[],"name":"TransferToZeroAddress","type":"error"},{"inputs":[{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"received","type":"uint256"}],"name":"UnexpectedContractVersion","type":"error"},{"inputs":[],"name":"ZeroAmountOfETH","type":"error"},{"inputs":[],"name":"ZeroMetadata","type":"error"},{"inputs":[],"name":"ZeroPauseDuration","type":"error"},{"inputs":[],"name":"ZeroRecipient","type":"error"},{"inputs":[],"name":"ZeroShareRate","type":"error"},{"inputs":[],"name":"ZeroTimestamp","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"baseURI","type":"string"}],"name":"BaseURISet","type":"event"},{"anonymous":false,"inputs":[],"name":"BunkerModeDisabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_sinceTimestamp","type":"uint256"}],"name":"BunkerModeEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"version","type":"uint256"}],"name":"ContractVersionSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_admin","type":"address"}],"name":"InitializedV1","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"nftDescriptorAddress","type":"address"}],"name":"NftDescriptorAddressSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[],"name":"Resumed","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":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"from","type":"uint256"},{"indexed":true,"internalType":"uint256","name":"to","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfETHLocked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"sharesToBurn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"WithdrawalBatchFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfETH","type":"uint256"}],"name":"WithdrawalClaimed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"requestId","type":"uint256"},{"indexed":true,"internalType":"address","name":"requestor","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOfShares","type":"uint256"}],"name":"WithdrawalRequested","type":"event"},{"inputs":[],"name":"BUNKER_MODE_DISABLED_TIMESTAMP","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FINALIZE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MANAGE_TOKEN_URI_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_BATCHES_LENGTH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_STETH_WITHDRAWAL_AMOUNT","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ORACLE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_INFINITELY","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PAUSE_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RESUME_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"STETH","outputs":[{"internalType":"contract IStETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WSTETH","outputs":[{"internalType":"contract IWstETH","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"bunkerModeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_maxShareRate","type":"uint256"},{"internalType":"uint256","name":"_maxTimestamp","type":"uint256"},{"internalType":"uint256","name":"_maxRequestsPerCall","type":"uint256"},{"components":[{"internalType":"uint256","name":"remainingEthBudget","type":"uint256"},{"internalType":"bool","name":"finished","type":"bool"},{"internalType":"uint256[36]","name":"batches","type":"uint256[36]"},{"internalType":"uint256","name":"batchesLength","type":"uint256"}],"internalType":"struct WithdrawalQueueBase.BatchesCalculationState","name":"_state","type":"tuple"}],"name":"calculateFinalizationBatches","outputs":[{"components":[{"internalType":"uint256","name":"remainingEthBudget","type":"uint256"},{"internalType":"bool","name":"finished","type":"bool"},{"internalType":"uint256[36]","name":"batches","type":"uint256[36]"},{"internalType":"uint256","name":"batchesLength","type":"uint256"}],"internalType":"struct WithdrawalQueueBase.BatchesCalculationState","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"claimWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"claimWithdrawals","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"},{"internalType":"address","name":"_recipient","type":"address"}],"name":"claimWithdrawalsTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_batches","type":"uint256[]"},{"internalType":"uint256","name":"_maxShareRate","type":"uint256"}],"name":"finalize","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256","name":"_firstIndex","type":"uint256"},{"internalType":"uint256","name":"_lastIndex","type":"uint256"}],"name":"findCheckpointHints","outputs":[{"internalType":"uint256[]","name":"hintIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBaseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"},{"internalType":"uint256[]","name":"_hints","type":"uint256[]"}],"name":"getClaimableEther","outputs":[{"internalType":"uint256[]","name":"claimableEthValues","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getContractVersion","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastCheckpointIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastFinalizedRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastRequestId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLockedEtherAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNFTDescriptorAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getResumeSinceTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"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":"address","name":"_owner","type":"address"}],"name":"getWithdrawalRequests","outputs":[{"internalType":"uint256[]","name":"requestsIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_requestIds","type":"uint256[]"}],"name":"getWithdrawalStatus","outputs":[{"components":[{"internalType":"uint256","name":"amountOfStETH","type":"uint256"},{"internalType":"uint256","name":"amountOfShares","type":"uint256"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bool","name":"isFinalized","type":"bool"},{"internalType":"bool","name":"isClaimed","type":"bool"}],"internalType":"struct WithdrawalQueueBase.WithdrawalRequestStatus[]","name":"statuses","type":"tuple[]"}],"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"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isBunkerModeActive","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"_isBunkerModeNow","type":"bool"},{"internalType":"uint256","name":"_bunkerStartTimestamp","type":"uint256"},{"internalType":"uint256","name":"_currentReportTimestamp","type":"uint256"}],"name":"onOracleReport","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_duration","type":"uint256"}],"name":"pauseFor","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_pauseUntilInclusive","type":"uint256"}],"name":"pauseUntil","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_batches","type":"uint256[]"},{"internalType":"uint256","name":"_maxShareRate","type":"uint256"}],"name":"prefinalize","outputs":[{"internalType":"uint256","name":"ethToLock","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"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":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawals","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"}],"name":"requestWithdrawalsWstETH","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"_amounts","type":"uint256[]"},{"internalType":"address","name":"_owner","type":"address"},{"components":[{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"internalType":"struct WithdrawalQueue.PermitInput","name":"_permit","type":"tuple"}],"name":"requestWithdrawalsWstETHWithPermit","outputs":[{"internalType":"uint256[]","name":"requestIds","type":"uint256[]"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"resume","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":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_operator","type":"address"},{"internalType":"bool","name":"_approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_baseURI","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_nftDescriptorAddress","type":"address"}],"name":"setNFTDescriptorAddress","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":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_requestId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unfinalizedRequestNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unfinalizedStETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}] \ No newline at end of file diff --git a/package.json b/package.json index 976ea1485..1d389849e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "test:e2e": "npm run compile && ava -T 1000000 -v", "estimate-deposit-loop-gas": "yarn run test:unit ./estimate_deposit_loop_gas.js", "compile": "hardhat compile && yarn extract-abi", + "clean": "hardhat clean", "extract-abi": "node ./scripts/extract-abi.js", "size-contracts": "yarn run hardhat size-contracts", "deploy": "yarn run deploy:all", @@ -132,6 +133,7 @@ "@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/helpers/permit_helpers.js b/test/0.4.24/helpers/permit_helpers.js index 948e36c0b..c7fca8c7c 100644 --- a/test/0.4.24/helpers/permit_helpers.js +++ b/test/0.4.24/helpers/permit_helpers.js @@ -1,5 +1,6 @@ const { web3 } = require('hardhat') -const { ecSign, strip0x } = require('./sign_utils') +const { strip0x } = require('../../helpers/utils') +const { ecSign } = require('../../helpers/signatures') const transferWithAuthorizationTypeHash = web3.utils.keccak256( 'TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)' @@ -10,33 +11,39 @@ const permitTypeHash = web3.utils.keccak256( ) function signTransferAuthorization(from, to, value, validAfter, validBefore, nonce, domainSeparator, privateKey) { - return signEIP712( - domainSeparator, - transferWithAuthorizationTypeHash, - ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes32'], - [from, to, value, validAfter, validBefore, nonce], - privateKey - ) + const digest = calculateTransferAuthorizationDigest(from, to, value, validAfter, validBefore, nonce, domainSeparator) + return ecSign(digest, privateKey) } function signPermit(owner, spender, value, nonce, deadline, domainSeparator, privateKey) { - return signEIP712( + const digest = calculatePermitDigest(owner, spender, value, nonce, deadline, domainSeparator) + return ecSign(digest, privateKey) +} + +function calculatePermitDigest(owner, spender, value, nonce, deadline, domainSeparator) { + return calculateEIP712Digest( domainSeparator, permitTypeHash, ['address', 'address', 'uint256', 'uint256', 'uint256'], - [owner, spender, value, nonce, deadline], - privateKey + [owner, spender, value, nonce, deadline] ) } -function signEIP712(domainSeparator, typeHash, types, parameters, privateKey) { - const digest = web3.utils.keccak256( +function calculateTransferAuthorizationDigest(from, to, value, validAfter, validBefore, nonce, domainSeparator) { + return calculateEIP712Digest( + domainSeparator, + transferWithAuthorizationTypeHash, + ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes32'], + [from, to, value, validAfter, validBefore, nonce] + ) +} + +function calculateEIP712Digest(domainSeparator, typeHash, types, parameters) { + return web3.utils.keccak256( '0x1901' + strip0x(domainSeparator) + strip0x(web3.utils.keccak256(web3.eth.abi.encodeParameters(['bytes32', ...types], [typeHash, ...parameters]))) ) - - return ecSign(digest, privateKey) } function makeDomainSeparator(name, version, chainId, verifyingContract) { @@ -59,4 +66,6 @@ module.exports = { permitTypeHash, signTransferAuthorization, makeDomainSeparator, + calculatePermitDigest, + calculateTransferAuthorizationDigest, } diff --git a/test/0.4.24/helpers/sign_utils.js b/test/0.4.24/helpers/sign_utils.js deleted file mode 100644 index f05e4a503..000000000 --- a/test/0.4.24/helpers/sign_utils.js +++ /dev/null @@ -1,26 +0,0 @@ -const { ecsign } = require('ethereumjs-util') - -function hexStringFromBuffer(buf) { - return '0x' + buf.toString('hex') -} - -function bufferFromHexString(hex) { - return Buffer.from(strip0x(hex), 'hex') -} - -function strip0x(v) { - return v.replace(/^0x/, '') -} - -function ecSign(digest, privateKey) { - const { v, r, s } = ecsign(bufferFromHexString(digest), bufferFromHexString(privateKey)) - - return { v, r: hexStringFromBuffer(r), s: hexStringFromBuffer(s) } -} - -module.exports = { - hexStringFromBuffer, - strip0x, - ecSign, - bufferFromHexString, -} diff --git a/test/0.4.24/legacy-oracle.test.js b/test/0.4.24/legacy-oracle.test.js new file mode 100644 index 000000000..700ae3c3a --- /dev/null +++ b/test/0.4.24/legacy-oracle.test.js @@ -0,0 +1,255 @@ +const { contract, ethers, artifacts, web3 } = require('hardhat') +const { assert } = require('../helpers/assert') +const { impersonate } = require('../helpers/blockchain') +const { e9, e18, e27, toBN } = require('../helpers/utils') +const { legacyOracleFactory } = require('../helpers/factories') + +const OssifiableProxy = artifacts.require('OssifiableProxy') +const LegacyOracle = artifacts.require('LegacyOracle') +const MockLegacyOracle = artifacts.require('MockLegacyOracle') + +const LegacyOracleAbi = require('../../lib/abi/LegacyOracle.json') + +const { + deployAccountingOracleSetup, + initAccountingOracle, + EPOCHS_PER_FRAME, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + calcAccountingReportDataHash, + getAccountingReportDataItems, + computeTimestampAtSlot, + ZERO_HASH, + CONSENSUS_VERSION, + computeTimestampAtEpoch, +} = require('../0.8.9/oracle/accounting-oracle-deploy.test') + +const getReportFields = (override = {}) => ({ + consensusVersion: CONSENSUS_VERSION, + numValidators: 10, + clBalanceGwei: e9(320), + stakingModuleIdsWithNewlyExitedValidators: [1], + numExitedValidatorsByStakingModule: [3], + withdrawalVaultBalance: e18(1), + elRewardsVaultBalance: e18(2), + sharesRequestedToBurn: e18(3), + withdrawalFinalizationBatches: [1], + simulatedShareRate: e27(1), + isBunkerMode: true, + extraDataFormat: 0, + extraDataHash: ZERO_HASH, + extraDataItemsCount: 0, + ...override, +}) + +const oldGetCurrentEpochId = (timestamp) => { + return toBN(timestamp) + .sub(toBN(GENESIS_TIME)) + .div(toBN(SLOTS_PER_EPOCH).mul(toBN(SECONDS_PER_SLOT))) +} + +async function deployLegacyOracleWithAccountingOracle({ admin, initialEpoch = 1, lastProcessingRefSlot = 31 }) { + const legacyOracle = await legacyOracleFactory({ appManager: { address: admin } }) + const { locatorAddr, consensus, oracle, lido } = await deployAccountingOracleSetup(admin, { + initialEpoch, + legacyOracleAddrArg: legacyOracle.address, + getLegacyOracle: () => { + return legacyOracle + }, + }) + await legacyOracle.initialize(locatorAddr, consensus.address) + await initAccountingOracle({ admin, oracle, consensus, shouldMigrateLegacyOracle: false, lastProcessingRefSlot }) + return { legacyOracle, consensus, accountingOracle: oracle, lido } +} + +module.exports = { + deployLegacyOracleWithAccountingOracle, +} + +contract('LegacyOracle', ([admin, stranger]) => { + context('Fresh deploy and puppet methods checks', () => { + let legacyOracle, accountingOracle, lido + before('deploy', async () => { + const deployed = await deployLegacyOracleWithAccountingOracle({ admin }) + legacyOracle = deployed.legacyOracle + accountingOracle = deployed.accountingOracle + lido = deployed.lido + }) + + it('initial state is correct', async () => { + assert.equals(await legacyOracle.getVersion(), 4) + assert.equals(await legacyOracle.getAccountingOracle(), accountingOracle.address) + assert.equals(await legacyOracle.getLido(), lido.address) + const spec = await legacyOracle.getBeaconSpec() + assert.equals(spec.epochsPerFrame, EPOCHS_PER_FRAME) + assert.equals(spec.slotsPerEpoch, SLOTS_PER_EPOCH) + assert.equals(spec.secondsPerSlot, SECONDS_PER_SLOT) + assert.equals(spec.genesisTime, GENESIS_TIME) + assert.equals(await legacyOracle.getLastCompletedEpochId(), 0) + }) + + it('handlePostTokenRebase performs AC, emits event and changes state', async () => { + await impersonate(ethers.provider, lido.address) + await assert.reverts( + legacyOracle.handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7, { from: stranger }), + 'SENDER_NOT_ALLOWED' + ) + const tx = await legacyOracle.handlePostTokenRebase(1, 2, 3, 4, 5, 6, 7, { from: lido.address, gasPrice: 0 }) + assert.emits(tx, 'PostTotalShares', { + postTotalPooledEther: 6, + preTotalPooledEther: 4, + timeElapsed: 2, + totalShares: 5, + }) + const delta = await legacyOracle.getLastCompletedReportDelta() + assert.equals(delta.postTotalPooledEther, 6) + assert.equals(delta.preTotalPooledEther, 4) + assert.equals(delta.timeElapsed, 2) + }) + + it('handleConsensusLayerReport performs AC, emits event and changes state', async () => { + const refSlot = 3000 + await impersonate(ethers.provider, accountingOracle.address) + await assert.reverts( + legacyOracle.handleConsensusLayerReport(refSlot, 2, 3, { from: stranger }), + 'SENDER_NOT_ALLOWED' + ) + const tx = await legacyOracle.handleConsensusLayerReport(refSlot, 2, 3, { + from: accountingOracle.address, + gasPrice: 0, + }) + const epochId = Math.floor((refSlot + 1) / SLOTS_PER_EPOCH) + assert.emits(tx, 'Completed', { + epochId, + beaconBalance: 2, + beaconValidators: 3, + }) + const completedEpoch = await legacyOracle.getLastCompletedEpochId() + assert.equals(completedEpoch, epochId) + }) + }) + + context('getCurrentEpochId implementation is correct', () => { + let legacyOracle, consensus, oracle, locatorAddr + + before('deploy time-travelable mock', async () => { + const implementation = await MockLegacyOracle.new({ from: admin }) + const proxy = await OssifiableProxy.new(implementation.address, admin, '0x') + legacyOracle = await MockLegacyOracle.at(proxy.address) + ;({ consensus, oracle, locatorAddr } = await deployAccountingOracleSetup(admin, { + legacyOracleAddrArg: legacyOracle.address, + getLegacyOracle: () => { + return legacyOracle + }, + dataSubmitter: admin, + })) + await legacyOracle.initialize(locatorAddr, consensus.address) + await initAccountingOracle({ + admin, + oracle, + consensus, + shouldMigrateLegacyOracle: false, + lastProcessingRefSlot: 0, + }) + }) + + it('test', async () => { + for (let index = 0; index < 20; index++) { + assert.equals(await legacyOracle.getCurrentEpochId(), oldGetCurrentEpochId(await consensus.getTime())) + await consensus.advanceTimeByEpochs(1) + } + }) + }) + + context('Migration from old contract', () => { + const lastCompletedEpoch = 10 + let oldImplementation + let newImplementation + let proxy + let proxyAsOldImplementation + let proxyAsNewImplementation + let deployedInfra + + before('deploy old implementation and set as proxy', async () => { + oldImplementation = await MockLegacyOracle.new({ from: admin }) + newImplementation = await LegacyOracle.new({ from: admin }) + proxy = await OssifiableProxy.new(oldImplementation.address, admin, '0x') + proxyAsOldImplementation = await MockLegacyOracle.at(proxy.address) + }) + + it('implementations are petrified', async () => { + await assert.reverts(oldImplementation.initialize(stranger, stranger), 'INIT_ALREADY_INITIALIZED') + await assert.reverts(newImplementation.initialize(stranger, stranger), 'INIT_ALREADY_INITIALIZED') + }) + + it('set state to mimic legacy oracle', async () => { + await proxyAsOldImplementation.initializeAsV3() + await proxyAsOldImplementation.setParams( + EPOCHS_PER_FRAME, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + lastCompletedEpoch + ) + }) + + it('deploy&initialize all contracts', async () => { + deployedInfra = await deployAccountingOracleSetup(admin, { + legacyOracleAddrArg: proxy.address, + getLegacyOracle: () => { + return proxyAsOldImplementation + }, + dataSubmitter: admin, + }) + const { consensus, oracle } = deployedInfra + await initAccountingOracle({ admin, oracle, consensus, shouldMigrateLegacyOracle: true }) + }) + + it('upgrade implementation', async () => { + await proxy.proxy__upgradeTo(newImplementation.address) + proxyAsNewImplementation = await LegacyOracle.at(proxy.address) + await proxyAsNewImplementation.finalizeUpgrade_v4(deployedInfra.oracle.address) + }) + + it('submit report', async () => { + await deployedInfra.consensus.advanceTimeToNextFrameStart() + 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()) + 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 } + ) + 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() + assert.equals(frameEpochId, Math.floor((refSlot + 1) / SLOTS_PER_EPOCH)) + assert.equals(frameStartTime, computeTimestampAtSlot(refSlot + 1)) + assert.equals(frameEndTime, computeTimestampAtEpoch(+frameEpochId + EPOCHS_PER_FRAME) - 1) + }) + + it.skip('handlePostTokenRebase from lido') + }) +}) diff --git a/test/0.4.24/legacyoracle.test.js b/test/0.4.24/legacyoracle.test.js deleted file mode 100644 index 4db9b947c..000000000 --- a/test/0.4.24/legacyoracle.test.js +++ /dev/null @@ -1,5 +0,0 @@ -const { contract } = require('hardhat') - -contract('LegacyOracle', () => { - it.skip('TODO: legacy compat tests', async () => {}) -}) diff --git a/test/0.4.24/lido-deposit-scenarios.test.js b/test/0.4.24/lido-deposit-scenarios.test.js index 182e941c8..3cb12ed8f 100644 --- a/test/0.4.24/lido-deposit-scenarios.test.js +++ b/test/0.4.24/lido-deposit-scenarios.test.js @@ -9,7 +9,7 @@ const { PUBKEY_LENGTH, FakeValidatorKeys, SIGNATURE_LENGTH } = require('../helpe const { GenericStub } = require('../helpers/stubs/generic.stub') contract('Lido deposit scenarios', ([staker, depositor]) => { - const CURATED_MODULE_ID = 1 + const STAKING_MODULE_ID = 1 const DEPOSIT_CALLDATA = '0x0' let lido, stakingRouter let stakingModuleStub, depositContractStub @@ -59,14 +59,14 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(lido, initialLidoETHBalance + unaccountedLidoETHBalance) assert.equal(await getBalance(lido), initialLidoETHBalance + unaccountedLidoETHBalance) - const availableValidatorsCount = 2 + const depositableValidatorsCount = 2 await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { totalExitedValidators: 5, totalDepositedValidators: 16, - availableValidatorsCount, + depositableValidatorsCount, }) - const depositDataLength = availableValidatorsCount + const depositDataLength = depositableValidatorsCount await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { return: { depositDataLength }, }) @@ -77,10 +77,10 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { assert.equal(await getBalance(lido), initialLidoETHBalance + unaccountedLidoETHBalance + submitAmount) const maxDepositsCount = 10 - await lido.deposit(maxDepositsCount, CURATED_MODULE_ID, DEPOSIT_CALLDATA, { from: 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, availableValidatorsCount) + const depositedEther = wei`32 ether` * wei.min(maxDepositsCount, depositableValidatorsCount) assert.equals( await getBalance(lido), initialLidoETHBalance + unaccountedLidoETHBalance + submitAmount - depositedEther @@ -93,14 +93,14 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(stakingRouter, initialStakingRouterBalance) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const availableValidatorsCount = 2 + const depositableValidatorsCount = 2 await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { totalExitedValidators: 5, totalDepositedValidators: 16, - availableValidatorsCount, + depositableValidatorsCount, }) - const depositDataLength = availableValidatorsCount + 2 + const depositDataLength = depositableValidatorsCount + 2 await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { return: { depositDataLength }, }) @@ -114,9 +114,9 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { const maxDepositsCount = 10 await assert.reverts( - lido.deposit(maxDepositsCount, CURATED_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), + lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'InvalidPublicKeysBatchLength', - [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * availableValidatorsCount] + [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * depositableValidatorsCount] ) }) @@ -125,19 +125,19 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(stakingRouter, initialStakingRouterBalance) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const availableValidatorsCount = 2 + const depositableValidatorsCount = 2 await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { totalExitedValidators: 5, totalDepositedValidators: 16, - availableValidatorsCount, + depositableValidatorsCount, }) - const depositDataLength = availableValidatorsCount + 2 + const depositDataLength = depositableValidatorsCount + 2 const depositData = new FakeValidatorKeys(depositDataLength) await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { return: { publicKeysBatch: depositData.slice()[0], // two extra signatures returned - signaturesBatch: depositData.slice(0, availableValidatorsCount)[1], + signaturesBatch: depositData.slice(0, depositableValidatorsCount)[1], }, }) @@ -150,9 +150,9 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { const maxDepositsCount = 10 await assert.reverts( - lido.deposit(maxDepositsCount, CURATED_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), + lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'InvalidPublicKeysBatchLength', - [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * availableValidatorsCount] + [PUBKEY_LENGTH * depositDataLength, PUBKEY_LENGTH * depositableValidatorsCount] ) }) @@ -161,18 +161,18 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { await setBalance(stakingRouter, initialStakingRouterBalance) assert.equals(await getBalance(stakingRouter), initialStakingRouterBalance) - const availableValidatorsCount = 2 + const depositableValidatorsCount = 2 await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { totalExitedValidators: 5, totalDepositedValidators: 16, - availableValidatorsCount, + depositableValidatorsCount, }) - const depositDataLength = availableValidatorsCount + 2 + const depositDataLength = depositableValidatorsCount + 2 const depositData = new FakeValidatorKeys(depositDataLength) await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { return: { - publicKeysBatch: depositData.slice(0, availableValidatorsCount)[0], + publicKeysBatch: depositData.slice(0, depositableValidatorsCount)[0], signaturesBatch: depositData.slice()[1], // two extra signatures returned }, }) @@ -186,9 +186,9 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { const maxDepositsCount = 10 await assert.reverts( - lido.deposit(maxDepositsCount, CURATED_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), + lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'InvalidSignaturesBatchLength', - [SIGNATURE_LENGTH * depositDataLength, SIGNATURE_LENGTH * availableValidatorsCount] + [SIGNATURE_LENGTH * depositDataLength, SIGNATURE_LENGTH * depositableValidatorsCount] ) }) @@ -204,19 +204,19 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { assert.equal(await getBalance(lido), initialLidoETHBalance + submitAmount) - const availableValidatorsCount = 2 + const depositableValidatorsCount = 2 await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { totalExitedValidators: 5, totalDepositedValidators: 16, - availableValidatorsCount, + depositableValidatorsCount, }) - const depositDataLength = availableValidatorsCount + const depositDataLength = depositableValidatorsCount await StakingModuleStub.stubObtainDepositData(stakingModuleStub, { return: { depositDataLength }, }) const maxDepositsCount = 10 - await assert.reverts(lido.deposit(maxDepositsCount, CURATED_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor })) + await assert.reverts(lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor })) }) it('StakingModule reverted on obtainData', async () => { @@ -226,11 +226,11 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { assert.equal(await getBalance(lido), initialLidoETHBalance + submitAmount) - const availableValidatorsCount = 2 + const depositableValidatorsCount = 2 await StakingModuleStub.stubGetStakingModuleSummary(stakingModuleStub, { totalExitedValidators: 5, totalDepositedValidators: 16, - availableValidatorsCount, + depositableValidatorsCount, }) await StakingModuleStub.stub(stakingModuleStub, 'obtainDepositData', { @@ -239,9 +239,31 @@ contract('Lido deposit scenarios', ([staker, depositor]) => { const maxDepositsCount = 10 await assert.reverts( - lido.deposit(maxDepositsCount, CURATED_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), + lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }), 'INVALID_ALLOCATED_KEYS_COUNT' ) }) + + it('Zero deposit updates lastDepositAt and lastDepositBlock fields', async () => { + 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 + await lido.deposit(maxDepositsCount, STAKING_MODULE_ID, DEPOSIT_CALLDATA, { from: depositor }) + + const stakingModuleStateAfter = await stakingRouter.getStakingModule(STAKING_MODULE_ID) + + assert.notEquals(stakingModuleStateBefore.lastDepositAt, stakingModuleStateAfter.lastDepositAt) + assert.notEquals(stakingModuleStateBefore.lastDepositBlock, stakingModuleStateAfter.lastDepositBlock) + }) }) }) diff --git a/test/0.4.24/lido-handle-oracle-report.test.js b/test/0.4.24/lido-handle-oracle-report.test.js index ca89fbc6e..6ff1e8a65 100644 --- a/test/0.4.24/lido-handle-oracle-report.test.js +++ b/test/0.4.24/lido-handle-oracle-report.test.js @@ -1,9 +1,25 @@ const { artifacts, contract, ethers } = require('hardhat') const { assert } = require('../helpers/assert') -const { ETH, toBN, genKeys, StETH, calcSharesMintedAsFees } = require('../helpers/utils') +const { + e9, + shareRate, + ETH, + toBN, + genKeys, + StETH, + calcSharesMintedAsFees, + calcShareRateDeltaE27, + limitRebase, +} = require('../helpers/utils') const { deployProtocol } = require('../helpers/protocol') -const { EvmSnapshot, setBalance } = require('../helpers/blockchain') +const { + EvmSnapshot, + setBalance, + advanceChainTime, + getCurrentBlockTimestamp, + getBalance, +} = require('../helpers/blockchain') const { ZERO_ADDRESS, INITIAL_HOLDER } = require('../helpers/constants') const { setupNodeOperatorsRegistry } = require('../helpers/staking-modules') @@ -15,14 +31,26 @@ const ORACLE_REPORT_LIMITS_BOILERPLATE = { churnValidatorsPerDayLimit: 255, oneOffCLBalanceDecreaseBPLimit: 100, annualBalanceIncreaseBPLimit: 10000, - simulatedShareRateDeviationBPLimit: 10000, + simulatedShareRateDeviationBPLimit: 1, maxValidatorExitRequestsPerReport: 10000, maxAccountingExtraDataListItemsCount: 10000, maxNodeOperatorsPerExtraDataItemCount: 10000, - requestTimestampMargin: 0, + requestTimestampMargin: 24, maxPositiveTokenRebase: 1000000000, } +const DEFAULT_LIDO_ORACLE_REPORT = { + reportTimestamp: 0, // uint256, seconds + timeElapsed: 0, // uint256, seconds + clValidators: 0, // uint256, counter + postCLBalance: ETH(0), // uint256, wei + withdrawalVaultBalance: ETH(0), // uint256, wei + elRewardsVaultBalance: ETH(0), // uint256, wei + sharesRequestedToBurn: StETH(0), // uint256, wad + withdrawalFinalizationBatches: [], // uint256, index + simulatedShareRate: shareRate(0), // uint256, 10e27 +} + const checkEvents = async ({ tx, reportTimestamp = 0, @@ -85,8 +113,8 @@ const checkEvents = async ({ ) } -contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, anotherStranger, depositor, operator]) => { - let deployed, snapshot, lido, treasury, voting, oracle +contract('Lido: handleOracleReport', ([appManager, , , , , , bob, stranger, anotherStranger, depositor, operator]) => { + let deployed, snapshot, lido, treasury, voting, oracle, burner, withdrawalQueue let curatedModule, oracleReportSanityChecker, elRewardsVault let withdrawalVault let strangerBalanceBefore, @@ -125,12 +153,14 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another await curatedModule.setNodeOperatorStakingLimit(0, keysAmount, { from: deployed.voting.address }) lido = deployed.pool + burner = deployed.burner treasury = deployed.treasury.address voting = deployed.voting.address oracle = deployed.oracle.address oracleReportSanityChecker = deployed.oracleReportSanityChecker withdrawalVault = deployed.withdrawalVault.address elRewardsVault = deployed.elRewardsVault.address + withdrawalQueue = deployed.withdrawalQueue assert.equals(await lido.balanceOf(INITIAL_HOLDER), StETH(1)) await lido.submit(ZERO_ADDRESS, { from: stranger, value: ETH(30) }) @@ -212,16 +242,22 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another } it('handleOracleReport access control', async () => { - await assert.reverts(lido.handleOracleReport(0, 0, 0, 0, 0, 0, 0, 0, { from: stranger }), 'APP_AUTH_FAILED') + await assert.reverts( + lido.handleOracleReport(...Object.values(DEFAULT_LIDO_ORACLE_REPORT), { from: stranger }), + 'APP_AUTH_FAILED' + ) }) - it('handleOracleReport reverts whe protocol stopped', async () => { + it('handleOracleReport reverts when protocol is stopped', async () => { await lido.stop({ from: deployed.voting.address }) - await assert.reverts(lido.handleOracleReport(0, 0, 0, 0, 0, 0, 0, 0, { from: stranger }), 'CONTRACT_IS_STOPPED') + await assert.reverts( + lido.handleOracleReport(...Object.values(DEFAULT_LIDO_ORACLE_REPORT), { from: stranger }), + 'CONTRACT_IS_STOPPED' + ) }) it('zero report should do nothing', async () => { - const tx = await lido.handleOracleReport(0, 0, 0, 0, 0, 0, 0, 0, { from: oracle }) + const tx = await lido.handleOracleReport(...Object.values(DEFAULT_LIDO_ORACLE_REPORT), { from: oracle }) await checkEvents({ tx, preCLValidators: 0, @@ -250,9 +286,9 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }) }) - describe('clBalance', () => { + describe('clBalance', async () => { beforeEach(async () => { - await await lido.deposit(3, 1, '0x', { from: depositor }) + await lido.deposit(3, 1, '0x', { from: depositor }) await checkStat({ depositedValidators: 3, beaconValidators: 0, beaconBalance: 0 }) await checkBalanceDeltas({ totalPooledEtherDiff: 0, @@ -265,7 +301,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }) it('first report after deposit without rewards', async () => { - const tx = await lido.handleOracleReport(0, 0, 1, ETH(32), 0, 0, 0, 0, { from: oracle }) + const tx = await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 1, postCLBalance: ETH(32) }), + { from: oracle } + ) await checkEvents({ tx, preCLValidators: 0, @@ -295,7 +334,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }) it('first report after deposit with rewards', async () => { - const tx = await lido.handleOracleReport(0, ONE_YEAR, 1, ETH(33), 0, 0, 0, 0, { from: oracle }) + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 1, + postCLBalance: ETH(33), + }), + { from: oracle } + ) const sharesMintedAsFees = calcSharesMintedAsFees(ETH(1), 10, 100, ETH(100), ETH(101)) await checkEvents({ tx, @@ -311,7 +358,7 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another preTotalEther: ETH(100), postTotalShares: toBN(ETH(100)).add(sharesMintedAsFees).toString(), postTotalEther: ETH(101), - sharesMintedAsFees, + sharesMintedAsFees: sharesMintedAsFees.toString(), }) await checkStat({ depositedValidators: 3, beaconValidators: 1, beaconBalance: ETH(33) }) @@ -329,38 +376,60 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another describe('sanity checks', async () => { beforeEach(async () => { - await await lido.deposit(3, 1, '0x', { from: depositor }) + await lido.deposit(3, 1, '0x', { from: depositor }) }) it('reverts on reported more than deposited', async () => { - await assert.reverts(lido.handleOracleReport(0, 0, 4, 0, 0, 0, 0, 0, { from: oracle }), 'REPORTED_MORE_DEPOSITED') + await assert.reverts( + lido.handleOracleReport(...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 4 }), { from: oracle }), + 'REPORTED_MORE_DEPOSITED' + ) }) it('reverts on reported less than reported previously', async () => { - await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await assert.reverts( - lido.handleOracleReport(0, 0, 2, 0, 0, 0, 0, 0, { from: oracle }), + lido.handleOracleReport(...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 2 }), { from: oracle }), 'REPORTED_LESS_VALIDATORS' ) }) it('withdrawal vault balance check', async () => { await assert.reverts( - lido.handleOracleReport(0, 0, 0, 0, 1, 0, 0, 0, { from: oracle }), + lido.handleOracleReport(...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, withdrawalVaultBalance: 1 }), { + from: oracle, + }), 'IncorrectWithdrawalsVaultBalance(0)' ) }) - it('withdrawal vault balance check 2', async () => { + it('execution layer rewards vault balance check', async () => { await assert.reverts( - lido.handleOracleReport(0, 0, 0, 0, 1, 0, 0, 0, { from: oracle }), - 'IncorrectWithdrawalsVaultBalance(0)' + lido.handleOracleReport(...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, elRewardsVaultBalance: 1 }), { + from: oracle, + }), + 'IncorrectELRewardsVaultBalance(0)' + ) + }) + + it('burner shares to burn check', async () => { + await assert.reverts( + lido.handleOracleReport(...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, sharesRequestedToBurn: 1 }), { + from: oracle, + }), + 'IncorrectSharesRequestedToBurn(0)' ) }) it('does not revert on new total balance stay the same', async () => { - let tx = await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + let tx = await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkEvents({ tx, preCLValidators: 0, @@ -386,7 +455,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another curatedModuleBalanceDiff: 0, initialHolderBalanceDiff: 0, }) - tx = await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + tx = await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkEvents({ tx, preCLValidators: 3, @@ -418,7 +490,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another // set oneOffCLBalanceDecreaseBPLimit = 1% await oracleReportSanityChecker.setOracleReportLimits(ORACLE_REPORT_LIMITS_BOILERPLATE, { from: voting }) - let tx = await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + let tx = await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkEvents({ tx, preCLValidators: 0, @@ -444,7 +519,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another curatedModuleBalanceDiff: 0, initialHolderBalanceDiff: 0, }) - tx = await lido.handleOracleReport(0, 0, 3, ETH(95.04), 0, 0, 0, 0, { from: oracle }) + tx = await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(95.04) }), + { from: oracle } + ) await checkEvents({ tx, preCLValidators: 3, @@ -477,7 +555,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another // set oneOffCLBalanceDecreaseBPLimit = 1% await oracleReportSanityChecker.setOracleReportLimits(ORACLE_REPORT_LIMITS_BOILERPLATE, { from: voting }) - await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: 0, @@ -488,7 +569,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another initialHolderBalanceDiff: 0, }) await assert.reverts( - lido.handleOracleReport(0, 0, 3, ETH(95.03), 0, 0, 0, 0, { from: oracle }), + lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(95.03) }), + { from: oracle } + ), 'IncorrectCLBalanceDecrease(101)' ) }) @@ -503,7 +587,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another { from: voting } ) - await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: 0, @@ -513,7 +600,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another curatedModuleBalanceDiff: 0, initialHolderBalanceDiff: 0, }) - const tx = await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96.96), 0, 0, 0, 0, { from: oracle }) + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96.96), + }), + { from: oracle } + ) const sharesMintedAsFees = calcSharesMintedAsFees(ETH(0.96), 10, 100, ETH(100), ETH(100.96)) await checkEvents({ tx, @@ -553,7 +648,10 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another { from: voting } ) - await lido.handleOracleReport(0, 0, 3, ETH(96), 0, 0, 0, 0, { from: oracle }) + await lido.handleOracleReport( + ...Object.values({ ...DEFAULT_LIDO_ORACLE_REPORT, clValidators: 3, postCLBalance: ETH(96) }), + { from: oracle } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: 0, @@ -564,7 +662,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another initialHolderBalanceDiff: 0, }) await assert.reverts( - lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96.97), 0, 0, 0, 0, { from: oracle }), + lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96.97), + }), + { from: oracle } + ), 'IncorrectCLBalanceIncrease(101)' ) }) @@ -581,7 +687,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 100, ETH(3200), 0, 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 100, + postCLBalance: ETH(3200), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 100, beaconValidators: 100, beaconBalance: ETH(3200) }) }) @@ -597,7 +711,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another { from: voting, gasPrice: 1 } ) await assert.reverts( - lido.handleOracleReport(0, ONE_DAY, 101, ETH(3200), 0, 0, 0, 0, { from: oracle, gasPrice: 1 }), + lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 101, + postCLBalance: ETH(3200), + }), + { from: oracle, gasPrice: 1 } + ), 'IncorrectAppearedValidators(101)' ) }) @@ -605,7 +727,7 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another describe('smooth report', async () => { beforeEach(async () => { - await await lido.deposit(3, 1, '0x', { from: depositor }) + await lido.deposit(3, 1, '0x', { from: depositor }) await checkStat({ depositedValidators: 3, beaconValidators: 0, beaconBalance: 0 }) await checkBalanceDeltas({ totalPooledEtherDiff: 0, @@ -625,7 +747,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(97), 0, 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(97), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(97) }) }) @@ -637,7 +767,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(100), 0, 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(100), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(100) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(4), @@ -660,7 +798,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96), ETH(1), 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96), + withdrawalVaultBalance: ETH(1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -684,7 +831,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96), ETH(1.1), 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96), + withdrawalVaultBalance: ETH(1.1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -709,7 +865,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96), 0, ETH(1), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96), + elRewardsVaultBalance: ETH(1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ @@ -735,7 +900,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(95.5), 0, ETH(1.5), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(95.5), + elRewardsVaultBalance: ETH(1.5), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(95.5) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -760,7 +934,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96), 0, ETH(1.1), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96), + elRewardsVaultBalance: ETH(1.1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -784,7 +967,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96.1), 0, ETH(0.9), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.9), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) await checkBalanceDeltas({ @@ -810,7 +1002,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_YEAR, 3, ETH(96.1), 0, ETH(1.1), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(1.1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -823,10 +1024,308 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0.2)) }) + + it('does not smooth shares to burn if report in limit with shares', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + + const sharesToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, // 1% + }, + { from: voting, gasPrice: 1 } + ) + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96), + sharesRequestedToBurn: sharesToBurn, + }), + { from: oracle, gasPrice: 1 } + ) + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: ETH(0), + postBufferedEther: ETH(5), + timeElapsed: ONE_YEAR, + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: toBN(ETH(101)).sub(sharesToBurn).toString(), + postTotalEther: ETH(101), + sharesMintedAsFees: 0, + }) + + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(1), // bob's deposit + treasuryBalanceDiff: ETH(0), // no rewards reported + strangerBalanceDiff: ETH(0.3), // though, bob has sacrificed stETH shares for all users + anotherStrangerBalanceDiff: ETH(0.69), + curatedModuleBalanceDiff: ETH(0), // no rewards reported + initialHolderBalanceDiff: ETH(0.01), + }) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(coverShares.add(nonCoverShares), StETH(0)) + assert.equals(await lido.balanceOf(burner.address), StETH(0)) + }) + + it('smooth shares to burn if report in limit without shares and no fees', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + await setBalance(elRewardsVault, ETH(0.5)) + + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, // 1% + }, + { from: voting, gasPrice: 1 } + ) + + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96), + elRewardsVaultBalance: ETH(0.5), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + + const { elBalanceUpdate, sharesToBurn } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0), + ETH(0.5), + sharesRequestedToBurn + ) + + const postTotalShares = toBN(ETH(101)).sub(toBN(sharesToBurn)) + const postTotalEther = toBN(ETH(101)).add(toBN(elBalanceUpdate)) + + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: ETH(0.5), + postBufferedEther: ETH(5.5), + timeElapsed: ONE_YEAR, + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: postTotalShares.toString(), + postTotalEther: postTotalEther.toString(), + sharesMintedAsFees: 0, // no rewards on CL side => no minted fee + }) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) + const shareRateDeltaE27 = calcShareRateDeltaE27(ETH(101), postTotalEther, ETH(101), postTotalShares) + + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(1.5), + treasuryBalanceDiff: ETH(0), // no fee minted + strangerBalanceDiff: shareRateDeltaE27.mul(toBN(30)).div(toBN(e9(1))), // though, bob has sacrificed stETH shares for all users + anotherStrangerBalanceDiff: shareRateDeltaE27.mul(toBN(69)).div(toBN(e9(1))), + curatedModuleBalanceDiff: ETH(0), // no fee minted + initialHolderBalanceDiff: shareRateDeltaE27.mul(toBN(1)).div(toBN(e9(1))), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0)) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(sharesRequestedToBurn.sub(coverShares.add(nonCoverShares)), sharesToBurn) + assert.equals( + await lido.balanceOf(burner.address), + await lido.getPooledEthByShares(toBN(sharesRequestedToBurn).sub(sharesToBurn)) + ) + }) + + it('smooth shares to burn if report in limit without shares and some fees', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + await setBalance(elRewardsVault, ETH(0.4)) + + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, // 1% + }, + { from: voting, gasPrice: 1 } + ) + + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.4), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + + const { elBalanceUpdate, sharesToBurn } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0.1), + ETH(0.4), + sharesRequestedToBurn + ) + + const postTotalEther = toBN(ETH(101.1)).add(toBN(elBalanceUpdate)) + const sharesMintedAsFees = calcSharesMintedAsFees(ETH(0.5), 10, 100, ETH(101), postTotalEther) + const postTotalShares = toBN(ETH(101)).add(sharesMintedAsFees).sub(toBN(sharesToBurn)) + + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96.1), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: ETH(0.4), + postBufferedEther: ETH(5.4), + timeElapsed: ONE_YEAR, + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: postTotalShares.toString(), + postTotalEther: postTotalEther.toString(), + sharesMintedAsFees: sharesMintedAsFees.toString(), + }) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) + const shareRateDeltaE27 = calcShareRateDeltaE27(ETH(101), postTotalEther, ETH(101), postTotalShares) + + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(1.5), + treasuryBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + strangerBalanceDiff: shareRateDeltaE27.mul(toBN(30)).div(toBN(e9(1))), + anotherStrangerBalanceDiff: shareRateDeltaE27.mul(toBN(69)).div(toBN(e9(1))), + curatedModuleBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + initialHolderBalanceDiff: shareRateDeltaE27.mul(toBN(1)).div(toBN(e9(1))), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0)) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(sharesRequestedToBurn.sub(coverShares.add(nonCoverShares)), sharesToBurn) + assert.equals( + await lido.balanceOf(burner.address), + await lido.getPooledEthByShares(toBN(sharesRequestedToBurn).sub(sharesToBurn)) + ) + }) + + it('postpone all shares to burn if report out of limit even without shares', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + await setBalance(elRewardsVault, ETH(4.9)) + + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, // 1% + }, + { from: voting, gasPrice: 1 } + ) + + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_YEAR, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(4.9), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + + const { elBalanceUpdate, sharesToBurn } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0.1), + ETH(4.9), + sharesRequestedToBurn + ) + + const postTotalEther = toBN(ETH(101.1)).add(toBN(elBalanceUpdate)) + const sharesMintedAsFees = calcSharesMintedAsFees( + toBN(ETH(0.1)).add(elBalanceUpdate), + 10, + 100, + ETH(101), + postTotalEther + ) + const postTotalShares = toBN(ETH(101)).add(sharesMintedAsFees).sub(toBN(sharesToBurn)) + + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96.1), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: elBalanceUpdate.toString(), + postBufferedEther: toBN(ETH(5)).add(elBalanceUpdate).toString(), + timeElapsed: ONE_YEAR, + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: postTotalShares.toString(), + postTotalEther: postTotalEther.toString(), + sharesMintedAsFees: sharesMintedAsFees.toString(), + }) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) + const shareRateDeltaE27 = calcShareRateDeltaE27(ETH(101), postTotalEther, ETH(101), postTotalShares) + + await checkBalanceDeltas({ + totalPooledEtherDiff: toBN(ETH(1.1)).add(elBalanceUpdate), + treasuryBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + strangerBalanceDiff: shareRateDeltaE27.mul(toBN(30)).div(toBN(e9(1))), + anotherStrangerBalanceDiff: shareRateDeltaE27.mul(toBN(69)).div(toBN(e9(1))), + curatedModuleBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + initialHolderBalanceDiff: shareRateDeltaE27.mul(toBN(1)).div(toBN(e9(1))), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), toBN(ETH(4.9)).sub(elBalanceUpdate)) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(sharesToBurn, 0) + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + assert.equals(await lido.balanceOf(burner.address), await lido.getPooledEthByShares(sharesRequestedToBurn)) + }) }) - describe('daily reports', () => { + + describe('daily reports', async () => { beforeEach(async () => { - await await lido.deposit(3, 1, '0x', { from: depositor }) + await lido.deposit(3, 1, '0x', { from: depositor }) await checkStat({ depositedValidators: 3, beaconValidators: 0, beaconBalance: 0 }) await checkBalanceDeltas({ totalPooledEtherDiff: 0, @@ -848,7 +1347,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96.1), 0, ETH(1.1), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(1.1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -869,7 +1377,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96.0027), 0, 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.0027), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.0027) }) }) @@ -882,7 +1398,15 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96 + 0.0028), 0, 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.0028), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96 + clIncrease) }) assert( 0.00014 + 0.0028 * 0.9 * 0.3 + 0.0028 * 0.9 * 0.69 + 0.0028 * 0.01 * 0.9 + 0.00014 === clIncrease, @@ -909,7 +1433,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96), ETH(1), 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96), + withdrawalVaultBalance: ETH(1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -933,7 +1466,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96), ETH(1.1), 0, 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96), + withdrawalVaultBalance: ETH(1.1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -958,7 +1500,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96), 0, ETH(1), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96), + elRewardsVaultBalance: ETH(1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ @@ -984,7 +1535,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(95.5), 0, ETH(1.5), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(95.5), + elRewardsVaultBalance: ETH(1.5), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(95.5) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -1009,7 +1569,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96), 0, ETH(1.1), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96), + elRewardsVaultBalance: ETH(1.1), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) await checkBalanceDeltas({ totalPooledEtherDiff: ETH(1), @@ -1033,7 +1602,16 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another }, { from: voting, gasPrice: 1 } ) - await lido.handleOracleReport(0, ONE_DAY, 3, ETH(96.1), 0, ETH(0.9), 0, 0, { from: oracle, gasPrice: 1 }) + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.9), + }), + { from: oracle, gasPrice: 1 } + ) await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) await checkBalanceDeltas({ @@ -1047,5 +1625,572 @@ contract('Lido: handleOracleReport', ([appManager, , , , , , , stranger, another assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0.1)) }) + + it('does not smooth shares to burn if report in limit with shares', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + + const sharesToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, + }, + { from: voting, gasPrice: 1 } + ) + + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96), + sharesRequestedToBurn: sharesToBurn, + }), + { from: oracle, gasPrice: 1 } + ) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(1), + treasuryBalanceDiff: 0, + strangerBalanceDiff: ETH(0.3), + anotherStrangerBalanceDiff: ETH(0.69), + curatedModuleBalanceDiff: 0, + initialHolderBalanceDiff: ETH(0.01), + }) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(coverShares.add(nonCoverShares), StETH(0)) + assert.equals(await lido.balanceOf(burner.address), StETH(0)) + }) + + it('smooth shares to burn if report in limit without shares and no fees', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + await setBalance(elRewardsVault, ETH(0.5)) + + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, + }, + { from: voting, gasPrice: 1 } + ) + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96), + elRewardsVaultBalance: ETH(0.5), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) + + const { elBalanceUpdate, sharesToBurn } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0), + ETH(0.5), + sharesRequestedToBurn + ) + + const postTotalShares = toBN(ETH(101)).sub(toBN(sharesToBurn)) + const postTotalEther = toBN(ETH(101)).add(toBN(elBalanceUpdate)) + + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: ETH(0.5), + postBufferedEther: ETH(5.5), + timeElapsed: ONE_DAY, // NB: day-long + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: postTotalShares.toString(), + postTotalEther: postTotalEther.toString(), + sharesMintedAsFees: 0, // no rewards on CL side => no minted fee + }) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96) }) + const shareRateDeltaE27 = calcShareRateDeltaE27(ETH(101), postTotalEther, ETH(101), postTotalShares) + + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(1.5), + treasuryBalanceDiff: ETH(0), // no fee minted + strangerBalanceDiff: shareRateDeltaE27.mul(toBN(30)).div(toBN(e9(1))), // though, bob has sacrificed stETH shares for all users + anotherStrangerBalanceDiff: shareRateDeltaE27.mul(toBN(69)).div(toBN(e9(1))), + curatedModuleBalanceDiff: ETH(0), // no fee minted + initialHolderBalanceDiff: shareRateDeltaE27.mul(toBN(1)).div(toBN(e9(1))), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0)) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(sharesRequestedToBurn.sub(coverShares.add(nonCoverShares)), sharesToBurn) + assert.equals( + await lido.balanceOf(burner.address), + await lido.getPooledEthByShares(toBN(sharesRequestedToBurn).sub(sharesToBurn)) + ) + }) + + it('smooth shares to burn if report in limit without shares and some fees', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + await setBalance(elRewardsVault, ETH(0.4)) + + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, // 1% + }, + { from: voting, gasPrice: 1 } + ) + + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.4), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + + const { elBalanceUpdate, sharesToBurn } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0.1), + ETH(0.4), + sharesRequestedToBurn + ) + + const postTotalEther = toBN(ETH(101.1)).add(toBN(elBalanceUpdate)) + const sharesMintedAsFees = calcSharesMintedAsFees(ETH(0.5), 10, 100, ETH(101), postTotalEther) + const postTotalShares = toBN(ETH(101)).add(sharesMintedAsFees).sub(toBN(sharesToBurn)) + + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96.1), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: ETH(0.4), + postBufferedEther: ETH(5.4), + timeElapsed: ONE_DAY, + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: postTotalShares.toString(), + postTotalEther: postTotalEther.toString(), + sharesMintedAsFees: sharesMintedAsFees.toString(), + }) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) + const shareRateDeltaE27 = calcShareRateDeltaE27(ETH(101), postTotalEther, ETH(101), postTotalShares) + + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(1.5), + treasuryBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + strangerBalanceDiff: shareRateDeltaE27.mul(toBN(30)).div(toBN(e9(1))), + anotherStrangerBalanceDiff: shareRateDeltaE27.mul(toBN(69)).div(toBN(e9(1))), + curatedModuleBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + initialHolderBalanceDiff: shareRateDeltaE27.mul(toBN(1)).div(toBN(e9(1))), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0)) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(sharesRequestedToBurn.sub(coverShares.add(nonCoverShares)), sharesToBurn) + assert.equals( + await lido.balanceOf(burner.address), + await lido.getPooledEthByShares(toBN(sharesRequestedToBurn).sub(sharesToBurn)) + ) + }) + + it('postpone all shares to burn if report out of limit without shares', async () => { + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + await setBalance(elRewardsVault, ETH(4.9)) + + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: 10000000, // 1% + }, + { from: voting, gasPrice: 1 } + ) + + const tx = await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(4.9), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + + const { elBalanceUpdate, sharesToBurn } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0.1), + ETH(4.9), + sharesRequestedToBurn + ) + + const postTotalEther = toBN(ETH(101.1)).add(toBN(elBalanceUpdate)) + const sharesMintedAsFees = calcSharesMintedAsFees( + toBN(ETH(0.1)).add(elBalanceUpdate), + 10, + 100, + ETH(101), + postTotalEther + ) + const postTotalShares = toBN(ETH(101)).add(sharesMintedAsFees).sub(toBN(sharesToBurn)) + + await checkEvents({ + tx, + preCLValidators: 0, + postCLValidators: 3, + preCLBalance: ETH(96), + postCLBalance: ETH(96.1), + withdrawalsWithdrawn: 0, + executionLayerRewardsWithdrawn: elBalanceUpdate.toString(), + postBufferedEther: toBN(ETH(5)).add(elBalanceUpdate).toString(), + timeElapsed: ONE_DAY, + preTotalShares: ETH(101), + preTotalEther: ETH(101), + postTotalShares: postTotalShares.toString(), + postTotalEther: postTotalEther.toString(), + sharesMintedAsFees: sharesMintedAsFees.toString(), + }) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) + const shareRateDeltaE27 = calcShareRateDeltaE27(ETH(101), postTotalEther, ETH(101), postTotalShares) + + await checkBalanceDeltas({ + totalPooledEtherDiff: toBN(ETH(1.1)).add(elBalanceUpdate), + treasuryBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + strangerBalanceDiff: shareRateDeltaE27.mul(toBN(30)).div(toBN(e9(1))), + anotherStrangerBalanceDiff: shareRateDeltaE27.mul(toBN(69)).div(toBN(e9(1))), + curatedModuleBalanceDiff: await lido.getPooledEthByShares(sharesMintedAsFees.div(toBN(2))), + initialHolderBalanceDiff: shareRateDeltaE27.mul(toBN(1)).div(toBN(e9(1))), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), toBN(ETH(4.9)).sub(elBalanceUpdate)) + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(sharesToBurn, 0) + assert.equals(coverShares.add(nonCoverShares), sharesRequestedToBurn) + assert.equals(await lido.balanceOf(burner.address), await lido.getPooledEthByShares(sharesRequestedToBurn)) + }) + }) + + describe('reports with withdrawals finalization', () => { + beforeEach(async () => { + await lido.deposit(3, 1, '0x', { from: depositor }) + await checkStat({ depositedValidators: 3, beaconValidators: 0, beaconBalance: 0 }) + await checkBalanceDeltas({ + totalPooledEtherDiff: 0, + treasuryBalanceDiff: 0, + strangerBalanceDiff: 0, + anotherStrangerBalanceDiff: 0, + curatedModuleBalanceDiff: 0, + initialHolderBalanceDiff: 0, + }) + }) + + it('dry-run eth_call works and returns proper values', async () => { + await setBalance(elRewardsVault, ETH(0.5)) + await setBalance(withdrawalVault, ETH(0.5)) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + churnValidatorsPerDayLimit: 100, + maxPositiveTokenRebase: 10000000, + }, + { from: voting, gasPrice: 1 } + ) + + const [postTotalPooledEther, postTotalShares, withdrawals, elRewards] = await lido.handleOracleReport.call( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.5), + withdrawalVaultBalance: ETH(0.5), + }), + { from: oracle, gasPrice: 1 } + ) + + await checkStat({ depositedValidators: 3, beaconValidators: 0, beaconBalance: ETH(0) }) + await checkBalanceDeltas({ + totalPooledEtherDiff: ETH(0), + treasuryBalanceDiff: ETH(0), + strangerBalanceDiff: ETH(0), + anotherStrangerBalanceDiff: ETH(0), + curatedModuleBalanceDiff: ETH(0), + initialHolderBalanceDiff: ETH(0), + }) + assert.equals(await ethers.provider.getBalance(elRewardsVault), ETH(0.5)) + assert.equals(await ethers.provider.getBalance(withdrawalVault), ETH(0.5)) + + const sharesMintedAsFees = calcSharesMintedAsFees(ETH(1), 10, 100, ETH(100), ETH(101)) + + assert.equals(postTotalPooledEther, ETH(101)) + assert.equals(postTotalShares, toBN(ETH(100)).add(sharesMintedAsFees)) + assert.equals(withdrawals, ETH(0.5)) + assert.equals(elRewards, ETH(0.4)) + }) + + it('withdrawal finalization works after dry-run call', async () => { + await setBalance(elRewardsVault, ETH(0.5)) + await setBalance(withdrawalVault, ETH(0.5)) + + assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), toBN(0)) + await withdrawalQueue.resume({ from: appManager }) + assert.isFalse(await withdrawalQueue.isPaused()) + + await lido.approve(withdrawalQueue.address, StETH(1), { from: stranger }) + await withdrawalQueue.requestWithdrawals([StETH(1)], stranger, { from: stranger }) + assert.equals(await withdrawalQueue.unfinalizedStETH(), StETH(1)) + assert.equals(await withdrawalQueue.unfinalizedRequestNumber(), 1) + + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + churnValidatorsPerDayLimit: 100, + maxPositiveTokenRebase: 10000000, + }, + { from: voting, gasPrice: 1 } + ) + + await advanceChainTime(30) + + const [postTotalPooledEther, postTotalShares, ,] = await lido.handleOracleReport.call( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.5), + withdrawalVaultBalance: ETH(0.5), + }), + { from: oracle, gasPrice: 1 } + ) + + await advanceChainTime(30) + + const simulatedShareRate = postTotalPooledEther.mul(toBN(shareRate(1))).div(postTotalShares) + const tooLowSimulatedShareRate = simulatedShareRate.mul(toBN(3)).div(toBN(4)) + + await assert.reverts( + lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + reportTimestamp: await getCurrentBlockTimestamp(), + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.5), + withdrawalVaultBalance: ETH(0.5), + withdrawalFinalizationBatches: [1], + simulatedShareRate: tooLowSimulatedShareRate, + }), + { from: oracle, gasPrice: 1 } + ), + `TooLowSimulatedShareRate(${tooLowSimulatedShareRate.toString()}, ${simulatedShareRate.toString()})` + ) + + const tooHighSimulatedShareRate = simulatedShareRate.mul(toBN(3)).div(toBN(2)) + + await assert.reverts( + lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + reportTimestamp: await getCurrentBlockTimestamp(), + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.5), + withdrawalVaultBalance: ETH(0.5), + withdrawalFinalizationBatches: [1], + simulatedShareRate: tooHighSimulatedShareRate, + }), + { from: oracle, gasPrice: 1 } + ), + `TooHighSimulatedShareRate(${tooHighSimulatedShareRate.toString()}, ${simulatedShareRate.toString()})` + ) + + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + reportTimestamp: await getCurrentBlockTimestamp(), + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.5), + withdrawalVaultBalance: ETH(0.5), + withdrawalFinalizationBatches: [1], + simulatedShareRate, + }), + { from: oracle, gasPrice: 1 } + ) + + assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), toBN(1)) + await withdrawalQueue.claimWithdrawal(1, { from: stranger }) + }) + + it('check simulated share rate correctness when limit is higher due to withdrawals', async () => { + // Execution layer rewards and withdrawal vault balance to report + // NB: both don't exceed daily rebase by themselves + await setBalance(elRewardsVault, ETH(0.25)) + await setBalance(withdrawalVault, ETH(0.5)) + + // Bob decides to burn stETH amount corresponding to ETH(1) + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1) }) + const sharesRequestedToBurn = await lido.sharesOf(bob) + await lido.approve(burner.address, await lido.balanceOf(bob), { from: bob }) + await burner.requestBurnShares(bob, sharesRequestedToBurn, { from: voting }) + + // Check that we haven't finalized anything yet + assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), toBN(0)) + await withdrawalQueue.resume({ from: appManager }) + assert.isFalse(await withdrawalQueue.isPaused()) + + // Stranger decides to withdraw his stETH(1) + await lido.approve(withdrawalQueue.address, StETH(1), { from: stranger }) + await withdrawalQueue.requestWithdrawals([StETH(1)], stranger, { from: stranger }) + assert.equals(await withdrawalQueue.unfinalizedStETH(), StETH(1)) + assert.equals(await withdrawalQueue.unfinalizedRequestNumber(), 1) + + // Setting daily positive rebase as 1% + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + churnValidatorsPerDayLimit: 100, + maxPositiveTokenRebase: 10000000, + }, + { from: voting, gasPrice: 1 } + ) + + await advanceChainTime(30) + + // Performing dry-run to estimate simulated share rate + const [postTotalPooledEther, postTotalShares, withdrawals, elRewards] = await lido.handleOracleReport.call( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.25), + withdrawalVaultBalance: ETH(0.5), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + const { elBalanceUpdate } = limitRebase( + toBN(10000000), + ETH(101), + ETH(101), + ETH(0.1), + ETH(0.5 + 0.25), + sharesRequestedToBurn + ) + assert.equals(withdrawals.add(elRewards), elBalanceUpdate) + // Ensuring that vaults don't hit the positive rebase limit + assert.equals(await getBalance(elRewardsVault), elRewards) + assert.equals(await getBalance(withdrawalVault), withdrawals) + const simulatedShareRate = postTotalPooledEther.mul(toBN(shareRate(1))).div(postTotalShares) + + await advanceChainTime(30) + + // Bob decides to stake in between reference slot and real report submission + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(1.137) }) + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(0.17) }) + await lido.submit(ZERO_ADDRESS, { from: bob, value: ETH(0.839) }) + + // Sending the real report with finalization attempts + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + reportTimestamp: await getCurrentBlockTimestamp(), + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0.25), + withdrawalVaultBalance: ETH(0.5), + sharesRequestedToBurn: sharesRequestedToBurn.toString(), + withdrawalFinalizationBatches: [1], + simulatedShareRate: simulatedShareRate.toString(), + }), + { from: oracle, gasPrice: 1 } + ) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) + + // Checking that both vaults are withdrawn + assert.equals(await getBalance(elRewardsVault), toBN(0)) + assert.equals(await getBalance(withdrawalVault), toBN(0)) + // But have excess shares to burn later + let { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + assert.isTrue(sharesRequestedToBurn.gt(coverShares.add(nonCoverShares))) + assert.isTrue(coverShares.add(nonCoverShares).gt(toBN(0))) + // Check total pooled ether + const totalPooledEtherAfterFinalization = await lido.getTotalPooledEther() + // Add Bob's recently staked funds, deduct finalized with 1:1 stranger's StETH(1) + assert.equals(totalPooledEtherAfterFinalization, postTotalPooledEther.add(toBN(ETH(1.137 + 0.17 + 0.839 - 1)))) + + // Checking that finalization of the previously placed withdrawal request completed + assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), toBN(1)) + const strangerBalanceBeforeClaim = await getBalance(stranger) + await withdrawalQueue.claimWithdrawal(1, { from: stranger, gasPrice: 0 }) + const strangerBalanceAfterClaim = await getBalance(stranger) + // Happy-path: user receive ETH corresponding to the requested StETH amount + assert.equals(strangerBalanceAfterClaim - strangerBalanceBeforeClaim, StETH(1)) + + // Reporting once again allowing shares to be burnt completely + await lido.handleOracleReport( + ...Object.values({ + ...DEFAULT_LIDO_ORACLE_REPORT, + reportTimestamp: await getCurrentBlockTimestamp(), + timeElapsed: ONE_DAY, + clValidators: 3, + postCLBalance: ETH(96.1), + elRewardsVaultBalance: ETH(0), + withdrawalVaultBalance: ETH(0), + sharesRequestedToBurn: coverShares.add(nonCoverShares).toString(), + }), + { from: oracle, gasPrice: 1 } + ) + await checkStat({ depositedValidators: 3, beaconValidators: 3, beaconBalance: ETH(96.1) }) + // Checking that no shares to burn remain + ;({ coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn()) + assert.equals(coverShares, toBN(0)) + assert.equals(nonCoverShares, toBN(0)) + }) }) }) diff --git a/test/0.4.24/lido.test.js b/test/0.4.24/lido.test.js index 57550ae92..92137d8c0 100644 --- a/test/0.4.24/lido.test.js +++ b/test/0.4.24/lido.test.js @@ -111,8 +111,8 @@ contract('Lido', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody }) const pushReport = async (clValidators, clBalance) => { - const elRewards = await web3.eth.getBalance(elRewardsVault.address) - await pushOracleReport(consensus, oracle, clValidators, clBalance, elRewards) + const elRewardsVaultBalance = await web3.eth.getBalance(elRewardsVault.address) + await pushOracleReport(consensus, oracle, clValidators, clBalance, elRewardsVaultBalance) await advanceChainTime(SECONDS_PER_FRAME + 1000) } @@ -381,14 +381,14 @@ contract('Lido', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody await app.setELRewardsWithdrawalLimit(initialValue, { from: voting }) // unable to receive execution layer rewards from arbitrary account - await assert.reverts(app.receiveELRewards({ from: user1, value: ETH(1) }), 'EXECUTION_LAYER_REWARDS_VAULT_ONLY') + await assert.reverts(app.receiveELRewards({ from: user1, value: ETH(1) })) }) }) }) describe('receiveELRewards()', async () => { it('unable to receive eth from arbitrary account', async () => { - await assert.reverts(app.receiveELRewards({ from: nobody, value: ETH(1) }), 'EXECUTION_LAYER_REWARDS_VAULT_ONLY') + await assert.reverts(app.receiveELRewards({ from: nobody, value: ETH(1) })) }) it('event work', async () => { @@ -486,7 +486,11 @@ contract('Lido', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody // +1 ETH await web3.eth.sendTransaction({ to: app.address, from: user1, value: ETH(1) }) - await app.methods['deposit(uint256,uint256,bytes)'](MAX_DEPOSITS, CURATED_MODULE_ID, CALLDATA, { from: depositor }) + // can not deposit with unset withdrawalCredentials even with O ETH deposit + await assert.reverts( + app.deposit(MAX_DEPOSITS, CURATED_MODULE_ID, CALLDATA, { from: depositor }), + 'EmptyWithdrawalsCredentials()' + ) await checkStat({ depositedValidators: 0, beaconValidators: 0, beaconBalance: ETH(0) }) assert.equals(await depositContract.totalCalls(), 0) assert.equals(await app.getTotalPooledEther(), ETH(2)) @@ -984,7 +988,7 @@ contract('Lido', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody await checkStat({ depositedValidators: 1, beaconValidators: 0, beaconBalance: ETH(0) }) await assert.reverts( - app.handleOracleReport(await getCurrentBlockTimestamp(), 1, ETH(30), 0, 0, 0, 0, 0, { from: appManager }), + app.handleOracleReport(await getCurrentBlockTimestamp(), 1, ETH(30), 0, 0, 0, 0, [], 0, { from: appManager }), 'APP_AUTH_FAILED' ) @@ -992,7 +996,7 @@ contract('Lido', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody await checkStat({ depositedValidators: 1, beaconValidators: 1, beaconBalance: ETH(30) }) await assert.reverts( - app.handleOracleReport(await getCurrentBlockTimestamp(), 1, ETH(29), 0, 0, 0, 0, 0, { from: nobody }), + app.handleOracleReport(await getCurrentBlockTimestamp(), 1, ETH(29), 0, 0, 0, 0, [], 0, { from: nobody }), 'APP_AUTH_FAILED' ) @@ -1634,7 +1638,7 @@ contract('Lido', ([appManager, , , , , , , , , , , , user1, user2, user3, nobody assert.equals(await app.sharesOf(user1), shares(0)) // voting can't continue burning if user already has no shares - await assert.reverts(app.burnShares(user1, 1, { from: voting }), 'BURN_AMOUNT_EXCEEDS_BALANCE') + await assert.reverts(app.burnShares(user1, 1, { from: voting }), 'BALANCE_EXCEEDED') }) context('treasury', () => { diff --git a/test/0.4.24/node-operators-registry-penalty.test.js b/test/0.4.24/node-operators-registry-penalty.test.js index 9c810fadf..5951e9ba8 100644 --- a/test/0.4.24/node-operators-registry-penalty.test.js +++ b/test/0.4.24/node-operators-registry-penalty.test.js @@ -99,7 +99,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use ]) // calls distributeRewards() inside - await app.testing__distributeRewards({ from: voting }) + await app.testing_distributeRewards({ from: voting }) const recipientsSharesAfter = await Promise.all([ steth.sharesOf(user1), @@ -117,7 +117,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use await steth.mintShares(app.address, ETH(10)) // calls distributeRewards() inside - await app.testing__distributeRewards({ from: voting }) + await app.testing_distributeRewards({ from: voting }) assert.equals(await steth.sharesOf(user1), ETH(3)) assert.equals(await steth.sharesOf(user2), ETH(7)) @@ -129,7 +129,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use await steth.mintShares(app.address, ETH(10)) // calls distributeRewards() inside - const receipt = await app.testing__distributeRewards({ from: voting }) + const receipt = await app.testing_distributeRewards({ from: voting }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(3) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(7) }) @@ -167,7 +167,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use // perValidatorShare 10*10^18 / 8 = 1250000000000000000 == 1.25 * 10^18 // calls distributeRewards() inside - const receipt = await app.testing__distributeRewards({ from: voting }) + const receipt = await app.testing_distributeRewards({ from: voting }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(2 * 1.25) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(6 * 1.25) }) @@ -207,7 +207,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use // but half goes to burner // calls distributeRewards() inside - const receipt = await app.testing__distributeRewards({ from: voting }) + const receipt = await app.testing_distributeRewards({ from: voting }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(1.25) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(6 * 1.25) }) @@ -226,7 +226,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use await app.updateRefundedValidatorsCount(firstNodeOperator, 1, { from: voting }) // calls distributeRewards() inside - const receipt = await app.testing__distributeRewards({ from: voting }) + const receipt = await app.testing_distributeRewards({ from: voting }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(1.25) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(6 * 1.25) }) @@ -258,7 +258,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use await app.updateRefundedValidatorsCount(firstNodeOperator, 1, { from: voting }) // calls distributeRewards() inside - const receipt = await app.testing__distributeRewards({ from: voting }) + const receipt = await app.testing_distributeRewards({ from: voting }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(1) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(4 * 2) }) assert.notEmits(receipt, 'RewardsDistributed', { rewardAddress: user3, sharesAmount: 0 }) @@ -284,7 +284,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, use assert.isFalse(await app.testing_isNodeOperatorPenalized(firstNodeOperator)) // calls distributeRewards() inside - const receipt = await app.testing__distributeRewards({ from: voting }) + const receipt = await app.testing_distributeRewards({ from: voting }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(2.5) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(7.5) }) diff --git a/test/0.4.24/node-operators-registry.test.js b/test/0.4.24/node-operators-registry.test.js index 65c2f2302..d9d3e602a 100644 --- a/test/0.4.24/node-operators-registry.test.js +++ b/test/0.4.24/node-operators-registry.test.js @@ -12,6 +12,7 @@ const signingKeys = require('../helpers/signing-keys') const { prepIdsCountsPayload, ETH, pad, hexConcat, toBN, padRight } = require('../helpers/utils') const { getRandomLocatorConfig } = require('../helpers/locator') const { ZERO_ADDRESS } = require('../helpers/constants') +const { wei } = require('../helpers/wei') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistryMock') const SigningKeys = artifacts.require('SigningKeys') @@ -65,9 +66,11 @@ const PENALTY_DELAY = 2 * 24 * 60 * 60 // 2 days const StETH = artifacts.require('StETHMock') -contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nobody]) => { +contract('NodeOperatorsRegistry', (addresses) => { let appBase, app, locator, steth, dao const snapshot = new EvmSnapshot(ethers.provider) + const [admin, limitsManager, nodeOperatorsManager, signingKeysManager, stakingRouter, user1, user2, user3, nobody] = + addresses before('deploy base app', async () => { // Deploy the app's base contract. @@ -78,15 +81,15 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) locator = await LidoLocator.new(locatorConfig) - dao = await AragonDAO.create(appManager) + dao = await AragonDAO.create(admin) app = await dao.newAppInstance({ name: 'node-operators-registry', base: appBase, permissions: { - MANAGE_SIGNING_KEYS: voting, - MANAGE_NODE_OPERATOR_ROLE: voting, - SET_NODE_OPERATOR_LIMIT_ROLE: voting, - STAKING_ROUTER_ROLE: voting, + MANAGE_SIGNING_KEYS: [admin, signingKeysManager], + MANAGE_NODE_OPERATOR_ROLE: [admin, nodeOperatorsManager], + SET_NODE_OPERATOR_LIMIT_ROLE: [admin, limitsManager], + STAKING_ROUTER_ROLE: [admin, stakingRouter], }, }) @@ -234,7 +237,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const exitedSigningKeysCount = NODE_OPERATORS.reduce((sum, c) => sum + c.exitedSigningKeysCount, 0) assert.equals(totalSigningKeysStatsAfter.totalSigningKeysCount.toNumber(), totalSigningKeysCount) - assert.equals(totalSigningKeysStatsAfter.vettedSigningKeysCount.toNumber(), vettedSigningKeysCount) + assert.equals(totalSigningKeysStatsAfter.maxValidatorsCount.toNumber(), vettedSigningKeysCount) assert.equals(totalSigningKeysStatsAfter.depositedSigningKeysCount.toNumber(), depositedSigningKeysCount) assert.equals(totalSigningKeysStatsAfter.exitedSigningKeysCount.toNumber(), exitedSigningKeysCount) }) @@ -361,51 +364,57 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('reverts with error "WRONG_NAME_LENGTH" when called with empty name', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) - await assert.reverts(app.addNodeOperator('', ADDRESS_1, { from: voting }), 'WRONG_NAME_LENGTH') + await assert.reverts(app.addNodeOperator('', ADDRESS_1, { from: nodeOperatorsManager }), 'WRONG_NAME_LENGTH') }) it('reverts with error "WRONG_NAME_LENGTH" when called with name length > MAX_NODE_OPERATOR_NAME_LENGTH', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) const maxNameLength = await app.MAX_NODE_OPERATOR_NAME_LENGTH() const tooLongName = '&'.repeat(maxNameLength.toNumber() + 1) - await assert.reverts(app.addNodeOperator(tooLongName, ADDRESS_1, { from: voting }), 'WRONG_NAME_LENGTH') + await assert.reverts( + app.addNodeOperator(tooLongName, ADDRESS_1, { from: nodeOperatorsManager }), + 'WRONG_NAME_LENGTH' + ) }) it('reverts with error "ZERO_ADDRESS" when called with zero reward address', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) const name = 'Node Operator #1' - await assert.reverts(app.addNodeOperator(name, ZERO_ADDRESS, { from: voting }), 'ZERO_ADDRESS') + await assert.reverts(app.addNodeOperator(name, ZERO_ADDRESS, { from: nodeOperatorsManager }), 'ZERO_ADDRESS') }) it('reverts with error "MAX_COUNT_EXCEEDED" when total count of node operators = MAX_COUNT_EXCEEDED', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) const maxNodeOperatorsCount = await app.MAX_NODE_OPERATORS_COUNT() for (let i = 0; i < maxNodeOperatorsCount; ++i) { - await app.addNodeOperator(`Node Operator #${i}`, ADDRESS_1, { from: voting }) + await app.addNodeOperator(`Node Operator #${i}`, ADDRESS_1, { from: nodeOperatorsManager }) } assert.equals(await app.getNodeOperatorsCount(), maxNodeOperatorsCount) - await assert.reverts(app.addNodeOperator(`exceeded`, ADDRESS_2, { from: voting }), 'MAX_OPERATORS_COUNT_EXCEEDED') + await assert.reverts( + app.addNodeOperator(`exceeded`, ADDRESS_2, { from: nodeOperatorsManager }), + 'MAX_OPERATORS_COUNT_EXCEEDED' + ) }) it('creates node operator with correct state', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) const name = `Node Operator #1` - await app.addNodeOperator(name, ADDRESS_1, { from: voting }) + await app.addNodeOperator(name, ADDRESS_1, { from: nodeOperatorsManager }) const expectedNodeOperatorId = 0 @@ -421,45 +430,49 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('returns correct node operator id', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) assert.equals(await app.getNodeOperatorsCount(), 0) const name = `Node Operator #1` - let expectedId = await app.methods['addNodeOperator(string,address)'].call(name, ADDRESS_1, { from: voting }) + let expectedId = await app.methods['addNodeOperator(string,address)'].call(name, ADDRESS_1, { + from: nodeOperatorsManager, + }) assert.equals(expectedId, 0) // create node operator to check that next id is correct - await app.addNodeOperator(name, ADDRESS_1, { from: voting }) + await app.addNodeOperator(name, ADDRESS_1, { from: nodeOperatorsManager }) - expectedId = await app.methods['addNodeOperator(string,address)'].call(name, ADDRESS_1, { from: voting }) + expectedId = await app.methods['addNodeOperator(string,address)'].call(name, ADDRESS_1, { + from: nodeOperatorsManager, + }) assert.equals(expectedId, 1) }) it('active & total operators count update correctly', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) assert.equals(await app.getNodeOperatorsCount(), 0) assert.equals(await app.getActiveNodeOperatorsCount(), 0) - await app.addNodeOperator(`Node Operator 1`, ADDRESS_1, { from: voting }) + await app.addNodeOperator(`Node Operator 1`, ADDRESS_1, { from: nodeOperatorsManager }) assert.equals(await app.getNodeOperatorsCount(), 1) assert.equals(await app.getActiveNodeOperatorsCount(), 1) }) it('emits NodeOperatorAdded events with correct params', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) assert.equals(await app.getNodeOperatorsCount(), 0) const name = `Node Operator 1` - const tx = await app.addNodeOperator(name, ADDRESS_1, { from: voting }) + const tx = await app.addNodeOperator(name, ADDRESS_1, { from: nodeOperatorsManager }) assert.emits( tx, @@ -472,9 +485,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob describe('activateNodeOperator()', () => { beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: admin }) const stakingModuleSummary = await app.getStakingModuleSummary() assert.equals(stakingModuleSummary.totalExitedValidators, 1) @@ -490,10 +503,10 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('reverts when called with non-existent operator id', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) const nodeOperatorId = Number.MAX_SAFE_INTEGER - await assert.reverts(app.activateNodeOperator(nodeOperatorId, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts(app.activateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }), 'OUT_OF_RANGE') }) it('reverts with WRONG_OPERATOR_ACTIVE_STATE when called on active node operator', async () => { @@ -503,7 +516,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.isTrue(activeNodeOperator.active) await assert.reverts( - app.activateNodeOperator(activeNodeOperatorId, { from: voting }), + app.activateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }), 'WRONG_OPERATOR_ACTIVE_STATE' ) }) @@ -516,7 +529,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob app.getNonce(), ]) assert.isFalse(nodeOperator.active) - await app.activateNodeOperator(nodeOperatorId, { from: voting }) + await app.activateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) assert.notEquals(nonceAfter, nonceBefore) @@ -527,7 +540,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const nodeOperator = await app.getNodeOperator(nodeOperatorId, false) const keysOpIndexBefore = await app.getKeysOpIndex() assert.isFalse(nodeOperator.active) - const receipt = await app.activateNodeOperator(nodeOperatorId, { from: voting }) + const receipt = await app.activateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }) const nonceAfter = await app.getNonce() assert.emits(receipt, 'KeysOpIndexSet', { keysOpIndex: keysOpIndexBefore.toNumber() + 1 }) assert.emits(receipt, 'NonceChanged', { nonce: nonceAfter }) @@ -539,7 +552,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.isFalse(notActiveNodeOperator.active) - await app.activateNodeOperator(notActiveNodeOperatorId, { from: voting }) + await app.activateNodeOperator(notActiveNodeOperatorId, { from: nodeOperatorsManager }) const nodeOperator = await app.getNodeOperator(notActiveNodeOperatorId, false) assert.isTrue(nodeOperator.active) @@ -551,7 +564,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const activeNodeOperatorsCountBefore = await app.getActiveNodeOperatorsCount() assert.isFalse(notActiveNodeOperator.active) - await app.activateNodeOperator(notActiveNodeOperatorId, { from: voting }) + await app.activateNodeOperator(notActiveNodeOperatorId, { from: nodeOperatorsManager }) const activeNodeOperatorsCountAfter = await app.getActiveNodeOperatorsCount() assert.equals(activeNodeOperatorsCountAfter.toNumber(), activeNodeOperatorsCountBefore.toNumber() + 1) @@ -560,7 +573,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits NodeOperatorActiveSet(activate) event', async () => { const nodeOperatorId = await nodeOperators.findNodeOperatorId(app, (operator) => !operator.active) assert.notEqual(nodeOperatorId, -1, `Invariant: not active node operator not found`) - const tx = await app.activateNodeOperator(nodeOperatorId, { from: voting }) + const tx = await app.activateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }) assert.emits( tx, 'NodeOperatorActiveSet', @@ -574,7 +587,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const nodeOperatorId = nodeOperatorsBefore.findIndex((operator) => !operator.active) assert.notEqual(nodeOperatorId, -1, `Invariant: not active node operator not found`) - await app.activateNodeOperator(nodeOperatorId, { from: voting }) + await app.activateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }) const nodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) @@ -586,7 +599,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const nodeOperatorId = nodeOperatorsBefore.findIndex((operator) => !operator.active) assert.notEqual(nodeOperatorId, -1, `Invariant: not active node operator not found`) - await app.activateNodeOperator(nodeOperatorId, { from: voting }) + await app.activateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }) const nodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) @@ -602,9 +615,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob describe('deactivateNodeOperator()', async () => { beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: admin }) const stakingModuleSummary = await app.getStakingModuleSummary() assert.equals(stakingModuleSummary.totalExitedValidators, 1) @@ -623,12 +636,12 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('reverts with "OUT_OF_RANGE" error when called with non-existent operator id', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'MANAGE_NODE_OPERATOR_ROLE') + const hasPermission = await dao.hasPermission(nodeOperatorsManager, app, 'MANAGE_NODE_OPERATOR_ROLE') assert.isTrue(hasPermission) const nodeOperatorId = Number.MAX_SAFE_INTEGER - await assert.reverts(app.deactivateNodeOperator(nodeOperatorId, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts(app.deactivateNodeOperator(nodeOperatorId, { from: nodeOperatorsManager }), 'OUT_OF_RANGE') }) it('reverts with "WRONG_OPERATOR_ACTIVE_STATE" when called on not active node operator', async () => { @@ -639,7 +652,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.isFalse(activeNodeOperator.active) await assert.reverts( - app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }), + app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }), 'WRONG_OPERATOR_ACTIVE_STATE' ) }) @@ -648,7 +661,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const activeNodeOperatorId = await nodeOperators.findNodeOperatorId(app, (operator) => operator.active) assert.notEqual(activeNodeOperatorId, -1, `Invariant: active node operator not found`) - await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) const nodeOperator = await app.getNodeOperator(activeNodeOperatorId, false) assert.isFalse(nodeOperator.active) @@ -660,7 +673,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const activeNodeOperatorsCountBefore = await app.getActiveNodeOperatorsCount() - await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) const activeNodeOperatorsCountAfter = await app.getActiveNodeOperatorsCount() assert.equals(activeNodeOperatorsCountAfter.toNumber(), activeNodeOperatorsCountBefore.toNumber() - 1) @@ -685,7 +698,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob +operatorReportBefore.totalDepositedValidators > 0, 'Invariant Failed: vettedSigningKeysCount === 0' ) - await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) const [operatorReportAfter, allValidatorsReportAfter] = await Promise.all([ app.getNodeOperatorSummary(activeNodeOperatorId), @@ -712,7 +725,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob 'invariant failed: readyToDepositValidatorsKeysCountBefore <= depositedSigningKeysCount' ) - const receipt = await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + const receipt = await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) assert.emits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: activeNodeOperatorId, approvedValidatorsCount: usedSigningKeys, @@ -730,7 +743,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ]) assert.isTrue(nodeOperator.active, 'Invariant Failed: not active') - await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) const [operatorReportAfter, allValidatorsReportAfter] = await Promise.all([ app.getNodeOperatorSummary(activeNodeOperatorId), @@ -748,7 +761,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const activeNodeOperatorId = await nodeOperators.findNodeOperatorId(app, (operator) => operator.active) assert.notEqual(activeNodeOperatorId, -1, `Invariant: active node operator not found`) - const receipt = await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + const receipt = await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) assert.emits(receipt, 'NodeOperatorActiveSet', { nodeOperatorId: activeNodeOperatorId, active: false }) }) @@ -757,7 +770,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const activeNodeOperatorId = await nodeOperators.findNodeOperatorId(app, (operator) => operator.active) assert.notEqual(activeNodeOperatorId, -1, `Invariant: active node operator not found`) const [keysOpIndexBefore, nonceBefore] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) - await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) assert.notEquals(nonceAfter, nonceBefore) @@ -767,7 +780,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const activeNodeOperatorId = await nodeOperators.findNodeOperatorId(app, (operator) => operator.active) assert.notEqual(activeNodeOperatorId, -1, `Invariant: active node operator not found`) const keysOpIndexBefore = await app.getKeysOpIndex() - const receipt = await app.deactivateNodeOperator(activeNodeOperatorId, { from: voting }) + const receipt = await app.deactivateNodeOperator(activeNodeOperatorId, { from: nodeOperatorsManager }) const nonceAfter = await app.getNonce() assert.emits(receipt, 'KeysOpIndexSet', { keysOpIndex: keysOpIndexBefore.toNumber() + 1 }) assert.emits(receipt, 'NonceChanged', { nonce: nonceAfter }) @@ -776,21 +789,24 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob describe('setNodeOperatorName()', async () => { beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: admin }) }) it('reverts with "OUT_OF_RANGE" error when called on non existent node operator', async () => { const notExitedNodeOperatorId = await app.getNodeOperatorsCount() await assert.reverts( - app.setNodeOperatorName(notExitedNodeOperatorId, 'new name', { from: voting }), + app.setNodeOperatorName(notExitedNodeOperatorId, 'new name', { from: nodeOperatorsManager }), 'OUT_OF_RANGE' ) }) it('reverts with "WRONG_NAME_LENGTH" error when called with empty name', async () => { const nodeOperatorId = 0 - await assert.reverts(app.setNodeOperatorName(nodeOperatorId, '', { from: voting }), 'WRONG_NAME_LENGTH') + await assert.reverts( + app.setNodeOperatorName(nodeOperatorId, '', { from: nodeOperatorsManager }), + 'WRONG_NAME_LENGTH' + ) }) it('reverts with "WRONG_NAME_LENGTH" error when name exceeds MAX_NODE_OPERATOR_NAME_LENGTH', async () => { @@ -798,7 +814,10 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const maxNameLength = await app.MAX_NODE_OPERATOR_NAME_LENGTH() const tooLongName = '#'.repeat(maxNameLength.toNumber() + 1) assert(tooLongName.length > maxNameLength.toNumber()) - await assert.reverts(app.setNodeOperatorName(nodeOperatorId, tooLongName, { from: voting }), 'WRONG_NAME_LENGTH') + await assert.reverts( + app.setNodeOperatorName(nodeOperatorId, tooLongName, { from: nodeOperatorsManager }), + 'WRONG_NAME_LENGTH' + ) }) it('reverts with "APP_AUTH_FAILED" error when called by address without MANAGE_NODE_OPERATOR_ROLE', async () => { @@ -810,13 +829,16 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('reverts with "VALUE_IS_THE_SAME" error when called with the same name', async () => { const nodeOperatorId = 0 const { name: currentName } = await app.getNodeOperator(nodeOperatorId, true) - await assert.reverts(app.setNodeOperatorName(nodeOperatorId, currentName, { from: voting }), 'VALUE_IS_THE_SAME') + await assert.reverts( + app.setNodeOperatorName(nodeOperatorId, currentName, { from: nodeOperatorsManager }), + 'VALUE_IS_THE_SAME' + ) }) it('updates the node operator name', async () => { const nodeOperatorId = 0 const newName = 'new name' - await app.setNodeOperatorName(nodeOperatorId, newName, { from: voting }) + await app.setNodeOperatorName(nodeOperatorId, newName, { from: nodeOperatorsManager }) const { name: nameAfter } = await app.getNodeOperator(nodeOperatorId, true) assert(nameAfter === newName) }) @@ -824,7 +846,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits NodeOperatorNameSet event with correct params', async () => { const nodeOperatorId = 0 const newName = 'new name' - const receipt = await app.setNodeOperatorName(nodeOperatorId, newName, { from: voting }) + const receipt = await app.setNodeOperatorName(nodeOperatorId, newName, { from: nodeOperatorsManager }) assert.emits( receipt, 'NodeOperatorNameSet', @@ -837,7 +859,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const anotherNodeOperatorId = 1 const newName = 'new name' const { name: anotherNodeOperatorNameBefore } = await app.getNodeOperator(anotherNodeOperatorId, true) - await app.setNodeOperatorName(nodeOperatorId, newName, { from: voting }) + await app.setNodeOperatorName(nodeOperatorId, newName, { from: nodeOperatorsManager }) const { name: anotherNodeOperatorNameAfter } = await app.getNodeOperator(anotherNodeOperatorId, true) assert.equals(anotherNodeOperatorNameBefore, anotherNodeOperatorNameAfter) }) @@ -849,20 +871,20 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const notExistedNodeOperatorId = 2 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) }) it('reverts with "OUT_OF_RANGE" error when called on non existent node operator', async () => { await assert.reverts( - app.setNodeOperatorRewardAddress(notExistedNodeOperatorId, ADDRESS_4, { from: voting }), + app.setNodeOperatorRewardAddress(notExistedNodeOperatorId, ADDRESS_4, { from: nodeOperatorsManager }), 'OUT_OF_RANGE' ) }) it('reverts with "ZERO_ADDRESS" error when new address is zero', async () => { await assert.reverts( - app.setNodeOperatorRewardAddress(firstNodeOperatorId, ZERO_ADDRESS, { from: voting }), + app.setNodeOperatorRewardAddress(firstNodeOperatorId, ZERO_ADDRESS, { from: nodeOperatorsManager }), 'ZERO_ADDRESS' ) }) @@ -879,7 +901,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it(`reverts with "VALUE_IS_THE_SAME" error when new reward address is the same`, async () => { const nodeOperator = await app.getNodeOperator(firstNodeOperatorId, false) await assert.reverts( - app.setNodeOperatorRewardAddress(firstNodeOperatorId, nodeOperator.rewardAddress, { from: voting }), + app.setNodeOperatorRewardAddress(firstNodeOperatorId, nodeOperator.rewardAddress, { + from: nodeOperatorsManager, + }), 'VALUE_IS_THE_SAME' ) }) @@ -887,13 +911,15 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('updates the reward address of the node operator', async () => { const { rewardAddress: rewardAddressBefore } = await app.getNodeOperator(firstNodeOperatorId, false) assert.notEqual(rewardAddressBefore, ADDRESS_4) - await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: voting }) + await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: nodeOperatorsManager }) const { rewardAddress: rewardAddressAfter } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(rewardAddressAfter, ADDRESS_4) }) it('emits "NodeOperatorRewardAddressSet" event with correct params', async () => { - const receipt = await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: voting }) + const receipt = await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { + from: nodeOperatorsManager, + }) assert.emits(receipt, 'NodeOperatorRewardAddressSet', { nodeOperatorId: firstNodeOperatorId, rewardAddress: ADDRESS_4, @@ -905,7 +931,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob secondNodeOperatorId, true ) - await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: voting }) + await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: nodeOperatorsManager }) const { rewardAddress: secondNodeOperatorRewardAddressAfter } = await app.getNodeOperator( secondNodeOperatorId, true @@ -923,12 +949,12 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await nodeOperators.addNodeOperator( app, { ...NODE_OPERATORS[0], totalSigningKeysCount: 100, vettedSigningKeysCount: 50, depositedSigningKeysCount: 20 }, - { from: voting } + { from: admin } ) await nodeOperators.addNodeOperator( app, { ...NODE_OPERATORS[1], totalSigningKeysCount: 50, vettedSigningKeysCount: 45, depositedSigningKeysCount: 30 }, - { from: voting } + { from: admin } ) }) @@ -942,39 +968,39 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('reverts with "OUT_OF_RANGE" error when called on non existent validator', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'SET_NODE_OPERATOR_LIMIT_ROLE') + const hasPermission = await dao.hasPermission(limitsManager, app, 'SET_NODE_OPERATOR_LIMIT_ROLE') assert.isTrue(hasPermission) await assert.reverts( - app.setNodeOperatorStakingLimit(notExistedNodeOperatorId, 40, { from: voting }), + app.setNodeOperatorStakingLimit(notExistedNodeOperatorId, 40, { from: limitsManager }), 'OUT_OF_RANGE' ) }) it('reverts with "WRONG_OPERATOR_ACTIVE_STATE" error when node operator deactivated', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'SET_NODE_OPERATOR_LIMIT_ROLE') + const hasPermission = await dao.hasPermission(limitsManager, app, 'SET_NODE_OPERATOR_LIMIT_ROLE') assert.isTrue(hasPermission) - await app.deactivateNodeOperator(secondNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(secondNodeOperatorId, { from: nodeOperatorsManager }) assert.isFalse(await app.getNodeOperatorIsActive(secondNodeOperatorId)) await assert.reverts( - app.setNodeOperatorStakingLimit(secondNodeOperatorId, 40, { from: voting }), + app.setNodeOperatorStakingLimit(secondNodeOperatorId, 40, { from: limitsManager }), 'WRONG_OPERATOR_ACTIVE_STATE' ) }) it('newStakingLimit < depositedSigningKeys :: sets staking limit to deposited signing keys count', async () => { - await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 10, { from: voting }) + await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 10, { from: limitsManager }) const nodeOperator = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(nodeOperator.stakingLimit, 20) }) it('newStakingLimit > totalSigningKeysCount :: sets staking limit to total signing keys count', async () => { - await app.setNodeOperatorStakingLimit(secondNodeOperatorId, 1000, { from: voting }) + await app.setNodeOperatorStakingLimit(secondNodeOperatorId, 1000, { from: limitsManager }) const nodeOperator = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(nodeOperator.stakingLimit, 50) }) it('depositedSigningKeys <= newStakingLimit <= totalSigningKeysCount :: sets staking limit to passed value', async () => { - await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 75, { from: voting }) + await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 75, { from: limitsManager }) const nodeOperator = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(nodeOperator.stakingLimit, 75) }) @@ -985,7 +1011,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob app.getNonce(), app.getKeysOpIndex(), ]) - const receipt = await app.setNodeOperatorStakingLimit(firstNodeOperatorId, stakingLimitBefore, { from: voting }) + const receipt = await app.setNodeOperatorStakingLimit(firstNodeOperatorId, stakingLimitBefore, { + from: limitsManager, + }) assert.notEmits(receipt, 'VettedSigningKeysCountChanged') const [{ stakingLimit: stakingLimitAfter }, nonceAfter, keysOpIndexAfter] = await Promise.all([ app.getNodeOperator(firstNodeOperatorId, false), @@ -998,22 +1026,24 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('reduces total vetted validator keys count correctly if new value less than previous', async () => { - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 30, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 30, { from: limitsManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(vettedSigningKeysCountBefore.toNumber() - vettedSigningKeysCountAfter.toNumber(), 20) }) it('increases total vetted validator keys count correctly if new value greater than previous', async () => { - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 100, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.setNodeOperatorStakingLimit(firstNodeOperatorId, 100, { from: limitsManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(vettedSigningKeysCountAfter.toNumber() - vettedSigningKeysCountBefore.toNumber(), 50) }) it('emits VettedSigningKeysCountChanged event with correct params', async () => { const newStakingLimit = 75 - const receipt = await app.setNodeOperatorStakingLimit(firstNodeOperatorId, newStakingLimit, { from: voting }) + const receipt = await app.setNodeOperatorStakingLimit(firstNodeOperatorId, newStakingLimit, { + from: limitsManager, + }) assert.emits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: firstNodeOperatorId, approvedValidatorsCount: newStakingLimit, @@ -1022,7 +1052,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('increases keysOpIndex & changes nonce on vettedSigningKeysCount change', async () => { const [keysOpIndexBefore, nonceBefore] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) - await app.setNodeOperatorStakingLimit(0, 40, { from: voting }) + await app.setNodeOperatorStakingLimit(0, 40, { from: limitsManager }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) assert.equals(nonceAfter, nonceBefore.toNumber() + 1) @@ -1030,7 +1060,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits KeysOpIndexSet & NonceChanged on vettedSigningKeysCount change', async () => { const keysOpIndexBefore = await app.getKeysOpIndex() - const receipt = await app.setNodeOperatorStakingLimit(0, 40, { from: voting }) + const receipt = await app.setNodeOperatorStakingLimit(0, 40, { from: limitsManager }) const nonceAfter = await app.getNonce() assert.emits(receipt, 'KeysOpIndexSet', { keysOpIndex: keysOpIndexBefore.toNumber() + 1 }) assert.emits(receipt, 'NonceChanged', { nonce: nonceAfter }) @@ -1041,7 +1071,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob secondNodeOperatorId, true ) - await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: voting }) + await app.setNodeOperatorRewardAddress(firstNodeOperatorId, ADDRESS_4, { from: nodeOperatorsManager }) const { stakingLimit: secondNodeOperatorStakingLimitAfter } = await app.getNodeOperator( secondNodeOperatorId, true @@ -1056,15 +1086,18 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const notExistedNodeOperatorId = 2 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], exitedSigningKeysCount: 3 }, { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], exitedSigningKeysCount: 3 }, { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) }) it('reverts with "OUT_OF_RANGE" error when called on non existent validator', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'STAKING_ROUTER_ROLE') + const hasPermission = await dao.hasPermission(stakingRouter, app, 'STAKING_ROUTER_ROLE') assert.isTrue(hasPermission) const { operatorIds, keysCounts } = prepIdsCountsPayload(notExistedNodeOperatorId, 40) - await assert.reverts(app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }), + 'OUT_OF_RANGE' + ) }) it('reverts with "APP_AUTH_FAILED" error when called by sender without STAKING_ROUTER_ROLE', async () => { @@ -1083,7 +1116,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob false ) const { operatorIds, keysCounts } = prepIdsCountsPayload(firstNodeOperatorId, exitedValidatorsKeysCountBefore) - await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) const { stoppedValidators: exitedValidatorsKeysCountAfter } = await app.getNodeOperator( firstNodeOperatorId, false @@ -1097,7 +1130,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob false ) const { operatorIds, keysCounts } = prepIdsCountsPayload(firstNodeOperatorId, exitedValidatorsKeysCountBefore) - const receipt = await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + const receipt = await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) assert.notEmits(receipt, 'ExitedSigningKeysCountChanged') }) @@ -1106,7 +1139,10 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const nodeOperator = await app.getNodeOperator(firstNodeOperatorId, false) assert(newExitedValidatorsCount > nodeOperator.usedSigningKeys.toNumber()) const { operatorIds, keysCounts } = prepIdsCountsPayload(firstNodeOperatorId, newExitedValidatorsCount) - await assert.reverts(app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }), + 'OUT_OF_RANGE' + ) }) it('reverts with "EXITED_VALIDATORS_COUNT_DECREASED" error when new exitedValidatorsKeysCount less then current one', async () => { @@ -1115,7 +1151,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const newExitedValidatorsKeysCount = nodeOperator.stoppedValidators.toNumber() - 1 const { operatorIds, keysCounts } = prepIdsCountsPayload(firstNodeOperatorId, newExitedValidatorsKeysCount) await assert.reverts( - app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }), + app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }), 'EXITED_VALIDATORS_COUNT_DECREASED' ) }) @@ -1128,7 +1164,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ) assert.notEquals(exitedValidatorsKeysCountBefore, newExitedValidatorsCount) const { operatorIds, keysCounts } = prepIdsCountsPayload(secondNodeOperatorId, newExitedValidatorsCount) - await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) const { stoppedValidators: exitedValidatorsKeysCountAfter } = await app.getNodeOperator( secondNodeOperatorId, false @@ -1146,7 +1182,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob app.testing_getTotalSigningKeysStats(), ]) const { operatorIds, keysCounts } = prepIdsCountsPayload(firstNodeOperatorId, newExitedValidatorsCount) - await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) const exitedSigningKeysCountIncrement = newExitedValidatorsCount - exitedValidatorsKeysCountBefore.toNumber() const { exitedSigningKeysCount: exitedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals( @@ -1158,7 +1194,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits ExitedSigningKeysCountChanged event with correct params', async () => { const newExitedValidatorsCount = 4 const { operatorIds, keysCounts } = prepIdsCountsPayload(firstNodeOperatorId, newExitedValidatorsCount) - const receipt = await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + const receipt = await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) assert.emits(receipt, 'ExitedSigningKeysCountChanged', { nodeOperatorId: firstNodeOperatorId, exitedValidatorsCount: newExitedValidatorsCount, @@ -1172,7 +1208,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob true ) const { operatorIds, keysCounts } = prepIdsCountsPayload(secondNodeOperatorId, newExitedValidatorsCount) - await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) const { stakingLimit: secondNodeOperatorStakingLimitAfter } = await app.getNodeOperator(firstNodeOperatorId, true) assert.equals(secondNodeOperatorStakingLimitAfter, secondNodeOperatorStakingLimitBefore) }) @@ -1186,20 +1222,20 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const stuckValidatorsCount = 2 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], exitedSigningKeysCount }, { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], exitedSigningKeysCount }, { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) }) it('decreases the stuck validators count when new value is less then previous one', async () => { const newStuckValidatorsCount = 1 await app.unsafeUpdateValidatorsCount(firstNodeOperatorId, exitedSigningKeysCount, stuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stuckValidatorsCount: stuckValidatorsCountBefore } = await app.getNodeOperatorSummary(firstNodeOperatorId) assert(newStuckValidatorsCount < stuckValidatorsCountBefore) await app.unsafeUpdateValidatorsCount(firstNodeOperatorId, exitedSigningKeysCount, newStuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stuckValidatorsCount: stuckValidatorsCountAfter } = await app.getNodeOperatorSummary(firstNodeOperatorId) assert.equals(stuckValidatorsCountAfter, newStuckValidatorsCount) @@ -1212,7 +1248,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ) assert(newStuckValidatorsCount > stuckValidatorsCountBefore) await app.unsafeUpdateValidatorsCount(secondNodeOperatorId, exitedSigningKeysCount, newStuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stuckValidatorsCount: stuckValidatorsCountAfter } = await app.getNodeOperatorSummary(secondNodeOperatorId) assert.equals(stuckValidatorsCountAfter, newStuckValidatorsCount) @@ -1224,7 +1260,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob firstNodeOperatorId, exitedSigningKeysCount, newStuckValidatorsCount, - { from: voting } + { from: stakingRouter } ) assert.emits(receipt, 'StuckPenaltyStateChanged', { nodeOperatorId: firstNodeOperatorId, @@ -1235,7 +1271,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it("doesn't change the state when new stuck validators value is equal to the previous one", async () => { const { stuckValidatorsCount: stuckValidatorsCountBefore } = await app.getNodeOperatorSummary(firstNodeOperatorId) await app.unsafeUpdateValidatorsCount(firstNodeOperatorId, exitedSigningKeysCount, stuckValidatorsCountBefore, { - from: voting, + from: stakingRouter, }) const { stuckValidatorsCount: stuckValidatorsCountAfter } = await app.getNodeOperatorSummary(firstNodeOperatorId) @@ -1249,7 +1285,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob exitedSigningKeysCount, stuckValidatorsCountBefore, { - from: voting, + from: stakingRouter, } ) assert.notEmits(receipt, 'StuckPenaltyStateChanged') @@ -1261,7 +1297,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob firstNodeOperatorId ) await app.unsafeUpdateValidatorsCount(secondNodeOperatorId, exitedSigningKeysCount, newStuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stuckValidatorsCount: secondNodeOperatorStuckValidatorsCountAfter } = await app.getNodeOperatorSummary( firstNodeOperatorId @@ -1275,17 +1311,17 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert(newStuckValidatorsCount > stuckValidatorsCount) await assert.reverts( app.unsafeUpdateValidatorsCount(firstNodeOperatorId, exitedSigningKeysCount, newStuckValidatorsCount, { - from: voting, + from: stakingRouter, }), 'OUT_OF_RANGE' ) }) it('reverts with "OUT_OF_RANGE" error when called on non existent validator', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'STAKING_ROUTER_ROLE') + const hasPermission = await dao.hasPermission(stakingRouter, app, 'STAKING_ROUTER_ROLE') assert.isTrue(hasPermission) await assert.reverts( - app.unsafeUpdateValidatorsCount(notExistedNodeOperatorId, 40, stuckValidatorsCount, { from: voting }), + app.unsafeUpdateValidatorsCount(notExistedNodeOperatorId, 40, stuckValidatorsCount, { from: stakingRouter }), 'OUT_OF_RANGE' ) }) @@ -1308,7 +1344,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob firstNodeOperatorId, exitedValidatorsKeysCountBefore, stuckValidatorsCount, - { from: voting } + { from: stakingRouter } ) const { stoppedValidators: exitedValidatorsKeysCountAfter } = await app.getNodeOperator( firstNodeOperatorId, @@ -1327,7 +1363,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob exitedValidatorsKeysCountBefore, stuckValidatorsCount, { - from: voting, + from: stakingRouter, } ) assert.notEmits(receipt, 'ExitedSigningKeysCountChanged') @@ -1339,7 +1375,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert(newExitedValidatorsCount > nodeOperator.usedSigningKeys.toNumber()) await assert.reverts( app.unsafeUpdateValidatorsCount(firstNodeOperatorId, newExitedValidatorsCount, stuckValidatorsCount, { - from: voting, + from: stakingRouter, }), 'OUT_OF_RANGE' ) @@ -1354,7 +1390,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ) assert(newExitedValidatorsCount < exitedValidatorsKeysCountBefore.toNumber()) await app.unsafeUpdateValidatorsCount(firstNodeOperatorId, newExitedValidatorsCount, newStuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stoppedValidators: exitedValidatorsKeysCountAfter } = await app.getNodeOperator( firstNodeOperatorId, @@ -1371,7 +1407,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ) assert(newExitedValidatorsCount > exitedValidatorsKeysCountBefore.toNumber()) await app.unsafeUpdateValidatorsCount(secondNodeOperatorId, newExitedValidatorsCount, stuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stoppedValidators: exitedValidatorsKeysCountAfter } = await app.getNodeOperator( secondNodeOperatorId, @@ -1388,7 +1424,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ] = await Promise.all([app.getNodeOperator(firstNodeOperatorId, false), app.testing_getTotalSigningKeysStats()]) assert(newExitedValidatorsCount < exitedValidatorsKeysCountBefore.toNumber()) await app.unsafeUpdateValidatorsCount(firstNodeOperatorId, newExitedValidatorsCount, stuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const exitedSigningKeysCountIncrement = exitedValidatorsKeysCountBefore.toNumber() - newExitedValidatorsCount const { exitedSigningKeysCount: exitedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() @@ -1407,7 +1443,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ] = await Promise.all([app.getNodeOperator(firstNodeOperatorId, false), app.testing_getTotalSigningKeysStats()]) assert(newExitedValidatorsCount > exitedValidatorsKeysCountBefore.toNumber()) await app.unsafeUpdateValidatorsCount(firstNodeOperatorId, newExitedValidatorsCount, newStuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const exitedSigningKeysCountIncrement = newExitedValidatorsCount - exitedValidatorsKeysCountBefore.toNumber() const { exitedSigningKeysCount: exitedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() @@ -1423,7 +1459,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob secondNodeOperatorId, newExitedValidatorsCount, stuckValidatorsCount, - { from: voting } + { from: stakingRouter } ) assert.emits(receipt, 'ExitedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId, @@ -1438,20 +1474,84 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob true ) await app.unsafeUpdateValidatorsCount(secondNodeOperatorId, newExitedValidatorsCount, stuckValidatorsCount, { - from: voting, + from: stakingRouter, }) await app.unsafeUpdateValidatorsCount(secondNodeOperatorId, newExitedValidatorsCount, stuckValidatorsCount, { - from: voting, + from: stakingRouter, }) const { stakingLimit: secondNodeOperatorStakingLimitAfter } = await app.getNodeOperator(firstNodeOperatorId, true) assert.equals(secondNodeOperatorStakingLimitAfter, secondNodeOperatorStakingLimitBefore) }) }) + describe('updateTargetValidatorsLimits()', () => { + const firstNodeOperatorId = 0 + const secondNodeOperatorId = 1 + + beforeEach(async () => { + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[firstNodeOperatorId] }, { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[secondNodeOperatorId], { from: admin }) + }) + + it('reverts with "APP_AUTH_FAILED" error when called by sender without STAKING_ROUTER_ROLE', async () => { + const hasPermission = await dao.hasPermission(nobody, app, 'STAKING_ROUTER_ROLE') + assert.isFalse(hasPermission) + const isTargetLimitSet = false + const targetLimit = 0 + await assert.reverts( + app.updateTargetValidatorsLimits(firstNodeOperatorId, isTargetLimitSet, targetLimit, { from: nobody }), + 'APP_AUTH_FAILED' + ) + }) + + it('reverts with "OUT_OF_RANGE" error when called with targetLimit > UINT64_MAX', async () => { + const isTargetLimitSet = true + const targetLimit = toBN('0x10000000000000000') + await assert.reverts( + app.updateTargetValidatorsLimits(firstNodeOperatorId, isTargetLimitSet, targetLimit, { from: stakingRouter }), + 'OUT_OF_RANGE' + ) + }) + + it('updates node operator target limit if called by sender with STAKING_ROUTER_ROLE', async () => { + const hasPermission = await dao.hasPermission(stakingRouter, app, 'STAKING_ROUTER_ROLE') + assert.isTrue(hasPermission) + + const targetLimit = 10 + const isTargetLimitSet = true + await app.updateTargetValidatorsLimits(firstNodeOperatorId, isTargetLimitSet, targetLimit, { + from: stakingRouter, + }) + + const keysStatTotal = await app.getStakingModuleSummary() + const expectedExitedValidatorsCount = + NODE_OPERATORS[firstNodeOperatorId].exitedSigningKeysCount + + NODE_OPERATORS[secondNodeOperatorId].exitedSigningKeysCount + assert.equals(keysStatTotal.totalExitedValidators, expectedExitedValidatorsCount) + + const expectedDepositedValidatorsCount = + NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount + + NODE_OPERATORS[secondNodeOperatorId].depositedSigningKeysCount + assert.equals(keysStatTotal.totalDepositedValidators, expectedDepositedValidatorsCount) + + const firstNodeOperatorDepositableValidators = + NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount - + NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount + + const secondNodeOperatorDepositableValidators = + NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - + NODE_OPERATORS[secondNodeOperatorId].depositedSigningKeysCount + + const expectedDepositableValidatorsCount = + Math.min(targetLimit, firstNodeOperatorDepositableValidators) + secondNodeOperatorDepositableValidators + assert.equals(keysStatTotal.depositableValidatorsCount, expectedDepositableValidatorsCount) + }) + }) + describe('onWithdrawalCredentialsChanged()', () => { beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) // node operator without unused keys await nodeOperators.addNodeOperator( app, @@ -1460,19 +1560,19 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob vettedSigningKeysCount: NODE_OPERATORS[2].totalSigningKeysCount, depositedSigningKeysCount: NODE_OPERATORS[2].totalSigningKeysCount, }, - { from: voting } + { from: admin } ) }) - it('reverts with "APP_AUTH_FAILED" error when called by sender without MANAGE_NODE_OPERATOR_ROLE role', async () => { - const hasPermission = await dao.hasPermission(nobody, app, 'MANAGE_NODE_OPERATOR_ROLE') + it('reverts with "APP_AUTH_FAILED" error when called by sender without STAKING_ROUTER_ROLE role', async () => { + const hasPermission = await dao.hasPermission(nobody, app, 'STAKING_ROUTER_ROLE') assert.isFalse(hasPermission) - await assert.reverts(app.onWithdrawalCredentialsChanged(), 'APP_AUTH_FAILED') + await assert.reverts(app.onWithdrawalCredentialsChanged({ from: nobody }), 'APP_AUTH_FAILED') }) it('sets totalSigningKeysCount and vettedSigningKeysCount equal to depositedSigningKeys for all node operators', async () => { const allNodeOperatorsBefore = await nodeOperators.getAllNodeOperators(app) - await app.onWithdrawalCredentialsChanged({ from: voting }) + await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const allNodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) for (let i = 0; i < allNodeOperatorsBefore.length; ++i) { const nodeOperatorBefore = allNodeOperatorsBefore[i] @@ -1484,7 +1584,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits TotalSigningKeysCountChanged & VettedSigningKeysCountChanged events for node operator only if it had unused keys', async () => { const allNodeOperatorsBefore = await nodeOperators.getAllNodeOperators(app) - const receipt = await app.onWithdrawalCredentialsChanged({ from: voting }) + const receipt = await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const allNodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) for (let i = 0; i < allNodeOperatorsBefore.length; ++i) { const nodeOperatorBefore = allNodeOperatorsBefore[i] @@ -1507,7 +1607,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits NodeOperatorTotalKeysTrimmed event for node operator only if it had unused keys', async () => { const allNodeOperatorsBefore = await nodeOperators.getAllNodeOperators(app) - const receipt = await app.onWithdrawalCredentialsChanged({ from: voting }) + const receipt = await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const allNodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) for (let i = 0; i < allNodeOperatorsBefore.length; ++i) { const nodeOperatorBefore = allNodeOperatorsBefore[i] @@ -1524,20 +1624,20 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob } }) - it('sets total vetted signing keys count & total signing keys count values to deposited signing keys count', async () => { + it('sets total max validators count & total validators count values to deposited validators count', async () => { const totalSigningKeysStatsBefore = await app.testing_getTotalSigningKeysStats() assert.notEquals( - totalSigningKeysStatsBefore.vettedSigningKeysCount, + totalSigningKeysStatsBefore.maxValidatorsCount, totalSigningKeysStatsBefore.depositedSigningKeysCount ) assert.notEquals( totalSigningKeysStatsBefore.totalSigningKeysCount, totalSigningKeysStatsBefore.depositedSigningKeysCount ) - await app.onWithdrawalCredentialsChanged({ from: voting }) + await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const totalSigningKeysStatsAfter = await app.testing_getTotalSigningKeysStats() assert.equals( - totalSigningKeysStatsAfter.vettedSigningKeysCount, + totalSigningKeysStatsAfter.maxValidatorsCount, totalSigningKeysStatsBefore.depositedSigningKeysCount ) assert.equals( @@ -1548,7 +1648,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('increases keysOpIndex & changes nonce', async () => { const [keysOpIndexBefore, nonceBefore] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) - await app.onWithdrawalCredentialsChanged({ from: voting }) + await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) assert.notEquals(nonceAfter, nonceBefore) @@ -1556,7 +1656,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits KeysOpIndexSet & NonceChanged', async () => { const keysOpIndexBefore = await app.getKeysOpIndex() - const receipt = await app.onWithdrawalCredentialsChanged({ from: voting }) + const receipt = await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const nonceAfter = await app.getNonce() assert.emits(receipt, 'KeysOpIndexSet', { keysOpIndex: keysOpIndexBefore.toNumber() + 1 }) assert.emits(receipt, 'NonceChanged', { nonce: nonceAfter }) @@ -1564,9 +1664,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it("doesn't change validators keys nonce if keys weren't invalidated", async () => { // invalidated all keys before the test to remove all unused keys of node operators - await app.onWithdrawalCredentialsChanged({ from: voting }) + await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) // the second invalidation must not invalidate keys - const receipt = await app.onWithdrawalCredentialsChanged({ from: voting }) + const receipt = await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const nonceBefore = await app.getNonce() assert.notEmits(receipt, 'NodeOperatorTotalKeysTrimmed') const nonceAfter = await app.getNonce() @@ -1574,12 +1674,102 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) }) + describe('invalidateReadyToDepositKeysRange()', () => { + beforeEach(async () => { + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) + // node operator without unused keys + await nodeOperators.addNodeOperator( + app, + { + ...NODE_OPERATORS[2], + vettedSigningKeysCount: NODE_OPERATORS[2].totalSigningKeysCount, + depositedSigningKeysCount: NODE_OPERATORS[2].totalSigningKeysCount, + }, + { from: admin } + ) + }) + + it('reverts with "APP_AUTH_FAILED" error when called by sender without MANAGE_NODE_OPERATOR_ROLE role', async () => { + const hasPermission = await dao.hasPermission(nobody, app, 'MANAGE_NODE_OPERATOR_ROLE') + assert.isFalse(hasPermission) + const indexFrom = 0 + const indexTo = NODE_OPERATORS.length - 1 + await assert.reverts( + app.invalidateReadyToDepositKeysRange(indexFrom, indexTo, { from: nobody }), + 'APP_AUTH_FAILED' + ) + }) + + it('reverts with "OUT_OF_RANGE" error when called with indexFrom > indexTo', async () => { + const indexFrom = NODE_OPERATORS.length - 1 + const indexTo = 0 + await assert.reverts( + app.invalidateReadyToDepositKeysRange(indexFrom, indexTo, { from: nodeOperatorsManager }), + 'OUT_OF_RANGE' + ) + }) + + it('reverts with "OUT_OF_RANGE" error when called with indexTo >= node operators count', async () => { + const indexFrom = 0 + const indexTo = wei.int(await app.getNodeOperatorsCount()) + await assert.reverts( + app.invalidateReadyToDepositKeysRange(indexFrom, wei.str(indexTo), { from: nodeOperatorsManager }), + 'OUT_OF_RANGE' + ) + await assert.reverts( + app.invalidateReadyToDepositKeysRange(indexFrom, wei.str(indexTo + 1n), { from: nodeOperatorsManager }), + 'OUT_OF_RANGE' + ) + }) + + it('allows invalidate keys for one node operator', async () => { + const nodeOperatorIndex = 1 + const allNodeOperatorsBefore = await nodeOperators.getAllNodeOperators(app) + await app.invalidateReadyToDepositKeysRange(nodeOperatorIndex, nodeOperatorIndex, { from: nodeOperatorsManager }) + const allNodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) + const nodeOperatorBefore = allNodeOperatorsBefore[nodeOperatorIndex] + const nodeOperatorAfter = allNodeOperatorsAfter[nodeOperatorIndex] + assert.equals(nodeOperatorAfter.stakingLimit, nodeOperatorBefore.usedSigningKeys) + assert.equals(nodeOperatorAfter.totalSigningKeys, nodeOperatorBefore.usedSigningKeys) + }) + + it('sets totalSigningKeysCount and vettedSigningKeysCount equal to depositedSigningKeys for each node operator in range', async () => { + const indexFrom = 1 + const indexTo = NODE_OPERATORS.length - 1 + const allNodeOperatorsBefore = await nodeOperators.getAllNodeOperators(app) + await app.invalidateReadyToDepositKeysRange(indexFrom, indexTo, { from: nodeOperatorsManager }) + const allNodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) + for (let i = indexFrom; i <= indexTo; ++i) { + const nodeOperatorBefore = allNodeOperatorsBefore[i] + const nodeOperatorAfter = allNodeOperatorsAfter[i] + assert.equals(nodeOperatorAfter.stakingLimit, nodeOperatorBefore.usedSigningKeys) + assert.equals(nodeOperatorAfter.totalSigningKeys, nodeOperatorBefore.usedSigningKeys) + } + }) + + it("doesn't modify totalSigningKeysCount and vettedSigningKeysCount for all node operators not in range", async () => { + const indexFrom = 2 + const indexTo = NODE_OPERATORS.length - 1 + const allNodeOperatorsBefore = await nodeOperators.getAllNodeOperators(app) + await app.invalidateReadyToDepositKeysRange(indexFrom, indexTo, { from: nodeOperatorsManager }) + const allNodeOperatorsAfter = await nodeOperators.getAllNodeOperators(app) + for (let i = 0; i < indexTo; ++i) { + const nodeOperatorBefore = allNodeOperatorsBefore[i] + const nodeOperatorAfter = allNodeOperatorsAfter[i] + assert.equals(nodeOperatorAfter.stakingLimit, nodeOperatorBefore.stakingLimit) + assert.equals(nodeOperatorAfter.usedSigningKeys, nodeOperatorBefore.usedSigningKeys) + assert.equals(nodeOperatorAfter.totalSigningKeys, nodeOperatorBefore.totalSigningKeys) + } + }) + }) + describe('getSigningKeysAllocationData() with target limit', async () => { const firstNodeOperatorId = 0 const secondNodeOperatorId = 1 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], vettedSigningKeysCount: 8 }, { from: voting }) - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[1], depositedSigningKeysCount: 5 }, { from: voting }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], vettedSigningKeysCount: 8 }, { from: admin }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[1], depositedSigningKeysCount: 5 }, { from: admin }) }) it('_getCorrectedNodeOperator() - deposited < exited+target < vetted', async () => { @@ -1589,7 +1779,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equals(+firstNodeOperatorKeysStats.depositedSigningKeysCount, 5) assert.equals(+firstNodeOperatorKeysStats.exitedSigningKeysCount, 1) - await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, 6, { from: voting }) + await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, 6, { from: stakingRouter }) firstNodeOperatorKeysStats = await app.testing_getNodeOperator(firstNodeOperatorId) assert.equals(+firstNodeOperatorKeysStats.maxSigningKeysCount, 7) @@ -1604,7 +1794,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equals(+firstNodeOperatorKeysStats.depositedSigningKeysCount, 5) assert.equals(+firstNodeOperatorKeysStats.exitedSigningKeysCount, 1) - await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, 1000, { from: voting }) + await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, 1000, { from: stakingRouter }) firstNodeOperatorKeysStats = await app.testing_getNodeOperator(firstNodeOperatorId) assert.equals(+firstNodeOperatorKeysStats.maxSigningKeysCount, 8) @@ -1619,7 +1809,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equals(+firstNodeOperatorKeysStats.depositedSigningKeysCount, 5) assert.equals(+firstNodeOperatorKeysStats.exitedSigningKeysCount, 1) - await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, 4, { from: voting }) + await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, 4, { from: stakingRouter }) firstNodeOperatorKeysStats = await app.testing_getNodeOperator(firstNodeOperatorId) assert.equals( @@ -1630,22 +1820,37 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equals(+firstNodeOperatorKeysStats.exitedSigningKeysCount, 1) }) - it.skip('respects staking limit', async () => { + it('allow set large targetLimit (=UINT64_MAX)', async () => { + let firstNodeOperatorKeysStats = await app.testing_getNodeOperator(firstNodeOperatorId) + assert.equals(+firstNodeOperatorKeysStats.maxSigningKeysCount, 8) + + const targetLimit = toBN('0xFFFFFFFFFFFFFFFF') // UINT64_MAX + await app.updateTargetValidatorsLimits(firstNodeOperatorId, true, targetLimit, { from: stakingRouter }) + + firstNodeOperatorKeysStats = await app.testing_getNodeOperator(firstNodeOperatorId) + assert.equals(+firstNodeOperatorKeysStats.maxSigningKeysCount, 8) + }) + + it('_getSigningKeysAllocationData respects staking limit', async () => { const [firstNodeOperatorKeysStats, secondNodeOperatorKeysStats] = await Promise.all([ app.getNodeOperatorSummary(firstNodeOperatorId), - app.getValidatorsKeysStats(secondNodeOperatorId), + app.getNodeOperatorSummary(secondNodeOperatorId), ]) - assert.isTrue(firstNodeOperatorKeysStats.readyToDepositValidatorsKeysCount.toNumber() > 0) - assert.isTrue(secondNodeOperatorKeysStats.readyToDepositValidatorsKeysCount.toNumber() > 0) + assert.isTrue(firstNodeOperatorKeysStats.depositableValidatorsCount.toNumber() > 0) + assert.isTrue(secondNodeOperatorKeysStats.depositableValidatorsCount.toNumber() > 0) - assert.equals(firstNodeOperatorKeysStats.exitedValidatorsCount, 1) - assert.equals(firstNodeOperatorKeysStats.activeValidatorsKeysCount, 4) - assert.equals(firstNodeOperatorKeysStats.readyToDepositValidatorsKeysCount, 3) + const firstNodeOperatorActiveKeysCount = + firstNodeOperatorKeysStats.totalDepositedValidators.toNumber() - + firstNodeOperatorKeysStats.totalExitedValidators.toNumber() + assert.equals(firstNodeOperatorActiveKeysCount, 4) + assert.equals(firstNodeOperatorKeysStats.depositableValidatorsCount, 3) - assert.equals(secondNodeOperatorKeysStats.exitedValidatorsCount, 0) - assert.equals(secondNodeOperatorKeysStats.activeValidatorsKeysCount, 5) - assert.equals(secondNodeOperatorKeysStats.readyToDepositValidatorsKeysCount, 5) + const secondNodeOperatorActiveKeysCount = + secondNodeOperatorKeysStats.totalDepositedValidators.toNumber() - + secondNodeOperatorKeysStats.totalExitedValidators.toNumber() + assert.equals(secondNodeOperatorActiveKeysCount, 5) + assert.equals(secondNodeOperatorKeysStats.depositableValidatorsCount, 5) const keysToAllocate = 7 const { allocatedKeysCount, nodeOperatorIds, activeKeyCountsAfterAllocation } = @@ -1658,15 +1863,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob assert.equals(activeKeyCountsAfterAllocation.length, 2) // the first node operator has to receive 3 deposits cause reached limit - assert.equals( - activeKeyCountsAfterAllocation[0], - firstNodeOperatorKeysStats.activeValidatorsKeysCount.toNumber() + 3 - ) + assert.equals(activeKeyCountsAfterAllocation[0], firstNodeOperatorActiveKeysCount + 3) // the second receives 4 deposits - assert.equals( - activeKeyCountsAfterAllocation[1], - secondNodeOperatorKeysStats.activeValidatorsKeysCount.toNumber() + 4 - ) + assert.equals(activeKeyCountsAfterAllocation[1], secondNodeOperatorActiveKeysCount + 4) }) }) @@ -1674,8 +1873,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const firstNodeOperatorId = 0 const secondNodeOperatorId = 1 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], vettedSigningKeysCount: 8 }, { from: voting }) - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[1], depositedSigningKeysCount: 5 }, { from: voting }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], vettedSigningKeysCount: 8 }, { from: admin }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[1], depositedSigningKeysCount: 5 }, { from: admin }) }) it('returns empty result when registry has no node operators', async () => { @@ -1692,8 +1891,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('returns empty result when registry has no active node operators', async () => { // deactivate node operators before testing - await app.deactivateNodeOperator(firstNodeOperatorId, { from: voting }) - await app.deactivateNodeOperator(secondNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(firstNodeOperatorId, { from: nodeOperatorsManager }) + await app.deactivateNodeOperator(secondNodeOperatorId, { from: nodeOperatorsManager }) const [firstNodeOperator, secondNodeOperator] = await Promise.all([ app.getNodeOperator(firstNodeOperatorId, false), app.getNodeOperator(secondNodeOperatorId, false), @@ -1711,7 +1910,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('returns empty result when registry has no unused keys', async () => { // remove unused keys - await app.onWithdrawalCredentialsChanged({ from: voting }) + await app.onWithdrawalCredentialsChanged({ from: stakingRouter }) const [firstNodeOperator, secondNodeOperator] = await Promise.all([ app.getNodeOperator(firstNodeOperatorId, false), app.getNodeOperator(secondNodeOperatorId, false), @@ -1814,7 +2013,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it("doesn't allocates keys to deactivated node operators", async () => { - await app.deactivateNodeOperator(firstNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(firstNodeOperatorId, { from: nodeOperatorsManager }) const [firstNodeOperatorReport, secondNodeOperatorReport] = await Promise.all([ app.getNodeOperatorSummary(firstNodeOperatorId), @@ -1896,8 +2095,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const secondNodeOperatorId = 1 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) }) it('reverts with error "APP_AUTH_FAILED" when called by sender without STAKING_ROUTER_ROLE', async () => { @@ -1917,17 +2116,17 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('reverts with error "INVALID_ALLOCATED_KEYS_COUNT" when module has not enough keys', async () => { await app.testing_resetRegistry() - await app.addNodeOperator('fo o', ADDRESS_1, { from: voting }) - await app.addNodeOperator(' bar', ADDRESS_2, { from: voting }) + await app.addNodeOperator('fo o', ADDRESS_1, { from: nodeOperatorsManager }) + await app.addNodeOperator(' bar', ADDRESS_2, { from: nodeOperatorsManager }) const firstOperatorKeys = new signingKeys.FakeValidatorKeys(3) const secondOperatorKeys = new signingKeys.FakeValidatorKeys(3) - await app.addSigningKeys(0, 3, ...firstOperatorKeys.slice(), { from: voting }) - await app.addSigningKeys(1, 3, ...secondOperatorKeys.slice(), { from: voting }) + await app.addSigningKeys(0, 3, ...firstOperatorKeys.slice(), { from: signingKeysManager }) + await app.addSigningKeys(1, 3, ...secondOperatorKeys.slice(), { from: signingKeysManager }) - await app.setNodeOperatorStakingLimit(0, 10, { from: voting }) - await app.setNodeOperatorStakingLimit(1, 10, { from: voting }) + await app.setNodeOperatorStakingLimit(0, 10, { from: limitsManager }) + await app.setNodeOperatorStakingLimit(1, 10, { from: limitsManager }) const stakingModuleSummary = await app.getStakingModuleSummary() assert.equals(stakingModuleSummary.depositableValidatorsCount, 6) @@ -1940,17 +2139,17 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob // clear the registry to remove all unused keys with node operators await app.testing_resetRegistry() - await app.addNodeOperator('fo o', ADDRESS_1, { from: voting }) - await app.addNodeOperator(' bar', ADDRESS_2, { from: voting }) + await app.addNodeOperator('fo o', ADDRESS_1, { from: nodeOperatorsManager }) + await app.addNodeOperator(' bar', ADDRESS_2, { from: nodeOperatorsManager }) const firstOperatorKeys = new signingKeys.FakeValidatorKeys(3) const secondOperatorKeys = new signingKeys.FakeValidatorKeys(3) - await app.addSigningKeys(0, 3, ...firstOperatorKeys.slice(), { from: voting }) - await app.addSigningKeys(1, 3, ...secondOperatorKeys.slice(), { from: voting }) + await app.addSigningKeys(0, 3, ...firstOperatorKeys.slice(), { from: signingKeysManager }) + await app.addSigningKeys(1, 3, ...secondOperatorKeys.slice(), { from: signingKeysManager }) - await app.setNodeOperatorStakingLimit(0, 10, { from: voting }) - await app.setNodeOperatorStakingLimit(1, 10, { from: voting }) + await app.setNodeOperatorStakingLimit(0, 10, { from: limitsManager }) + await app.setNodeOperatorStakingLimit(1, 10, { from: limitsManager }) let stakingModuleSummary = await app.getStakingModuleSummary() assert.equals(stakingModuleSummary.depositableValidatorsCount, 6) @@ -2069,14 +2268,17 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const secondNodeOperatorId = 1 const notExistedNodeOperatorId = 2 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) }) it('reverts with "OUT_OF_RANGE" error when called on non existent validator', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'STAKING_ROUTER_ROLE') + const hasPermission = await dao.hasPermission(stakingRouter, app, 'STAKING_ROUTER_ROLE') assert.isTrue(hasPermission) - await assert.reverts(app.getNodeOperator(notExistedNodeOperatorId, false, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.getNodeOperator(notExistedNodeOperatorId, false, { from: stakingRouter }), + 'OUT_OF_RANGE' + ) }) it('returns correct node operator info', async () => { @@ -2103,9 +2305,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const secondNodeOperatorId = 1 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[2], { from: admin }) }) it('returns empty data when no node operators', async () => { @@ -2118,8 +2320,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('returns empty data when all node operators are deactivated', async () => { - await app.deactivateNodeOperator(firstNodeOperatorId, { from: voting }) - await app.deactivateNodeOperator(secondNodeOperatorId, { from: voting }) + await app.deactivateNodeOperator(firstNodeOperatorId, { from: nodeOperatorsManager }) + await app.deactivateNodeOperator(secondNodeOperatorId, { from: nodeOperatorsManager }) const totalRewardsShare = web3.utils.toWei('10') const { recipients, shares } = await app.getRewardsDistribution(totalRewardsShare) @@ -2135,7 +2337,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob NODE_OPERATORS[secondNodeOperatorId].depositedSigningKeysCount, ] ) - await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: voting }) + await app.updateExitedValidatorsCount(operatorIds, keysCounts, { from: stakingRouter }) const activeNodeOperators = await nodeOperators.filterNodeOperators(app, (nodeOperator) => nodeOperator.active) const totalRewardsShare = web3.utils.toWei('10') const { recipients, shares } = await app.getRewardsDistribution(totalRewardsShare) @@ -2158,9 +2360,11 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob 0n ) - const perValidatorReward = totalRewardsShare / totalActiveNodeOperators const expectedRewardsDistribution = NODE_OPERATORS.filter((n) => n.isActive !== false).map((n) => - n.isActive === false ? 0n : perValidatorReward * BigInt(n.depositedSigningKeysCount - n.exitedSigningKeysCount) + n.isActive === false + ? 0n + : (totalRewardsShare * BigInt(n.depositedSigningKeysCount - n.exitedSigningKeysCount)) / + totalActiveNodeOperators ) assert.equals(shares.length, expectedRewardsDistribution.length) for (let i = 0; i < shares.length; ++i) { @@ -2177,8 +2381,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const nonExistentNodeOperatorId = 3 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { name: '1', rewardAddress: ADDRESS_1 }, { from: voting }) - await nodeOperators.addNodeOperator(app, { name: '2', rewardAddress: ADDRESS_2 }, { from: voting }) + await nodeOperators.addNodeOperator(app, { name: '1', rewardAddress: ADDRESS_1 }, { from: admin }) + await nodeOperators.addNodeOperator(app, { name: '2', rewardAddress: ADDRESS_2 }, { from: admin }) }) it('reverts with APP_AUTH_FAILED error when called by sender without MANAGE_SIGNING_KEYS role', async () => { @@ -2195,7 +2399,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('reverts with OUT_OF_RANGE error when keys count > UINT64_MAX', async () => { const keysCount = toBN('0x10000000000000000') await assert.reverts( - app.addSigningKeys(secondNodeOperatorId, keysCount, '0x', '0x', { from: voting }), + app.addSigningKeys(secondNodeOperatorId, keysCount, '0x', '0x', { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2204,7 +2408,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = firstNodeOperatorKeys.count const [publicKeys, signatures] = firstNodeOperatorKeys.slice() await assert.reverts( - app.addSigningKeys(nonExistentNodeOperatorId, keysCount, publicKeys, signatures, { from: voting }), + app.addSigningKeys(nonExistentNodeOperatorId, keysCount, publicKeys, signatures, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2212,7 +2416,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('reverts with "OUT_OF_RANGE" error when keys count is 0', async () => { const keysCount = 0 await assert.reverts( - app.addSigningKeys(firstNodeOperatorId, keysCount, '0x', '0x', { from: voting }), + app.addSigningKeys(firstNodeOperatorId, keysCount, '0x', '0x', { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2221,7 +2425,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = 2 const [publicKeys, signatures] = secondNodeOperatorKeys.slice(0, keysCount) await assert.reverts( - app.addSigningKeys(firstNodeOperatorId, keysCount, publicKeys + 'deadbeaf', signatures, { from: voting }), + app.addSigningKeys(firstNodeOperatorId, keysCount, publicKeys + 'deadbeaf', signatures, { + from: signingKeysManager, + }), 'LENGTH_MISMATCH' ) }) @@ -2230,7 +2436,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = 2 const [publicKeys, signatures] = secondNodeOperatorKeys.slice(0, keysCount) await assert.reverts( - app.addSigningKeys(firstNodeOperatorId, keysCount, publicKeys, signatures.slice(0, -2), { from: voting }), + app.addSigningKeys(firstNodeOperatorId, keysCount, publicKeys, signatures.slice(0, -2), { + from: signingKeysManager, + }), 'LENGTH_MISMATCH' ) }) @@ -2240,7 +2448,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const [publicKeys] = secondNodeOperatorKeys.slice(0, keysCount) const [, signatures] = secondNodeOperatorKeys.slice(0, keysCount + 1) await assert.reverts( - app.addSigningKeys(firstNodeOperatorId, keysCount, publicKeys, signatures.slice(0, -2), { from: voting }), + app.addSigningKeys(firstNodeOperatorId, keysCount, publicKeys, signatures.slice(0, -2), { + from: signingKeysManager, + }), 'LENGTH_MISMATCH' ) }) @@ -2249,7 +2459,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = 1 const [, signature] = firstNodeOperatorKeys.get(0) await assert.reverts( - app.addSigningKeys(firstNodeOperatorId, keysCount, signingKeys.EMPTY_PUBLIC_KEY, signature, { from: voting }), + app.addSigningKeys(firstNodeOperatorId, keysCount, signingKeys.EMPTY_PUBLIC_KEY, signature, { + from: signingKeysManager, + }), 'EMPTY_KEY' ) }) @@ -2257,7 +2469,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('increases node operator total signing keys counter correctly', async () => { const { totalSigningKeys: totalSigningKeysCountBefore } = await app.getNodeOperator(firstNodeOperatorId, false) await app.addSigningKeys(firstNodeOperatorId, firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) const { totalSigningKeys: totalSigningKeysCountAfter } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals( @@ -2269,7 +2481,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it("doesn't modify total signing keys counter of other node operators", async () => { const { totalSigningKeys: totalSigningKeysCountBefore } = await app.getNodeOperator(secondNodeOperatorId, false) await app.addSigningKeys(firstNodeOperatorId, firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) const { totalSigningKeys: totalSigningKeysCountAfter } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(totalSigningKeysCountBefore, totalSigningKeysCountAfter) @@ -2277,7 +2489,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('stores keys correctly for node operator without keys', async () => { await app.addSigningKeys(secondNodeOperatorId, secondNodeOperatorKeys.count, ...secondNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) for (let i = 0; i < secondNodeOperatorKeys.count; ++i) { const { key, depositSignature } = await app.getSigningKey(secondNodeOperatorId, i) @@ -2293,14 +2505,14 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob firstNodeOperatorId, initialKeysCount, ...firstNodeOperatorKeys.slice(0, initialKeysCount), - { from: voting } + { from: signingKeysManager } ) await app.addSigningKeys( firstNodeOperatorId, firstNodeOperatorKeys.count - initialKeysCount, ...firstNodeOperatorKeys.slice(2), { - from: voting, + from: signingKeysManager, } ) for (let i = initialKeysCount; i < firstNodeOperatorKeys.count; ++i) { @@ -2313,10 +2525,10 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it("doesn't modify the keys of other node operators", async () => { await app.addSigningKeys(firstNodeOperatorId, firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) await app.addSigningKeys(secondNodeOperatorId, secondNodeOperatorKeys.count, ...secondNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) for (let i = 0; i < firstNodeOperatorKeys.count; ++i) { const { key, depositSignature } = await app.getSigningKey(firstNodeOperatorId, i) @@ -2328,11 +2540,11 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('increases global total signing keys counter correctly', async () => { await app.addSigningKeys(secondNodeOperatorId, secondNodeOperatorKeys.count, ...secondNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) const { totalSigningKeysCount: totalSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() await app.addSigningKeys(firstNodeOperatorId, firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) const { totalSigningKeysCount: totalSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(totalSigningKeysCountAfter, totalSigningKeysCountBefore.toNumber() + firstNodeOperatorKeys.count) @@ -2341,7 +2553,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('increases keysOpIndex & changes nonce', async () => { const [keysOpIndexBefore, nonceBefore] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) await app.addSigningKeys(firstNodeOperatorId, firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) @@ -2355,7 +2567,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, } ) const nonceAfter = await app.getNonce() @@ -2369,7 +2581,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob firstNodeOperatorKeys.count, ...firstNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, } ) for (let i = 0; i < firstNodeOperatorKeys.count; ++i) { @@ -2388,7 +2600,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob secondNodeOperatorKeys.count, ...secondNodeOperatorKeys.slice(), { - from: voting, + from: signingKeysManager, } ) assert.emits(receipt, 'TotalSigningKeysCountChanged', { @@ -2403,7 +2615,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const firstNodeOperatorKeys = new signingKeys.FakeValidatorKeys(1) beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { name: '1', rewardAddress: user1 }, { from: voting }) + await nodeOperators.addNodeOperator(app, { name: '1', rewardAddress: user1 }, { from: admin }) }) it('reverts with APP_AUTH_FAILED error when called not by reward address', async () => { @@ -2454,15 +2666,18 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob beforeEach(async () => { await nodeOperators - .addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) + .addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) .then((r) => (firstNodeOperatorKeys = r.validatorKeys)) await nodeOperators - .addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + .addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) .then((r) => (secondNodeOperatorKeys = r.validatorKeys)) }) it('reverts with "OUT_OF_RANGE" error when called on non existent validator', async () => { - await assert.reverts(app.removeSigningKey(nonExistentNodeOperatorId, 0, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.removeSigningKey(nonExistentNodeOperatorId, 0, { from: signingKeysManager }), + 'OUT_OF_RANGE' + ) }) it('reverts with APP_AUTH_FAILED error when called by sender without MANAGE_SIGNING_KEYS role', async () => { @@ -2474,25 +2689,34 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('reverts with OUT_OF_RANGE error when index greater than UINT64_MAX', async () => { const keyIndex = toBN('0x10000000000000000') - await assert.reverts(app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }), + 'OUT_OF_RANGE' + ) }) it('reverts with OUT_OF_RANGE error when index is greater than total signing keys count', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - await assert.reverts(app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }), + 'OUT_OF_RANGE' + ) }) it('reverts with OUT_OF_RANGE error when key with passed index was deposited', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount - 1 assert(keyIndex >= 0) - await assert.reverts(app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }), 'OUT_OF_RANGE') + await assert.reverts( + app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }), + 'OUT_OF_RANGE' + ) }) it('decreases total signing keys counter for node operator', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].depositedSigningKeysCount assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) const { totalSigningKeys: totalSigningKeysBefore } = await app.getNodeOperator(secondNodeOperatorId, false) - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) const { totalSigningKeys: totalSigningKeysAfter } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(totalSigningKeysAfter.toNumber(), totalSigningKeysBefore.toNumber() - 1) }) @@ -2501,7 +2725,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].depositedSigningKeysCount + 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) const { totalSigningKeysCount: totalSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) const { totalSigningKeysCount: totalSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(totalSigningKeysCountAfter.toNumber(), totalSigningKeysCountBefore.toNumber() - 1) }) @@ -2510,7 +2734,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount + 1 assert.isTrue(keyIndex <= NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount) const { stakingLimit: stakingLimitBefore } = await app.getNodeOperator(firstNodeOperatorId, false) - await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) const { stakingLimit: stakingLimitAfter } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(stakingLimitBefore, stakingLimitAfter) }) @@ -2519,7 +2743,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount assert.isTrue(keyIndex <= NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount) const { stakingLimit: stakingLimitBefore } = await app.getNodeOperator(firstNodeOperatorId, false) - await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) const { stakingLimit: stakingLimitAfter } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(stakingLimitBefore, stakingLimitAfter) }) @@ -2527,7 +2751,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('sets vetted signing keys counter equal to passed key index if it less than vetted keys counter', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) const { stakingLimit: stakingLimitAfter } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(stakingLimitAfter.toNumber(), keyIndex) }) @@ -2535,9 +2759,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('correctly decreases global vetted signing keys count if key index is less then vetted keys counter of node operator', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() const vettedSigningKeysDecrement = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - keyIndex assert.equals( vettedSigningKeysCountAfter.toNumber(), @@ -2545,62 +2769,43 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob ) }) - it.skip('correctly decreases global vetted signing keys and totalTargetStats count if key index is less then vetted keys counter of node operator', async () => { + it('correctly decreases global vetted signing keys and totalTargetStats count if key index is less then vetted keys counter of node operator', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - - const { - isTargetLimitActive: isTargetLimitActiveBefore, - targetValidatorsCount: targetValidatorsCountBefore, - excessValidatorsCount: excessValidatorsCountBefore, - } = await app.testing_getTotalTargetStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() const vettedSigningKeysDecrement = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - keyIndex assert.equals( vettedSigningKeysCountAfter.toNumber(), vettedSigningKeysCountBefore.toNumber() - vettedSigningKeysDecrement ) - - const { - isTargetLimitActive: isTargetLimitActiveAfter, - targetValidatorsCount: targetValidatorsCountAfter, - excessValidatorsCount: excessValidatorsCountAfter, - } = await app.testing_getTotalTargetStats() - - assert.equals(isTargetLimitActiveAfter, isTargetLimitActiveBefore) - assert.equals( - targetValidatorsCountAfter, - targetValidatorsCountBefore.toNumber() - vettedSigningKeysCountBefore.toNumber() - vettedSigningKeysDecrement - ) - assert.equals(excessValidatorsCountAfter, excessValidatorsCountBefore) }) it("doesn't modify global vetted signing keys count if key index is equal to vettedSigningKeysCount", async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(vettedSigningKeysCountAfter, vettedSigningKeysCountBefore) }) it("doesn't modify global vetted signing keys count if key index is greater than vettedSigningKeysCount", async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount + 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(vettedSigningKeysCountAfter, vettedSigningKeysCountBefore) }) it('increases keysOpIndex & changes nonce', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount + 1 const [keysOpIndexBefore, nonceBefore] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) - await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) assert.notEquals(nonceAfter, nonceBefore) @@ -2609,7 +2814,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits KeysOpIndexSet & NonceChanged', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount + 1 const keysOpIndexBefore = await app.getKeysOpIndex() - const receipt = await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + const receipt = await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) const nonceAfter = await app.getNonce() assert.emits(receipt, 'KeysOpIndexSet', { keysOpIndex: keysOpIndexBefore.toNumber() + 1 }) assert.emits(receipt, 'NonceChanged', { nonce: nonceAfter }) @@ -2617,7 +2822,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('correctly removes the last unused signing key', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - 1 - await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) const { totalSigningKeys } = await app.getNodeOperator(firstNodeOperatorId, false) for (let i = 0; i < totalSigningKeys.toNumber(); ++i) { const { key, depositSignature } = await app.getSigningKey(firstNodeOperatorId, i) @@ -2630,7 +2835,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('correctly removes unused signing key from the middle', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 assert.notEqual(NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - 1, keyIndex) - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) for (let i = 0; i < keyIndex; ++i) { const { key, depositSignature } = await app.getSigningKey(secondNodeOperatorId, i) const [expectedPublicKey, expectedSignature] = secondNodeOperatorKeys.get(i) @@ -2658,7 +2863,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob for (let i = 0; i < unusedKeysCount; ++i) { // always remove the first signing key await app.removeSigningKey(firstNodeOperatorId, NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount, { - from: voting, + from: signingKeysManager, }) } const { totalSigningKeys, stakingLimit, usedSigningKeys } = await app.getNodeOperator(firstNodeOperatorId, false) @@ -2680,7 +2885,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob // remove all unused signing keys of first node operator for (let i = 0; i < unusedKeysCount; ++i) { await app.removeSigningKey(firstNodeOperatorId, NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount, { - from: voting, + from: signingKeysManager, }) } @@ -2695,10 +2900,12 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('after key removal new key adding works correctly', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) const { totalSigningKeys } = await app.getNodeOperator(secondNodeOperatorId, false) const keysToAdd = new signingKeys.FakeValidatorKeys(1) - await app.addSigningKeys(secondNodeOperatorId, keysToAdd.count, ...keysToAdd.slice(), { from: voting }) + await app.addSigningKeys(secondNodeOperatorId, keysToAdd.count, ...keysToAdd.slice(), { + from: signingKeysManager, + }) const { key, depositSignature } = await app.getSigningKey(secondNodeOperatorId, totalSigningKeys.toNumber()) assert.equals(key, keysToAdd.get(0)[0]) assert.equals(depositSignature, keysToAdd.get(0)[1]) @@ -2707,7 +2914,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits VettedSigningKeysCountChanged event with correct params if passed index is less then current vetted signing keys count', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const receipt = await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + const receipt = await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) assert.emits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId, approvedValidatorsCount: keyIndex, @@ -2717,21 +2924,21 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it("doesn't emit VettedSigningKeysCountChanged event if passed index is equal to the current vetted signing keys count", async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const receipt = await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + const receipt = await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) assert.notEmits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId }) }) it("doesn't emit VettedSigningKeysCountChanged event if passed index is greater than current vetted signing keys count", async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount + 1 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const receipt = await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: voting }) + const receipt = await app.removeSigningKey(secondNodeOperatorId, keyIndex, { from: signingKeysManager }) assert.notEmits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId }) }) it('emits TotalSigningKeysCountChanged event with correct params', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - 1 assert.isTrue(keyIndex <= NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount) - const receipt = await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + const receipt = await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) assert.emits( receipt, 'TotalSigningKeysCountChanged', @@ -2746,7 +2953,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('emits SigningKeyRemoved event with correct params', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount assert.isTrue(keyIndex < NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount) - const receipt = await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: voting }) + const receipt = await app.removeSigningKey(firstNodeOperatorId, keyIndex, { from: signingKeysManager }) assert.emits( receipt, 'SigningKeyRemoved', @@ -2767,10 +2974,10 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob beforeEach(async () => { await nodeOperators - .addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) + .addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) .then((r) => (firstNodeOperatorKeys = r.validatorKeys)) await nodeOperators - .addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + .addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) .then((r) => (secondNodeOperatorKeys = r.validatorKeys)) }) @@ -2784,7 +2991,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('returns earlier if keys count is 0', async () => { const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, 0, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, 0, { from: signingKeysManager }) const { totalSigningKeys } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(totalSigningKeys, NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount) }) @@ -2793,7 +3000,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = 0 const keysCount = 10 await assert.reverts( - app.removeSigningKeys(nonExistentNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeys(nonExistentNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2802,7 +3009,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = toBN('0x10000000000000000') const keysCount = 10 await assert.reverts( - app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2811,7 +3018,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount const keysCount = toBN('0x10000000000000000') await assert.reverts( - app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2820,7 +3027,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = toBN('0x8000000000000000') const keysCount = toBN('0x8000000000000000') await assert.reverts( - app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2830,7 +3037,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex + 1 assert(keyIndex + keysCount > NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) await assert.reverts( - app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2841,7 +3048,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount - keyIndex assert(keysCount > 0) await assert.reverts( - app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }), 'OUT_OF_RANGE' ) }) @@ -2851,7 +3058,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) const { totalSigningKeys: totalSigningKeysBefore } = await app.getNodeOperator(secondNodeOperatorId, false) - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { totalSigningKeys: totalSigningKeysAfter } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(totalSigningKeysAfter.toNumber(), totalSigningKeysBefore.toNumber() - keysCount) }) @@ -2861,7 +3068,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - keyIndex assert(keysCount > 0) const { totalSigningKeysCount: totalSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { totalSigningKeysCount: totalSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(totalSigningKeysCountAfter.toNumber(), totalSigningKeysCountBefore.toNumber() - keysCount) }) @@ -2871,7 +3078,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) const { stakingLimit: stakingLimitBefore } = await app.getNodeOperator(firstNodeOperatorId, false) - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { stakingLimit: stakingLimitAfter } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(stakingLimitBefore, stakingLimitAfter) }) @@ -2880,7 +3087,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount const keysCount = 1 const { stakingLimit: stakingLimitBefore } = await app.getNodeOperator(firstNodeOperatorId, false) - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { stakingLimit: stakingLimitAfter } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(stakingLimitBefore, stakingLimitAfter) }) @@ -2889,7 +3096,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { stakingLimit: stakingLimitAfter } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(stakingLimitAfter.toNumber(), keyIndex) }) @@ -2897,9 +3104,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('correctly decreases global vetted signing keys count if fromIndex is less then vetted keys counter of node operator', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 const keysCount = 2 - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() const vettedSigningKeysDecrement = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - keyIndex assert.equals( vettedSigningKeysCountAfter.toNumber(), @@ -2910,18 +3117,18 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it("doesn't modify global vetted signing keys count if fromIndex is equal to vettedSigningKeysCount", async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex - 1 - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(vettedSigningKeysCountAfter, vettedSigningKeysCountBefore) }) it("doesn't modify global vetted signing keys count if fromIndex is greater than vettedSigningKeysCount", async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount + 1 const keysCount = 1 - const { vettedSigningKeysCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) - const { vettedSigningKeysCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() + const { maxValidatorsCount: vettedSigningKeysCountBefore } = await app.testing_getTotalSigningKeysStats() + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) + const { maxValidatorsCount: vettedSigningKeysCountAfter } = await app.testing_getTotalSigningKeysStats() assert.equals(vettedSigningKeysCountAfter, vettedSigningKeysCountBefore) }) @@ -2930,7 +3137,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) const [keysOpIndexBefore, nonceBefore] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const [keysOpIndexAfter, nonceAfter] = await Promise.all([app.getKeysOpIndex(), app.getNonce()]) assert.equals(keysOpIndexAfter, keysOpIndexBefore.toNumber() + 1) assert.notEquals(nonceAfter, nonceBefore) @@ -2941,7 +3148,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keysCount = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) const keysOpIndexBefore = await app.getKeysOpIndex() - const receipt = await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + const receipt = await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { + from: signingKeysManager, + }) const nonceAfter = await app.getNonce() assert.emits(receipt, 'KeysOpIndexSet', { keysOpIndex: keysOpIndexBefore.toNumber() + 1 }) assert.emits(receipt, 'NonceChanged', { nonce: nonceAfter }) @@ -2951,7 +3160,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { totalSigningKeys, stakingLimit } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals(totalSigningKeys, NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount) assert.equals(stakingLimit, NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount) @@ -2967,7 +3176,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].depositedSigningKeysCount const keysCount = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - keyIndex assert(keysCount > 0) - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { totalSigningKeys, stakingLimit } = await app.getNodeOperator(secondNodeOperatorId, false) assert.equals( totalSigningKeys, @@ -3006,7 +3215,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount + 1 const keysCount = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keyIndex - 1 assert(keysCount > 0) - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { totalSigningKeys, stakingLimit } = await app.getNodeOperator(firstNodeOperatorId, false) assert.equals(totalSigningKeys, NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keysCount) assert.equals(stakingLimit, keyIndex) @@ -3039,7 +3248,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount assert(unusedKeysCount > 0) - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, unusedKeysCount, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, unusedKeysCount, { from: signingKeysManager }) const { totalSigningKeys, stakingLimit, usedSigningKeys } = await app.getNodeOperator(firstNodeOperatorId, false) for (let i = 0; i < totalSigningKeys.toNumber(); ++i) { const { key, depositSignature } = await app.getSigningKey(firstNodeOperatorId, i) @@ -3058,7 +3267,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount assert(unusedKeysCount > 0) - await app.removeSigningKeys(firstNodeOperatorId, keyIndex, unusedKeysCount, { from: voting }) + await app.removeSigningKeys(firstNodeOperatorId, keyIndex, unusedKeysCount, { from: signingKeysManager }) for (let i = 0; i < secondNodeOperatorKeys.count; ++i) { const { key, depositSignature } = await app.getSigningKey(secondNodeOperatorId, i) @@ -3071,10 +3280,12 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob it('after keys removal new key adding works correctly', async () => { const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 const keysCount = 2 - await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: signingKeysManager }) const { totalSigningKeys } = await app.getNodeOperator(secondNodeOperatorId, false) const keysToAdd = new signingKeys.FakeValidatorKeys(1) - await app.addSigningKeys(secondNodeOperatorId, keysToAdd.count, ...keysToAdd.slice(), { from: voting }) + await app.addSigningKeys(secondNodeOperatorId, keysToAdd.count, ...keysToAdd.slice(), { + from: signingKeysManager, + }) const { key, depositSignature } = await app.getSigningKey(secondNodeOperatorId, totalSigningKeys.toNumber()) assert.equals(key, keysToAdd.get(0)[0]) assert.equals(depositSignature, keysToAdd.get(0)[1]) @@ -3084,7 +3295,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount - 1 const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) - const receipt = await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + const receipt = await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { + from: signingKeysManager, + }) assert.emits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId, approvedValidatorsCount: keyIndex, @@ -3095,7 +3308,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount const keysCount = 3 assert.isTrue(keyIndex <= NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount) - const receipt = await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + const receipt = await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { + from: signingKeysManager, + }) assert.notEmits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId }) }) @@ -3103,7 +3318,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[secondNodeOperatorId].vettedSigningKeysCount + 1 const keysCount = NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) - const receipt = await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { from: voting }) + const receipt = await app.removeSigningKeys(secondNodeOperatorId, keyIndex, keysCount, { + from: signingKeysManager, + }) assert.notEmits(receipt, 'VettedSigningKeysCountChanged', { nodeOperatorId: secondNodeOperatorId }) }) @@ -3111,7 +3328,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount const keysCount = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) - const receipt = await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + const receipt = await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { + from: signingKeysManager, + }) assert.emits(receipt, 'TotalSigningKeysCountChanged', { nodeOperatorId: firstNodeOperatorId, totalValidatorsCount: NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount, @@ -3122,7 +3341,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount const keysCount = NODE_OPERATORS[firstNodeOperatorId].totalSigningKeysCount - keyIndex assert(keysCount > 0) - const receipt = await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { from: voting }) + const receipt = await app.removeSigningKeys(firstNodeOperatorId, keyIndex, keysCount, { + from: signingKeysManager, + }) for (let i = keyIndex; i < keyIndex + keysCount; ++i) { assert.emits( receipt, @@ -3142,13 +3363,13 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const rewardAddress = user1 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], rewardAddress }, { from: voting }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], rewardAddress }, { from: admin }) }) it('reverts with OUT_OF_RANGE error when index greater than UINT64_MAX', async () => { const keyIndex = toBN('0x10000000000000000') await assert.reverts( - app.removeSigningKeyOperatorBH(firstNodeOperatorId, keyIndex, { from: voting }), + app.removeSigningKeyOperatorBH(firstNodeOperatorId, keyIndex, { from: rewardAddress }), 'OUT_OF_RANGE' ) }) @@ -3175,14 +3396,14 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const rewardAddress = user1 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], rewardAddress }, { from: voting }) + await nodeOperators.addNodeOperator(app, { ...NODE_OPERATORS[0], rewardAddress }, { from: admin }) }) it('reverts with OUT_OF_RANGE error when index greater than UINT64_MAX', async () => { const keyIndex = toBN('0x10000000000000000') const keysCount = 1 await assert.reverts( - app.removeSigningKeysOperatorBH(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeysOperatorBH(firstNodeOperatorId, keyIndex, keysCount, { from: rewardAddress }), 'OUT_OF_RANGE' ) }) @@ -3191,7 +3412,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = NODE_OPERATORS[firstNodeOperatorId].vettedSigningKeysCount const keysCount = toBN('0x10000000000000000') await assert.reverts( - app.removeSigningKeysOperatorBH(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeysOperatorBH(firstNodeOperatorId, keyIndex, keysCount, { from: rewardAddress }), 'OUT_OF_RANGE' ) }) @@ -3200,7 +3421,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const keyIndex = toBN('0x8000000000000000') const keysCount = toBN('0x8000000000000000') await assert.reverts( - app.removeSigningKeysOperatorBH(firstNodeOperatorId, keyIndex, keysCount, { from: voting }), + app.removeSigningKeysOperatorBH(firstNodeOperatorId, keyIndex, keysCount, { from: rewardAddress }), 'OUT_OF_RANGE' ) }) @@ -3244,7 +3465,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob steth.sharesOf(user3), ]) // calls distributeRewards() inside - await app.onExitedAndStuckValidatorsCountsUpdated({ from: voting }) + await app.onExitedAndStuckValidatorsCountsUpdated({ from: stakingRouter }) const recipientsSharesAfter = await Promise.all([ steth.sharesOf(user1), steth.sharesOf(user2), @@ -3261,7 +3482,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await steth.mintShares(app.address, ETH(10)) // calls distributeRewards() inside - await app.onExitedAndStuckValidatorsCountsUpdated({ from: voting }) + await app.onExitedAndStuckValidatorsCountsUpdated({ from: stakingRouter }) assert.equals(await steth.sharesOf(user1), ETH(3)) assert.equals(await steth.sharesOf(user2), ETH(7)) @@ -3273,7 +3494,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await steth.mintShares(app.address, ETH(10)) // calls distributeRewards() inside - const receipt = await app.onExitedAndStuckValidatorsCountsUpdated({ from: voting }) + const receipt = await app.onExitedAndStuckValidatorsCountsUpdated({ from: stakingRouter }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user1, sharesAmount: ETH(3) }) assert.emits(receipt, 'RewardsDistributed', { rewardAddress: user2, sharesAmount: ETH(7) }) @@ -3289,7 +3510,9 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob // already have three operators added for (let i = 3; i < maxNodeOperatorsCount; ++i) { - await app.testing_addNodeOperator(`Node Operator #${i}`, generateRandomAddress(), 5, 5, 5, 0, { from: voting }) + await app.testing_addNodeOperator(`Node Operator #${i}`, generateRandomAddress(), 5, 5, 5, 0, { + from: limitsManager, + }) } assert.equals(await app.getNodeOperatorsCount(), maxNodeOperatorsCount) @@ -3297,7 +3520,7 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await steth.mintShares(app.address, ETH(10)) // calls distributeRewards() inside - const tx = await app.onExitedAndStuckValidatorsCountsUpdated({ from: voting }) + const tx = await app.onExitedAndStuckValidatorsCountsUpdated({ from: stakingRouter }) // just show the used gas console.log(`gas used to distribute rewards for ${maxNodeOperatorsCount} NOs:`, +tx.receipt.gasUsed) @@ -3313,8 +3536,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const notExistedNodeOperatorId = 3 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await app.addNodeOperator('empty', ADDRESS_2, { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await app.addNodeOperator('empty', ADDRESS_2, { from: nodeOperatorsManager }) }) it('reverts with OUT_OF_RANGE error when called with not existed node operator id', async () => { @@ -3339,8 +3562,8 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob const notExistedNodeOperatorId = 3 beforeEach(async () => { - await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) - await app.addNodeOperator('empty', ADDRESS_2, { from: voting }) + await nodeOperators.addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) + await app.addNodeOperator('empty', ADDRESS_2, { from: nodeOperatorsManager }) }) it('reverts with OUT_OF_RANGE error when called with not existed node operator id', async () => { @@ -3368,10 +3591,10 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob beforeEach(async () => { await nodeOperators - .addNodeOperator(app, NODE_OPERATORS[0], { from: voting }) + .addNodeOperator(app, NODE_OPERATORS[0], { from: admin }) .then((r) => (firstNodeOperatorKeys = r.validatorKeys)) await nodeOperators - .addNodeOperator(app, NODE_OPERATORS[1], { from: voting }) + .addNodeOperator(app, NODE_OPERATORS[1], { from: admin }) .then((r) => (secondNodeOperatorKeys = r.validatorKeys)) }) @@ -3436,18 +3659,18 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob }) it('reverts with OUT_OF_RANGE', async () => { - await app.addNodeOperator('0', user1, { from: voting }) + await app.addNodeOperator('0', user1, { from: nodeOperatorsManager }) await assert.reverts(app.getSigningKeys(0, 0, 10), 'OUT_OF_RANGE') }) it('returns specified signing keys', async () => { - await app.addNodeOperator('0', user1, { from: voting }) + await app.addNodeOperator('0', user1, { from: nodeOperatorsManager }) const keys = [pad('0xaa0101', 48), pad('0xaa0202', 48), pad('0xaa0303', 48)] const sigs = [pad('0xa1', 96), pad('0xa2', 96), pad('0xa3', 96)] - await app.addSigningKeys(0, 3, hexConcat(...keys), hexConcat(...sigs), { from: voting }) + await app.addSigningKeys(0, 3, hexConcat(...keys), hexConcat(...sigs), { from: signingKeysManager }) const { pubkeys, signatures, used } = await app.getSigningKeys(0, 1, 2) @@ -3464,9 +3687,39 @@ contract('NodeOperatorsRegistry', ([appManager, voting, user1, user2, user3, nob await assert.reverts(app.onRewardsMinted(123, { from: user1 })) }) it('no reverts with STAKING_ROUTER_ROLE', async () => { - const hasPermission = await dao.hasPermission(voting, app, 'STAKING_ROUTER_ROLE') + const hasPermission = await dao.hasPermission(stakingRouter, app, 'STAKING_ROUTER_ROLE') assert.isTrue(hasPermission) - await app.onRewardsMinted(123, { from: voting }) + await app.onRewardsMinted(123, { from: stakingRouter }) + }) + }) + + describe('setStuckPenaltyDelay()', () => { + it('reverts with error "APP_AUTH_FAILED" when called by sender without MANAGE_NODE_OPERATOR_ROLE', async () => { + const hasPermission = await dao.hasPermission(nobody, app, 'MANAGE_NODE_OPERATOR_ROLE') + assert.isFalse(hasPermission) + const maxStuckPenaltyDelay = await app.MAX_STUCK_PENALTY_DELAY() + await assert.reverts(app.setStuckPenaltyDelay(maxStuckPenaltyDelay, { from: nobody }), 'APP_AUTH_FAILED') + }) + + it('reverts with error "OUT_OF_RANGE" when new value exceeds MAX_PENALTY_DELAY', async () => { + const maxStuckPenaltyDelay = wei.int(await app.MAX_STUCK_PENALTY_DELAY()) + await assert.reverts( + app.setStuckPenaltyDelay(wei.str(maxStuckPenaltyDelay + 1n), { from: nodeOperatorsManager }), + 'OUT_OF_RANGE' + ) + }) + + it('sets stuck penalty delay correctly', async () => { + const newStuckPenaltyDelay = await app.MAX_STUCK_PENALTY_DELAY() + assert.notEquals(await app.getStuckPenaltyDelay(), newStuckPenaltyDelay) + await app.setStuckPenaltyDelay(newStuckPenaltyDelay, { from: nodeOperatorsManager }) + assert.equals(await app.getStuckPenaltyDelay(), newStuckPenaltyDelay) + }) + + it('emits event StuckPenaltyDelayChanged() when setStuckPenaltyDelay is called', async () => { + const sameStackPenaltyDelay = await app.getStuckPenaltyDelay() + const tx = await app.setStuckPenaltyDelay(sameStackPenaltyDelay, { from: nodeOperatorsManager }) + assert.emits(tx, 'StuckPenaltyDelayChanged', { stuckPenaltyDelay: sameStackPenaltyDelay }) }) }) }) diff --git a/test/0.4.24/steth.test.js b/test/0.4.24/steth.test.js index b05e4f7a1..4cd0ca01e 100644 --- a/test/0.4.24/steth.test.js +++ b/test/0.4.24/steth.test.js @@ -88,12 +88,16 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { context('transfer', async () => { it('reverts when recipient is the zero address', async () => { - await assert.reverts(stEth.transfer(ZERO_ADDRESS, tokens(1), { from: user1 }), 'TRANSFER_TO_THE_ZERO_ADDRESS') + await assert.reverts(stEth.transfer(ZERO_ADDRESS, tokens(1), { from: user1 }), 'TRANSFER_TO_ZERO_ADDR') + }) + + it('reverts when recipient is the `stETH` contract itself', async () => { + await assert.reverts(stEth.transfer(stEth.address, tokens(1), { from: user1 }), 'TRANSFER_TO_STETH_CONTRACT') }) it('reverts when the sender does not have enough balance', async () => { - await assert.reverts(stEth.transfer(user2, tokens(101), { from: user1 }), 'TRANSFER_AMOUNT_EXCEEDS_BALANCE') - await assert.reverts(stEth.transfer(user1, bn('1'), { from: user2 }), 'TRANSFER_AMOUNT_EXCEEDS_BALANCE') + await assert.reverts(stEth.transfer(user2, tokens(101), { from: user1 }), 'BALANCE_EXCEEDED') + await assert.reverts(stEth.transfer(user1, bn('1'), { from: user2 }), 'BALANCE_EXCEEDED') }) it('transfer all balance works and emits event', async () => { @@ -169,14 +173,21 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { it('reverts when recipient is zero address', async () => { await assert.reverts( stEth.transferFrom(user1, ZERO_ADDRESS, tokens(1), { from: user2 }), - 'TRANSFER_TO_THE_ZERO_ADDRESS' + 'TRANSFER_TO_ZERO_ADDR' + ) + }) + + it('reverts when recipient is the `stETH` contract itself', async () => { + await assert.reverts( + stEth.transferFrom(user1, stEth.address, tokens(1), { from: user2 }), + 'TRANSFER_TO_STETH_CONTRACT' ) }) it('reverts when sender is zero address', async () => { await assert.reverts( stEth.transferFrom(ZERO_ADDRESS, user3, tokens(0), { from: user2 }), - 'TRANSFER_FROM_THE_ZERO_ADDRESS' + 'TRANSFER_FROM_ZERO_ADDR' ) }) @@ -385,11 +396,11 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { }) it('reverts when burn from zero address', async () => { - await assert.reverts(stEth.burnShares(ZERO_ADDRESS, tokens(1), { from: user1 }), 'BURN_FROM_THE_ZERO_ADDRESS') + await assert.reverts(stEth.burnShares(ZERO_ADDRESS, tokens(1), { from: user1 }), 'BURN_FROM_ZERO_ADDR') }) it('reverts when burn amount exceeds balance', async () => { - await assert.reverts(stEth.burnShares(user1, tokens(101)), 'BURN_AMOUNT_EXCEEDS_BALANCE') + await assert.reverts(stEth.burnShares(user1, tokens(101)), 'BALANCE_EXCEEDED') }) it('burning zero value works', async () => { @@ -591,10 +602,7 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { assert.equals(await stEth.balanceOf(user1), tokens(69)) assert.equals(await stEth.balanceOf(nobody), tokens(30)) - await assert.reverts( - stEth.transferShares(nobody, tokens(75), { from: user1 }), - 'TRANSFER_AMOUNT_EXCEEDS_BALANCE' - ) + await assert.reverts(stEth.transferShares(nobody, tokens(75), { from: user1 }), 'BALANCE_EXCEEDED') await stEth.setTotalPooledEther(tokens(120)) @@ -623,10 +631,7 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { assert.equals(await stEth.balanceOf(user1), tokens(99)) assert.equals(await stEth.balanceOf(nobody), tokens(0)) - await assert.reverts( - stEth.transferSharesFrom(user1, nobody, tokens(30), { from: user2 }), - `TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE` - ) + await assert.reverts(stEth.transferSharesFrom(user1, nobody, tokens(30), { from: user2 }), `ALLOWANCE_EXCEEDED`) await stEth.approve(user2, tokens(30), { from: user1 }) receipt = await stEth.transferSharesFrom(user1, nobody, tokens(30), { from: user2 }) assert.emitsNumberOfEvents(receipt, 'Transfer', 1) @@ -637,22 +642,13 @@ contract('StETH', ([_, __, user1, user2, user3, nobody]) => { assert.equals(await stEth.balanceOf(user1), tokens(69)) assert.equals(await stEth.balanceOf(nobody), tokens(30)) - await assert.reverts( - stEth.transferSharesFrom(user1, nobody, tokens(75), { from: user2 }), - 'TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE' - ) + await assert.reverts(stEth.transferSharesFrom(user1, nobody, tokens(75), { from: user2 }), 'ALLOWANCE_EXCEEDED') await stEth.approve(user2, tokens(75), { from: user1 }) - await assert.reverts( - stEth.transferSharesFrom(user1, nobody, tokens(75), { from: user2 }), - 'TRANSFER_AMOUNT_EXCEEDS_BALANCE' - ) + await assert.reverts(stEth.transferSharesFrom(user1, nobody, tokens(75), { from: user2 }), 'BALANCE_EXCEEDED') await stEth.setTotalPooledEther(tokens(120)) - await assert.reverts( - stEth.transferSharesFrom(user1, nobody, tokens(70), { from: user2 }), - 'TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE' - ) + await assert.reverts(stEth.transferSharesFrom(user1, nobody, tokens(70), { from: user2 }), 'ALLOWANCE_EXCEEDED') await stEth.approve(user2, tokens(84), { from: user1 }) receipt = await stEth.transferSharesFrom(user1, nobody, tokens(69), { from: user2 }) diff --git a/test/0.4.24/stethpermit.test.js b/test/0.4.24/stethpermit.test.js index e5fca0088..c2dc30398 100644 --- a/test/0.4.24/stethpermit.test.js +++ b/test/0.4.24/stethpermit.test.js @@ -4,13 +4,18 @@ const { assert } = require('../helpers/assert') const crypto = require('crypto') const { ACCOUNTS_AND_KEYS, MAX_UINT256, ZERO_ADDRESS } = require('./helpers/constants') const { bn } = require('@aragon/contract-helpers-test') -const { signPermit, signTransferAuthorization, makeDomainSeparator } = require('./helpers/permit_helpers') -const { hexStringFromBuffer } = require('./helpers/sign_utils') -const { ETH } = require('../helpers/utils') -const { EvmSnapshot } = require('../helpers/blockchain') +const { + calculatePermitDigest, + calculateTransferAuthorizationDigest, + makeDomainSeparator, +} = require('./helpers/permit_helpers') +const { ETH, hex, hexStringFromBuffer } = require('../helpers/utils') +const { ecSign } = require('../helpers/signatures') +const { EvmSnapshot, setBalance } = require('../helpers/blockchain') const EIP712StETH = artifacts.require('EIP712StETH') const StETHPermit = artifacts.require('StETHPermitMock') +const ERC1271PermitSignerMock = artifacts.require('ERC1271PermitSignerMock') contract('StETHPermit', ([deployer, ...accounts]) => { let stEthPermit, eip712StETH, chainId, domainSeparator @@ -27,25 +32,61 @@ contract('StETHPermit', ([deployer, ...accounts]) => { await snapshot.make() }) + const getAccountsEOA = async () => { + return { + alice: ACCOUNTS_AND_KEYS[0], + bob: ACCOUNTS_AND_KEYS[1], + } + } + + const getAccountsEIP1271 = async () => { + const alice = await ERC1271PermitSignerMock.new() + const bob = await ERC1271PermitSignerMock.new() + return { alice, bob } + } + + const signEOA = async (digest, acct) => { + return ecSign(digest, acct.key) + } + + const signEIP1271 = async (digest, acct) => { + const sig = await acct.sign(digest) + return { v: hex(sig.v), r: hex(sig.r), s: hex(sig.s) } + } + afterEach(async () => { await snapshot.rollback() }) - context('permit', () => { - const [alice, bob] = ACCOUNTS_AND_KEYS - const charlie = accounts[1] + const test = ({ getAccounts, sign, desc }) => { + let alice, bob + let permitParams + const charlie = accounts[3] + + before(async () => { + const accts = await getAccounts() + alice = accts.alice + bob = accts.bob + + permitParams = { + owner: alice.address, + spender: bob.address, + value: 6e6, + nonce: 0, + deadline: MAX_UINT256, + } - const initialTotalSupply = 100e6 - const initialBalance = 90e6 + await snapshot.make() + }) - const permitParams = { - owner: alice.address, - spender: bob.address, - value: 6e6, - nonce: 0, - deadline: MAX_UINT256, + const signPermit = async (owner, spender, value, nonce, domainSeparator, deadline, acct) => { + const digest = calculatePermitDigest(owner, spender, value, nonce, domainSeparator, deadline) + return await sign(digest, acct) } + const initialTotalSupply = 100e6 + const initialBalance = 90e6 + beforeEach(async () => { await stEthPermit.setTotalPooledEther(initialTotalSupply, { from: deployer }) await stEthPermit.mintShares(permitParams.owner, initialBalance, { from: deployer }) @@ -77,7 +118,7 @@ contract('StETHPermit', ([deployer, ...accounts]) => { // on behalf, and sign with Alice's key let nonce = 0 - let { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + let { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // check that the allowance is initially zero assert.equals(await stEthPermit.allowance(owner, spender), bn(0)) @@ -99,7 +140,7 @@ contract('StETHPermit', ([deployer, ...accounts]) => { // increment nonce nonce = 1 value = 4e5 - ;({ v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key)) + ;({ v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice)) // submit the permit const receipt2 = await stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }) @@ -115,7 +156,7 @@ contract('StETHPermit', ([deployer, ...accounts]) => { it('reverts if the signature does not match given parameters', async () => { const { owner, spender, value, nonce, deadline } = permitParams // create a signed permit - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // try to cheat by claiming the approved amount + 1 await assert.reverts( @@ -129,7 +170,7 @@ contract('StETHPermit', ([deployer, ...accounts]) => { s, { from: charlie } ), - 'ERC20Permit: invalid signature' + 'INVALID_SIGNATURE' ) // check that msg is incorrect even if claim the approved amount - 1 @@ -144,7 +185,7 @@ contract('StETHPermit', ([deployer, ...accounts]) => { s, { from: charlie } ), - 'ERC20Permit: invalid signature' + 'INVALID_SIGNATURE' ) }) @@ -152,27 +193,25 @@ contract('StETHPermit', ([deployer, ...accounts]) => { const { owner, spender, value, nonce, deadline } = permitParams // create a signed permit to grant Bob permission to spend // Alice's funds on behalf, but sign with Bob's key instead of Alice's - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, bob.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, bob) // try to cheat by submitting the permit that is signed by a // wrong person await assert.reverts( - stEthPermit.permit(owner, spender, value, deadline, v, r, s, { - from: charlie, - }), - 'ERC20Permit: invalid signature' + stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }), + 'INVALID_SIGNATURE' ) // unlock bob account (allow transactions originated from bob.address) await ethers.provider.send('hardhat_impersonateAccount', [bob.address]) - await web3.eth.sendTransaction({ to: bob.address, from: accounts[0], value: ETH(10) }) + await setBalance(bob.address, ETH(10)) // even Bob himself can't call permit with the invalid sig await assert.reverts( stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: bob.address, }), - 'ERC20Permit: invalid signature' + 'INVALID_SIGNATURE' ) }) @@ -180,20 +219,18 @@ contract('StETHPermit', ([deployer, ...accounts]) => { const { owner, spender, value, nonce } = permitParams // create a signed permit that already invalid const deadline = (await stEthPermit.getBlockTime()).toString() - 1 - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // try to submit the permit that is expired await assert.reverts( - stEthPermit.permit(owner, spender, value, deadline, v, r, s, { - from: charlie, - }), - 'ERC20Permit: expired deadline' + stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }), + 'DEADLINE_EXPIRED' ) { // create a signed permit that valid for 1 minute (approximately) const deadline1min = (await stEthPermit.getBlockTime()).toString() + 60 - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline1min, domainSeparator, alice.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline1min, domainSeparator, alice) const receipt = await stEthPermit.permit(owner, spender, value, deadline1min, v, r, s, { from: charlie }) assert.equals(await stEthPermit.nonces(owner), bn(1)) @@ -205,64 +242,60 @@ contract('StETHPermit', ([deployer, ...accounts]) => { const { owner, spender, value, deadline } = permitParams const nonce = 1 // create a signed permit - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // check that the next nonce expected is 0, not 1 assert.equals(await stEthPermit.nonces(owner), bn(0)) // try to submit the permit await assert.reverts( - stEthPermit.permit(owner, spender, value, deadline, v, r, s, { - from: charlie, - }), - 'ERC20Permit: invalid signature' + stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }), + 'INVALID_SIGNATURE' ) }) it('reverts if the permit has already been used', async () => { const { owner, spender, value, nonce, deadline } = permitParams // create a signed permit - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // submit the permit await stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }) // try to submit the permit again await assert.reverts( - stEthPermit.permit(owner, spender, value, deadline, v, r, s, { - from: charlie, - }), - 'ERC20Permit: invalid signature' + stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }), + 'INVALID_SIGNATURE' ) - // unlock bob account (allow transactions originated from bob.address) + // unlock alice account (allow transactions originated from alice.address) await ethers.provider.send('hardhat_impersonateAccount', [alice.address]) - await web3.eth.sendTransaction({ to: alice.address, from: accounts[0], value: ETH(10) }) + await setBalance(alice.address, ETH(10)) // try to submit the permit again from Alice herself await assert.reverts( stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: alice.address, }), - 'ERC20Permit: invalid signature' + 'INVALID_SIGNATURE' ) }) it('reverts if the permit has a nonce that has already been used by the signer', async () => { const { owner, spender, value, nonce, deadline } = permitParams // create a signed permit - const permit = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + const permit = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // submit the permit await stEthPermit.permit(owner, spender, value, deadline, permit.v, permit.r, permit.s, { from: charlie }) // create another signed permit with the same nonce, but // with different parameters - const permit2 = signPermit(owner, spender, 1e6, nonce, deadline, domainSeparator, alice.key) + const permit2 = await signPermit(owner, spender, 1e6, nonce, deadline, domainSeparator, alice) // try to submit the permit again await assert.reverts( stEthPermit.permit(owner, spender, 1e6, deadline, permit2.v, permit2.r, permit2.s, { from: charlie }), - 'ERC20Permit: invalid signature' + 'INVALID_SIGNATURE' ) }) @@ -271,14 +304,12 @@ contract('StETHPermit', ([deployer, ...accounts]) => { // create a signed permit that attempts to grant allowance to the // zero address const spender = ZERO_ADDRESS - const { v, r, s } = signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice.key) + const { v, r, s } = await signPermit(owner, spender, value, nonce, deadline, domainSeparator, alice) // try to submit the permit with invalid approval parameters await assert.reverts( - stEthPermit.permit(owner, spender, value, deadline, v, r, s, { - from: charlie, - }), - 'APPROVE_TO_ZERO_ADDRESS' + stEthPermit.permit(owner, spender, value, deadline, v, r, s, { from: charlie }), + 'APPROVE_TO_ZERO_ADDR' ) }) @@ -287,24 +318,25 @@ contract('StETHPermit', ([deployer, ...accounts]) => { // create a signed permit for a transfer const validAfter = 0 const nonce = hexStringFromBuffer(crypto.randomBytes(32)) - const { v, r, s } = signTransferAuthorization( + const digest = calculateTransferAuthorizationDigest( from, to, value, validAfter, validBefore, nonce, - domainSeparator, - alice.key + domainSeparator ) + const { v, r, s } = await sign(digest, alice) // try to submit the transfer permit await assert.reverts( - stEthPermit.permit(from, to, value, validBefore, v, r, s, { - from: charlie, - }), - 'ERC20Permit: invalid signature' + stEthPermit.permit(from, to, value, validBefore, v, r, s, { from: charlie }), + 'INVALID_SIGNATURE' ) }) - }) + } + + context(`permit (ECDSA)`, () => test({ getAccounts: getAccountsEOA, sign: signEOA })) + context(`permit (EIP-1271)`, () => test({ getAccounts: getAccountsEIP1271, sign: signEIP1271 })) }) diff --git a/test/0.4.24/versioned.test.js b/test/0.4.24/versioned.test.js new file mode 100644 index 000000000..4ce477e9f --- /dev/null +++ b/test/0.4.24/versioned.test.js @@ -0,0 +1,57 @@ +const { contract, artifacts } = require('hardhat') +const { assert } = require('../helpers/assert') + +async function deployBehindOssifiableProxy(artifactName, proxyOwner, constructorArgs = []) { + const Contract = await artifacts.require(artifactName) + const implementation = await Contract.new(...constructorArgs, { from: proxyOwner }) + const OssifiableProxy = await artifacts.require('OssifiableProxy') + const proxy = await OssifiableProxy.new(implementation.address, proxyOwner, [], { from: proxyOwner }) + const proxied = await Contract.at(proxy.address) + return { implementation, proxy, proxied } +} + +contract('Versioned', ([admin, proxyOwner, account2, member1, member2]) => { + let versionedImpl + let versionedProxied + const VERSION_INIT = 1 + const VERSION_ZERO = 0 + + before('Deploy', async () => { + const deployed = await deployBehindOssifiableProxy( + 'contracts/0.4.24/test_helpers/VersionedMock.sol:VersionedMock', + proxyOwner, + [] + ) + versionedImpl = deployed.implementation + versionedProxied = deployed.proxied + }) + + describe('raw implementation', async () => { + it('default version is petrified', async () => { + const versionPetrified = await versionedImpl.getPetrifiedVersionMark() + assert.equals(await versionedImpl.getContractVersion(), versionPetrified) + await versionedImpl.checkContractVersion(versionPetrified) + await assert.reverts(versionedImpl.checkContractVersion(VERSION_ZERO), `UNEXPECTED_CONTRACT_VERSION`) + }) + }) + + describe('behind proxy', () => { + it('default version is zero', async () => { + const version = await versionedProxied.getContractVersion() + assert.equals(version, VERSION_ZERO) + await versionedProxied.checkContractVersion(VERSION_ZERO) + await assert.reverts(versionedProxied.checkContractVersion(VERSION_INIT), `UNEXPECTED_CONTRACT_VERSION`) + }) + + it('version can be set and event should be emitted', async () => { + const prevVersion = +(await versionedProxied.getContractVersion()) + const nextVersion = prevVersion + 1 + const tx = await versionedProxied.setContractVersion(nextVersion) + assert.emits(tx, 'ContractVersionSet', { version: nextVersion }) + await versionedProxied.checkContractVersion(nextVersion) + await assert.reverts(versionedProxied.checkContractVersion(prevVersion), `UNEXPECTED_CONTRACT_VERSION`) + const newVersion = +(await versionedProxied.getContractVersion()) + assert.equals(newVersion, nextVersion) + }) + }) +}) diff --git a/test/0.6.12/helpers/constants.js b/test/0.6.12/helpers/constants.js deleted file mode 100644 index 2ab497f21..000000000 --- a/test/0.6.12/helpers/constants.js +++ /dev/null @@ -1,75 +0,0 @@ -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' - -const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' -const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' - -// derived from mnemonic: clarify final village pulse require old seek excite mushroom forest satoshi video -const ACCOUNTS_AND_KEYS = [ - { - address: '0x8e81C8f0CFf3d6eA2Fe72c1A5ee49Fc377401c2D', - key: '84132dd41f32804774a98647c308c0c94a54c0f3931128c0210b6f3689d2b7e7', - }, - { - address: '0x244A0A1d21f21167c17e04EBc5FA33c885990674', - key: '31a372c197c7c5d6856bfac311a66f179bdc3bda20e78030b0fef90e40cbc83f', - }, - { - address: '0x6966f881B3Ee9074b0783CC614e3864e380B8b27', - key: '58bdc54eb2aa3a92e5a36fae01d23b7626fa89116a41e01ca3c6cb18799aee3d', - }, - { - address: '0xaC5faE80468DcC49D404d0625609C031B9AF2cb7', - key: 'd14ca7a30bc2921ddb89ea2f6c52393f91e794cd9c3c994f547eb7edeb092fd1', - }, - { - address: '0x18C246058e9e4Ae1737387fA90cD69E39f78F73A', - key: '1c09baae4343b0a66567d56769b62537623389542480f30e9acdae0624612872', - }, - { - address: '0xc4dc5dAD36fF9b8cB694E79238ECbe76ab13BcEc', - key: '2048571627de761088f2f369306f9c231e6c7ab94be1f0a0c979776a2f424328', - }, - { - address: '0x75Bf6E76D3dB629f46da549D49C9ea821CE8e9A9', - key: '9a9afb6a8e2384e4cce14824d15162bba2eed7e31df846ff77d697fed8cba0c1', - }, - { - address: '0x55937b3ae7a34F551e5aFa5BA51Fba4eD9f8FeD7', - key: '9747a0294186b8fef20ec1e4341a10c87840b18a8930b84c4c5dcd97799ba0bc', - }, - { - address: '0x1eEEBc3900803a28ca6E68Eb98FDeCf98350D97B', - key: 'cdbaf218f6332bfa630ec2abc7a8bc450b2e8746133d84450dfc2234fdfb83ef', - }, - { - address: '0x8719beDE472E67642b88DbA9aD1b1b9dc05CC60e', - key: 'c08dbf2c9aaf5dc3cd5ce5349e754de086202a5f352a512851a4f174ac246198', - }, - { - address: '0xE03701ea2248C7ca2Fed1e655ff3C803C7267302', - key: '40566af8625c93f95c5e1ea9aa42642bcbdd64fe4fe7926283d04173cb842189', - }, - { - address: '0x10506aB975D36aa781B77C1Ce204F46e8f87dA57', - key: '55ef7a25bba3449344d8a1e525ba3ca4999bdc763be4efaf9e94a4de396f8370', - }, - { - address: '0xee11cdb1f9eEe2eB40D2CF99b4fa7D782aE582A6', - key: '985117805090656613ae19743879efe4cff76049de03516debf41926313e3629', - }, - { - address: '0xB9203C29E242d7a19AE6970cDf0d873048B99419', - key: '3a37d1ad3751e697c26dd05a71cd9263c023f3e8a1d1067215044032c62ee916', - }, - { - address: '0xcC03603436Bc0Edd1e0661379f3Aa289ae967597', - key: 'db81ac68891890ec33021264ce81f9b5f9296a9fd95cdc785655db3066db2c08', - }, -] - -module.exports = { - ZERO_ADDRESS, - ZERO_BYTES32, - MAX_UINT256, - ACCOUNTS_AND_KEYS, -} diff --git a/test/0.6.12/helpers/index.js b/test/0.6.12/helpers/index.js deleted file mode 100644 index 39f2c5dba..000000000 --- a/test/0.6.12/helpers/index.js +++ /dev/null @@ -1,52 +0,0 @@ -const { ecsign } = require('ethereumjs-util') -const { assert } = require('chai') - -async function expectRevert(promise, reason) { - let err - try { - await promise - } catch (e) { - err = e - } - - if (!err) { - assert.fail('Exception not thrown') - } - - const errMsg = err.hijackedMessage || err.message - assert.match(errMsg, /revert/i) - - if (reason) { - if (reason instanceof RegExp) { - assert.match(errMsg, reason) - } else { - assert.include(errMsg, reason) - } - } -} - -function hexStringFromBuffer(buf) { - return '0x' + buf.toString('hex') -} - -function strip0x(v) { - return v.replace(/^0x/, '') -} - -function ecSign(digest, privateKey) { - const { v, r, s } = ecsign(bufferFromHexString(digest), bufferFromHexString(privateKey)) - - return { v, r: hexStringFromBuffer(r), s: hexStringFromBuffer(s) } -} - -function bufferFromHexString(hex) { - return Buffer.from(strip0x(hex), 'hex') -} - -module.exports = { - expectRevert, - hexStringFromBuffer, - strip0x, - ecSign, - bufferFromHexString, -} diff --git a/test/0.6.12/helpers/permit_helpers.js b/test/0.6.12/helpers/permit_helpers.js index f083368ac..7d72cba2f 100644 --- a/test/0.6.12/helpers/permit_helpers.js +++ b/test/0.6.12/helpers/permit_helpers.js @@ -1,5 +1,7 @@ const { web3 } = require('hardhat') -const { ecSign, strip0x } = require('./index') + +const { strip0x } = require('../../helpers/utils') +const { ecSign } = require('../../helpers/signatures') const transferWithAuthorizationTypeHash = web3.utils.keccak256( 'TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)' diff --git a/test/0.6.12/wsteth.permit.test.js b/test/0.6.12/wsteth.permit.test.js index a29b1655b..4e9c8d28f 100644 --- a/test/0.6.12/wsteth.permit.test.js +++ b/test/0.6.12/wsteth.permit.test.js @@ -1,8 +1,9 @@ const { artifacts, contract } = require('hardhat') const crypto = require('crypto') const { expect } = require('chai') -const { ACCOUNTS_AND_KEYS, MAX_UINT256, ZERO_ADDRESS } = require('./helpers/constants') -const { expectRevert, hexStringFromBuffer } = require('./helpers') +const { expectRevert } = require('@openzeppelin/test-helpers') +const { ACCOUNTS_AND_KEYS, MAX_UINT256, ZERO_ADDRESS } = require('../helpers/constants') +const { hexStringFromBuffer } = require('../helpers/utils') const { signPermit, signTransferAuthorization, makeDomainSeparator } = require('./helpers/permit_helpers') const WstETH = artifacts.require('WstETHMock') diff --git a/test/0.8.9/burner.test.js b/test/0.8.9/burner.test.js index cd3c4bf46..88dcc6f6a 100644 --- a/test/0.8.9/burner.test.js +++ b/test/0.8.9/burner.test.js @@ -40,6 +40,109 @@ contract('Burner', ([deployer, _, anotherAccount]) => { await snapshot.rollback() }) + describe('Burner ACL correctness', () => { + it(`REQUEST_BURN_MY_STETH_ROLE works`, async () => { + await web3.eth.sendTransaction({ from: anotherAccount, to: lido.address, value: ETH(2) }) + await lido.approve(burner.address, StETH(2), { from: anotherAccount }) + + assert.isFalse(await burner.hasRole(await burner.REQUEST_BURN_MY_STETH_ROLE(), anotherAccount)) + + await assert.revertsOZAccessControl( + burner.requestBurnMyStETH(StETH(1), { from: anotherAccount }), + anotherAccount, + `REQUEST_BURN_MY_STETH_ROLE` + ) + + await assert.revertsOZAccessControl( + burner.requestBurnMyStETHForCover(StETH(1), { from: anotherAccount }), + anotherAccount, + `REQUEST_BURN_MY_STETH_ROLE` + ) + + await burner.grantRole(await burner.REQUEST_BURN_MY_STETH_ROLE(), anotherAccount, { from: appManager }) + assert.isTrue(await burner.hasRole(await burner.REQUEST_BURN_MY_STETH_ROLE(), anotherAccount)) + + await burner.requestBurnMyStETH(StETH(1), { from: anotherAccount }) + await burner.requestBurnMyStETHForCover(StETH(1), { from: anotherAccount }) + }) + + it(`RECOVER_ASSETS_ROLE works`, async () => { + const nft1 = bn(666) + const totalERC20Supply = bn(1000000) + const mockERC20Token = await ERC20OZMock.new(totalERC20Supply, { from: deployer }) + const mockNFT = await ERC721OZMock.new({ from: deployer }) + await mockNFT.mintToken(nft1, { from: deployer }) + await web3.eth.sendTransaction({ from: anotherAccount, to: lido.address, value: ETH(2) }) + + await mockERC20Token.transfer(burner.address, bn(600000), { from: deployer }) + await mockNFT.transferFrom(deployer, burner.address, nft1, { from: deployer }) + await lido.transfer(burner.address, StETH(1), { from: anotherAccount }) + + assert.isFalse(await burner.hasRole(await burner.RECOVER_ASSETS_ROLE(), anotherAccount)) + + await assert.revertsOZAccessControl( + burner.recoverERC20(mockERC20Token.address, bn(600000), { from: anotherAccount }), + anotherAccount, + `RECOVER_ASSETS_ROLE` + ) + await assert.revertsOZAccessControl( + burner.recoverERC721(mockNFT.address, nft1, { from: anotherAccount }), + anotherAccount, + `RECOVER_ASSETS_ROLE` + ) + await assert.revertsOZAccessControl( + burner.recoverExcessStETH({ from: anotherAccount }), + anotherAccount, + `RECOVER_ASSETS_ROLE` + ) + + await burner.grantRole(await burner.RECOVER_ASSETS_ROLE(), anotherAccount, { from: appManager }) + assert.isTrue(await burner.hasRole(await burner.RECOVER_ASSETS_ROLE(), anotherAccount)) + + await burner.recoverERC20(mockERC20Token.address, bn(600000), { from: anotherAccount }) + await burner.recoverERC721(mockNFT.address, nft1, { from: anotherAccount }) + await burner.recoverExcessStETH({ from: anotherAccount }) + }) + + it(`REQUEST_BURN_SHARES_ROLE works`, async () => { + await web3.eth.sendTransaction({ from: anotherAccount, to: lido.address, value: ETH(2) }) + await lido.approve(burner.address, StETH(2), { from: anotherAccount }) + + assert.isFalse(await burner.hasRole(await burner.REQUEST_BURN_SHARES_ROLE(), anotherAccount)) + + await assert.revertsOZAccessControl( + burner.requestBurnSharesForCover(anotherAccount, stETHShares(1), { from: anotherAccount }), + anotherAccount, + `REQUEST_BURN_SHARES_ROLE` + ) + + await assert.revertsOZAccessControl( + burner.requestBurnShares(anotherAccount, stETHShares(1), { from: anotherAccount }), + anotherAccount, + `REQUEST_BURN_SHARES_ROLE` + ) + + await burner.grantRole(await burner.REQUEST_BURN_SHARES_ROLE(), anotherAccount, { from: appManager }) + assert.isTrue(await burner.hasRole(await burner.REQUEST_BURN_SHARES_ROLE(), anotherAccount)) + + await burner.requestBurnSharesForCover(anotherAccount, stETHShares(1), { from: anotherAccount }) + await burner.requestBurnShares(anotherAccount, stETHShares(1), { from: anotherAccount }) + }) + + it(`only Lido can commit shares to burn`, async () => { + assert.revertsWithCustomError(burner.commitSharesToBurn(0, { from: anotherAccount }), `AppAuthLidoFailed()`) + + await burner.commitSharesToBurn(0, { from: lido.address }) + }) + + it(`permissionless view functions are available for anyone`, async () => { + await burner.getSharesRequestedToBurn({ from: anotherAccount }) + await burner.getCoverSharesBurnt({ from: anotherAccount }) + await burner.getNonCoverSharesBurnt({ from: anotherAccount }) + await burner.getExcessStETH({ from: anotherAccount }) + }) + }) + describe('Requests and burn invocation', () => { const bnRound10 = (value) => bn(value).add(bn(5)).div(bn(10)).mul(bn(10)) @@ -115,7 +218,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { await lido.approve(burner.address, StETH(8), { from: deployer }) - // event deployer can't place burn request + // even deployer can't place burn request await assert.revertsOZAccessControl( burner.requestBurnMyStETH(StETH(8), { from: deployer }), deployer, @@ -135,12 +238,47 @@ contract('Burner', ([deployer, _, anotherAccount]) => { assert.equals(await lido.balanceOf(deployer), StETH(22)) }) + it(`reverts on attempt to burn more then requested`, async () => { + // provide allowance and request burn for cover + const sharesAmount8StETH = await lido.getSharesByPooledEth(StETH(8)) + await lido.approve(burner.address, StETH(8), { from: voting }) + let receipt = await burner.requestBurnMyStETHForCover(StETH(8), { from: voting }) + + assert.emits(receipt, `StETHBurnRequested`, { + isCover: true, + requestedBy: voting, + amountOfStETH: StETH(8), + amountOfShares: sharesAmount8StETH, + }) + + // check stETH balances + assert.equals(await lido.balanceOf(burner.address), StETH(8)) + assert.equals(await lido.balanceOf(voting), StETH(16)) + + const sharesAmount12 = sharesAmount8StETH.mul(bn(3)).div(bn(2)) + await lido.approve(burner.address, StETH(13), { from: voting }) + receipt = await burner.requestBurnMyStETH(StETH(12), { from: voting }) + + assert.emits(receipt, `StETHBurnRequested`, { + isCover: false, + requestedBy: voting, + amountOfStETH: StETH(12), + amountOfShares: sharesAmount12, + }) + + // check stETH balances again, we didn't execute the actual burn + assert.equals(await lido.balanceOf(burner.address), StETH(20)) + assert.equals(await lido.balanceOf(voting), StETH(4)) + + await assert.revertsWithCustomError( + burner.commitSharesToBurn(StETH(100), { from: lido.address }), + `BurnAmountExceedsActual(${StETH(100)}, ${StETH(20)})` + ) + }) + it(`request shares burn for cover works`, async () => { // allowance should be set explicitly to request burning - await assert.reverts( - burner.requestBurnMyStETHForCover(StETH(8), { from: voting }), - `TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE` - ) + await assert.reverts(burner.requestBurnMyStETHForCover(StETH(8), { from: voting }), `ALLOWANCE_EXCEEDED`) // provide allowance and request burn for cover const sharesAmount8StETH = await lido.getSharesByPooledEth(StETH(8)) @@ -184,7 +322,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { await assert.revertsWithCustomError(burner.commitSharesToBurn(ETH(10)), `AppAuthLidoFailed()`) // mimic the Lido for the callback invocation - const receipt = await burner.commitSharesToBurn(ETH(10), { from: lido.address }) + const receipt = await burner.commitSharesToBurn(ETH(0), { from: lido.address }) // no burn requests => zero events assert.emitsNumberOfEvents(receipt, `StETHBurnt`, 0) @@ -218,7 +356,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { `AppAuthLidoFailed()` ) - const receipt = await burner.commitSharesToBurn(ETH(10), { from: lido.address }) + const receipt = await burner.commitSharesToBurn(ETH(6), { from: lido.address }) assert.emits(receipt, `StETHBurnt`, { isCover: false, amountOfStETH: StETH(6), amountOfShares: sharesToBurn }) @@ -270,7 +408,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { `AppAuthLidoFailed()` ) - const receipt = await burner.commitSharesToBurn(ETH(3), { from: lido.address }) + const receipt = await burner.commitSharesToBurn(ETH(2), { from: lido.address }) assert.emits(receipt, `StETHBurnt`, { isCover: true, @@ -323,7 +461,8 @@ contract('Burner', ([deployer, _, anotherAccount]) => { await burner.requestBurnMyStETHForCover(coverStETHAmountToBurn, { from: voting }) await burner.requestBurnMyStETH(nonCoverStETHAmountToBurn, { from: voting }) - await burner.commitSharesToBurn(ETH(1), { from: lido.address }) + const { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + await burner.commitSharesToBurn(coverShares.add(nonCoverShares), { from: lido.address }) // accumulate burnt shares expectedCoverSharesBurnt = expectedCoverSharesBurnt.add(currentCoverSharesAmount) @@ -344,7 +483,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { assert.equals(await lido.balanceOf(anotherAccount), StETH(20)) assert.equals(await lido.balanceOf(deployer), StETH(30)) - await burner.commitSharesToBurn(ETH(50), { from: lido.address }) + await burner.commitSharesToBurn(ETH(24), { from: lido.address }) await lido.burnShares(burner.address, await lido.getPooledEthByShares(StETH(24))) @@ -367,7 +506,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { await burner.requestBurnMyStETHForCover(StETH(0.9), { from: voting }) assert.equals(await lido.sharesOf(burner.address), stETHShares(0.9)) - const receipt = await burner.commitSharesToBurn(StETH(100), { from: lido.address }) + const receipt = await burner.commitSharesToBurn(StETH(0.9), { from: lido.address }) assert.emits(receipt, `StETHBurnt`, { isCover: true, @@ -381,7 +520,9 @@ contract('Burner', ([deployer, _, anotherAccount]) => { await burner.requestBurnMyStETHForCover(await lido.getPooledEthByShares(stETHShares(0.1)), { from: voting }) assert.equals(bnRound10(await lido.sharesOf(burner.address)), bnRound10(stETHShares(0.1))) - await burner.commitSharesToBurn(ETH(1), { from: lido.address }) + + const { coverShares, nonCoverShares } = await burner.getSharesRequestedToBurn() + await burner.commitSharesToBurn(coverShares.add(nonCoverShares), { from: lido.address }) await lido.burnShares(burner.address, await lido.sharesOf(burner.address)) assert.equals(await lido.sharesOf(burner.address), stETHShares(0)) @@ -471,7 +612,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { assert.equals(await burner.getCoverSharesBurnt(), stETHShares(0.1)) assert.equals(await burner.getNonCoverSharesBurnt(), stETHShares(0.4)) - const receipt2 = await burner.commitSharesToBurn(ETH(0.5), { from: lido.address }) + const receipt2 = await burner.commitSharesToBurn(ETH(0.4), { from: lido.address }) assert.emits(receipt2, `StETHBurnt`, { isCover: false, @@ -511,7 +652,7 @@ contract('Burner', ([deployer, _, anotherAccount]) => { assert.equals(await burner.getCoverSharesBurnt(), stETHShares(0.1)) assert.equals(await burner.getNonCoverSharesBurnt(), stETHShares(0.4)) - const receipt2 = await burner.commitSharesToBurn(ETH(0.5), { from: lido.address }) + const receipt2 = await burner.commitSharesToBurn(ETH(0.4), { from: lido.address }) assert.emits(receipt2, `StETHBurnt`, { isCover: false, diff --git a/test/0.8.9/deposit-security-module.test.js b/test/0.8.9/deposit-security-module.test.js index 023154b47..5ed520c05 100644 --- a/test/0.8.9/deposit-security-module.test.js +++ b/test/0.8.9/deposit-security-module.test.js @@ -1,6 +1,5 @@ const { artifacts, contract, ethers, network, web3 } = require('hardhat') const { assert } = require('../helpers/assert') -const { BN } = require('bn.js') const { DSMAttestMessage, DSMPauseMessage } = require('../helpers/signatures') const { ZERO_ADDRESS } = require('../helpers/constants') @@ -16,7 +15,6 @@ const MIN_DEPOSIT_BLOCK_DISTANCE = 14 const PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS = 10 const STAKING_MODULE = 123 -const UINT24_MAX = new BN(2).pow(new BN(24)) const DEPOSIT_CALLDATA = '0x000000000000000000000000000000000000000000000000000000000000002a' const GUARDIAN1 = '0x5Fc0E75BF6502009943590492B02A1d08EAc9C43' @@ -626,18 +624,6 @@ contract('DepositSecurityModule', ([owner, stranger, guardian]) => { ) }) - it('reverts when staking module too large', async () => { - await assert.revertsWithCustomError( - depositSecurityModule.pauseDeposits( - stalePauseMessage.blockNumber, - UINT24_MAX, - stalePauseMessage.sign(GUARDIAN_PRIVATE_KEYS[GUARDIAN2]), - { from: stranger } - ), - 'StakingModuleIdTooLarge()' - ) - }) - it('reverts if called by a guardian with a future blockNumber', async () => { const futureBlockNumber = block.number + 100 await assert.reverts( 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 15ed998ff..a3ef5b8d5 100644 --- a/test/0.8.9/oracle-report-sanity-checker.test.js +++ b/test/0.8.9/oracle-report-sanity-checker.test.js @@ -8,6 +8,7 @@ const LidoStub = artifacts.require(`${mocksFilePath}:LidoStub`) const OracleReportSanityChecker = artifacts.require('OracleReportSanityChecker') const LidoLocatorStub = artifacts.require(`${mocksFilePath}:LidoLocatorStub`) const WithdrawalQueueStub = artifacts.require(`${mocksFilePath}:WithdrawalQueueStub`) +const BurnerStub = artifacts.require(`${mocksFilePath}:BurnerStub`) function wei(number, units = 'wei') { switch (units.toLowerCase()) { @@ -21,7 +22,7 @@ function wei(number, units = 'wei') { } contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewardsVault, ...accounts]) => { - let oracleReportSanityChecker, lidoLocatorMock, lidoMock, withdrawalQueueMock + let oracleReportSanityChecker, lidoLocatorMock, lidoMock, withdrawalQueueMock, burnerMock const managersRoster = { allLimitsManagers: accounts.slice(0, 2), churnValidatorsPerDayLimitManagers: accounts.slice(2, 4), @@ -51,6 +52,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa postCLBalance: ETH(100_001), withdrawalVaultBalance: 0, elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, preCLValidators: 0, postCLValidators: 0, } @@ -60,11 +62,13 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa await ethers.provider.send('hardhat_mine', ['0x' + Number(1024).toString(16), '0x' + Number(12).toString(16)]) lidoMock = await LidoStub.new({ from: deployer }) withdrawalQueueMock = await WithdrawalQueueStub.new({ from: deployer }) + burnerMock = await BurnerStub.new({ from: deployer }) lidoLocatorMock = await LidoLocatorStub.new( lidoMock.address, withdrawalVault, withdrawalQueueMock.address, elRewardsVault, + burnerMock.address, { from: deployer } ) @@ -239,7 +243,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa const newRequestId = 2 let oldRequestCreationTimestamp, newRequestCreationTimestamp const correctWithdrawalQueueOracleReport = { - requestIdToFinalizeUpTo: oldRequestId, + lastFinalizableRequestId: oldRequestId, refReportTimestamp: -1, } @@ -247,10 +251,10 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa const currentBlockTimestamp = await getCurrentBlockTimestamp() correctWithdrawalQueueOracleReport.refReportTimestamp = currentBlockTimestamp oldRequestCreationTimestamp = currentBlockTimestamp - defaultLimitsList.requestTimestampMargin - correctWithdrawalQueueOracleReport.requestIdToFinalizeUpTo = oldRequestCreationTimestamp - await withdrawalQueueMock.setRequestBlockNumber(oldRequestId, oldRequestCreationTimestamp) + correctWithdrawalQueueOracleReport.lastFinalizableRequestId = oldRequestCreationTimestamp + await withdrawalQueueMock.setRequestTimestamp(oldRequestId, oldRequestCreationTimestamp) newRequestCreationTimestamp = currentBlockTimestamp - Math.floor(defaultLimitsList.requestTimestampMargin / 2) - await withdrawalQueueMock.setRequestBlockNumber(newRequestId, newRequestCreationTimestamp) + await withdrawalQueueMock.setRequestTimestamp(newRequestId, newRequestCreationTimestamp) }) it('reverts with the error IncorrectRequestFinalization() when the creation timestamp of requestIdToFinalizeUpTo is too close to report timestamp', async () => { @@ -258,7 +262,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa oracleReportSanityChecker.checkWithdrawalQueueOracleReport( ...Object.values({ ...correctWithdrawalQueueOracleReport, - requestIdToFinalizeUpTo: newRequestId, + lastFinalizableRequestId: newRequestId, }) ), `IncorrectRequestFinalization(${newRequestCreationTimestamp})` @@ -281,10 +285,9 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa simulatedShareRate: (BigInt(2) * 10n ** 27n).toString(), } - it('reverts with error IncorrectSimulatedShareRate() when reported and onchain share rate differs', async () => { + 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 - const deviation = (100_00n * (simulatedShareRate - actualShareRate)) / actualShareRate await assert.revertsWithCustomError( oracleReportSanityChecker.checkSimulatedShareRate( ...Object.values({ @@ -292,12 +295,25 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa simulatedShareRate: simulatedShareRate.toString(), }) ), - `IncorrectSimulatedShareRate(${deviation.toString()})` + `TooHighSimulatedShareRate(${simulatedShareRate.toString()}, ${actualShareRate.toString()})` ) }) - it('reverts with error IncorrectSimulatedShareRate() when actual share rate is zero', async () => { - const deviation = 100_00n + 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( + oracleReportSanityChecker.checkSimulatedShareRate( + ...Object.values({ + ...correctSimulatedShareRate, + simulatedShareRate: simulatedShareRate.toString(), + }) + ), + `TooLowSimulatedShareRate(${simulatedShareRate.toString()}, ${actualShareRate.toString()})` + ) + }) + + it('reverts with error ActualShareRateIsZero() when actual share rate is zero', async () => { await assert.revertsWithCustomError( oracleReportSanityChecker.checkSimulatedShareRate( ...Object.values({ @@ -306,7 +322,7 @@ contract('OracleReportSanityChecker', ([deployer, admin, withdrawalVault, elRewa postTotalPooledEther: ETH(0), }) ), - `IncorrectSimulatedShareRate(${deviation.toString()})` + `ActualShareRateIsZero()` ) }) diff --git a/test/0.8.9/oracle/accounting-oracle-access-control.test.js b/test/0.8.9/oracle/accounting-oracle-access-control.test.js index 4c3eff0da..9f6976448 100644 --- a/test/0.8.9/oracle/accounting-oracle-access-control.test.js +++ b/test/0.8.9/oracle/accounting-oracle-access-control.test.js @@ -5,11 +5,11 @@ const { e9, e18, e27 } = require('../../helpers/utils') const { CONSENSUS_VERSION, deployAndConfigureAccountingOracle, - getReportDataItems, + getAccountingReportDataItems, encodeExtraDataItems, packExtraDataList, calcExtraDataListHash, - calcReportDataHash, + calcAccountingReportDataHash, EXTRA_DATA_FORMAT_EMPTY, EXTRA_DATA_FORMAT_LIST, ZERO_HASH, @@ -54,15 +54,16 @@ contract('AccountingOracle', ([admin, account1, account2, member1, member2, stra numExitedValidatorsByStakingModule: [3], withdrawalVaultBalance: e18(1), elRewardsVaultBalance: e18(2), - lastWithdrawalRequestIdToFinalize: 1, - finalizationShareRate: e27(1), + sharesRequestedToBurn: e18(3), + withdrawalFinalizationBatches: [1], + simulatedShareRate: e27(1), isBunkerMode: true, extraDataFormat: emptyExtraData ? EXTRA_DATA_FORMAT_EMPTY : EXTRA_DATA_FORMAT_LIST, extraDataHash: emptyExtraData ? ZERO_HASH : extraDataHash, extraDataItemsCount: emptyExtraData ? 0 : extraDataItems.length, } - reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await deployed.consensus.addMember(member1, 1, { from: admin }) await deployed.consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) 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 380db9e49..1815151c9 100644 --- a/test/0.8.9/oracle/accounting-oracle-deploy.test.js +++ b/test/0.8.9/oracle/accounting-oracle-deploy.test.js @@ -1,11 +1,13 @@ -const { artifacts, contract, web3 } = require('hardhat') +const { ethers, artifacts, contract, web3 } = require('hardhat') const { ZERO_ADDRESS } = require('../../helpers/constants') const { assert } = require('../../helpers/assert') -const { hex } = require('../../helpers/utils') +const { hex, toBN } = require('../../helpers/utils') +const { EvmSnapshot } = require('../../helpers/blockchain') const { updateLocatorImplementation, deployLocatorWithDummyAddressesImplementation, } = require('../../helpers/locator-deploy') +const { calcAccountingReportDataHash, getAccountingReportDataItems } = require('../../helpers/reportData') const { SLOTS_PER_EPOCH, @@ -46,35 +48,6 @@ const EXTRA_DATA_FORMAT_LIST = 1 const EXTRA_DATA_TYPE_STUCK_VALIDATORS = 1 const EXTRA_DATA_TYPE_EXITED_VALIDATORS = 2 -function getReportDataItems(r) { - return [ - r.consensusVersion, - +r.refSlot, - r.numValidators, - r.clBalanceGwei, - r.stakingModuleIdsWithNewlyExitedValidators, - r.numExitedValidatorsByStakingModule, - r.withdrawalVaultBalance, - r.elRewardsVaultBalance, - r.lastWithdrawalRequestIdToFinalize, - r.finalizationShareRate, - r.isBunkerMode, - r.extraDataFormat, - r.extraDataHash, - r.extraDataItemsCount, - ] -} - -function calcReportDataHash(reportItems) { - const data = web3.eth.abi.encodeParameters( - [ - '(uint256,uint256,uint256,uint256,uint256[],uint256[],uint256,uint256,uint256,uint256,bool,uint256,bytes32,uint256)', - ], - [reportItems] - ) - return web3.utils.keccak256(data) -} - function encodeExtraDataItem(itemIndex, itemType, moduleId, nodeOperatorIds, keysCounts) { const itemHeader = hex(itemIndex, 3) + hex(itemType, 2) const payloadHeader = hex(moduleId, 3) + hex(nodeOperatorIds.length, 8) @@ -99,64 +72,7 @@ function packExtraDataList(extraDataItems) { function calcExtraDataListHash(packedExtraDataList) { return web3.utils.keccak256(packedExtraDataList) } -async function deployOracleReportSanityCheckerForAccounting(lidoLocator, admin) { - const churnValidatorsPerDayLimit = 100 - const limitsList = [churnValidatorsPerDayLimit, 0, 0, 0, 32 * 12, 15, 16, 0, 0] - const managersRoster = [[admin], [admin], [admin], [admin], [admin], [admin], [admin], [admin], [admin], [admin]] - - const OracleReportSanityChecker = artifacts.require('OracleReportSanityChecker') - - const oracleReportSanityChecker = await OracleReportSanityChecker.new( - lidoLocator, - admin, - limitsList, - managersRoster, - { - from: admin, - } - ) - return oracleReportSanityChecker -} -module.exports = { - SLOTS_PER_EPOCH, - SECONDS_PER_SLOT, - GENESIS_TIME, - SECONDS_PER_EPOCH, - EPOCHS_PER_FRAME, - SLOTS_PER_FRAME, - SECONDS_PER_FRAME, - ZERO_HASH, - HASH_1, - HASH_2, - HASH_3, - HASH_4, - HASH_5, - computeSlotAt, - computeEpochAt, - computeEpochFirstSlotAt, - computeEpochFirstSlot, - computeTimestampAtSlot, - computeTimestampAtEpoch, - CONSENSUS_VERSION, - V1_ORACLE_LAST_COMPLETED_EPOCH, - V1_ORACLE_LAST_REPORT_SLOT, - EXTRA_DATA_FORMAT_EMPTY, - EXTRA_DATA_FORMAT_LIST, - EXTRA_DATA_TYPE_STUCK_VALIDATORS, - EXTRA_DATA_TYPE_EXITED_VALIDATORS, - deployAndConfigureAccountingOracle, - deployAccountingOracleSetup, - initAccountingOracle, - deployMockLegacyOracle, - deployMockLidoAndStakingRouter, - getReportDataItems, - calcReportDataHash, - encodeExtraDataItem, - encodeExtraDataItems, - packExtraDataList, - calcExtraDataListHash, -} async function deployMockLegacyOracle({ epochsPerFrame = EPOCHS_PER_FRAME, slotsPerEpoch = SLOTS_PER_EPOCH, @@ -194,13 +110,6 @@ async function deployAccountingOracleSetup( const { lido, stakingRouter, withdrawalQueue } = await getLidoAndStakingRouter() const oracleReportSanityChecker = await deployOracleReportSanityCheckerForAccounting(locatorAddr, admin) - await updateLocatorImplementation(locatorAddr, admin, { - lido: lido.address, - stakingRouter: stakingRouter.address, - withdrawalQueue: withdrawalQueue.address, - oracleReportSanityChecker: oracleReportSanityChecker.address, - }) - const legacyOracle = await getLegacyOracle() if (initialEpoch == null) { @@ -224,6 +133,13 @@ async function deployAccountingOracleSetup( genesisTime, initialEpoch, }) + await updateLocatorImplementation(locatorAddr, admin, { + lido: lido.address, + stakingRouter: stakingRouter.address, + withdrawalQueue: withdrawalQueue.address, + oracleReportSanityChecker: oracleReportSanityChecker.address, + accountingOracle: oracle.address, + }) // pretend we're at the first slot of the initial frame's epoch await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot) @@ -246,8 +162,20 @@ async function initAccountingOracle({ consensus, dataSubmitter = null, consensusVersion = CONSENSUS_VERSION, + shouldMigrateLegacyOracle = true, + lastProcessingRefSlot, }) { - const initTx = await oracle.initialize(admin, consensus.address, consensusVersion, { from: admin }) + let initTx + if (shouldMigrateLegacyOracle) + initTx = await oracle.initialize(admin, consensus.address, consensusVersion, { from: admin }) + else + initTx = await oracle.initializeWithoutMigration( + admin, + consensus.address, + consensusVersion, + lastProcessingRefSlot, + { from: admin } + ) await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin, { from: admin }) await oracle.grantRole(await oracle.MANAGE_CONSENSUS_VERSION_ROLE(), admin, { from: admin }) @@ -264,10 +192,121 @@ async function initAccountingOracle({ return initTx } +async function deployOracleReportSanityCheckerForAccounting(lidoLocator, admin) { + const churnValidatorsPerDayLimit = 100 + const limitsList = [churnValidatorsPerDayLimit, 0, 0, 0, 32 * 12, 15, 16, 0, 0] + const managersRoster = [[admin], [admin], [admin], [admin], [admin], [admin], [admin], [admin], [admin], [admin]] + + const OracleReportSanityChecker = artifacts.require('OracleReportSanityChecker') + + const oracleReportSanityChecker = await OracleReportSanityChecker.new( + lidoLocator, + admin, + limitsList, + managersRoster, + { + from: admin, + } + ) + return oracleReportSanityChecker +} + +async function configureAccountingOracleSetup({ + admin, + consensus, + oracle, + legacyOracle, + dataSubmitter = null, + consensusVersion = CONSENSUS_VERSION, +} = {}) { + // this is done as a part of the protocol upgrade voting execution + + const frameConfig = await consensus.getFrameConfig() + // TODO: Double check it + await consensus.setTimeInEpochs(await legacyOracle.getLastCompletedEpochId()) + + const initialEpoch = +(await legacyOracle.getLastCompletedEpochId()) + +frameConfig.epochsPerFrame + + const updateInitialEpochIx = await consensus.updateInitialEpoch(initialEpoch, { from: admin }) + const initTx = await initAccountingOracle({ admin, oracle, consensus, dataSubmitter, consensusVersion }) + + return { updateInitialEpochIx, initTx } +} + async function deployAndConfigureAccountingOracle(admin) { + /// this is done (far) before the protocol upgrade voting initiation: + /// 1. deploy HashConsensus + /// 2. deploy AccountingOracle impl const deployed = await deployAccountingOracleSetup(admin) - const initTx = await initAccountingOracle({ admin, ...deployed }) - return { ...deployed, initTx } + + // pretend we're after the legacy oracle's last proc epoch but before the new oracle's initial epoch + assert.isAbove(EPOCHS_PER_FRAME, 1) + const voteExecTime = GENESIS_TIME + (V1_ORACLE_LAST_COMPLETED_EPOCH + 1) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT + await deployed.consensus.setTime(voteExecTime) + + /// this is done as a part of the protocol upgrade voting execution: + /// 1. calculate HashConsensus initial epoch as the last finalized legacy epoch + frame size + /// 2. set HashConsensus initial epoch + /// 3. deploy AccountingOracle proxy (skipped in these tests as they're not testing the proxy setup) + /// 4. initialize AccountingOracle + const finalizeResult = await configureAccountingOracleSetup({ admin, ...deployed }) + + // pretend we're at the first slot of the new oracle's initial epoch + const initialEpoch = V1_ORACLE_LAST_COMPLETED_EPOCH + EPOCHS_PER_FRAME + await deployed.consensus.setTime(GENESIS_TIME + initialEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + + return { ...deployed, ...finalizeResult } +} + +async function getInitialFrameStartTime(consensus) { + const chainConfig = await consensus.getChainConfig() + const frameConfig = await consensus.getFrameConfig() + return toBN(frameConfig.initialEpoch) + .muln(+chainConfig.slotsPerEpoch) + .muln(+chainConfig.secondsPerSlot) + .add(toBN(chainConfig.genesisTime)) +} + +module.exports = { + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SECONDS_PER_EPOCH, + EPOCHS_PER_FRAME, + SLOTS_PER_FRAME, + SECONDS_PER_FRAME, + ZERO_HASH, + HASH_1, + HASH_2, + HASH_3, + HASH_4, + HASH_5, + computeSlotAt, + computeEpochAt, + computeEpochFirstSlotAt, + computeEpochFirstSlot, + computeTimestampAtSlot, + computeTimestampAtEpoch, + CONSENSUS_VERSION, + V1_ORACLE_LAST_COMPLETED_EPOCH, + V1_ORACLE_LAST_REPORT_SLOT, + EXTRA_DATA_FORMAT_EMPTY, + EXTRA_DATA_FORMAT_LIST, + EXTRA_DATA_TYPE_STUCK_VALIDATORS, + EXTRA_DATA_TYPE_EXITED_VALIDATORS, + deployAndConfigureAccountingOracle, + deployAccountingOracleSetup, + initAccountingOracle, + configureAccountingOracleSetup, + deployMockLegacyOracle, + deployMockLidoAndStakingRouter, + getAccountingReportDataItems, + calcAccountingReportDataHash, + encodeExtraDataItem, + encodeExtraDataItems, + packExtraDataList, + calcExtraDataListHash, + getInitialFrameStartTime, } contract('AccountingOracle', ([admin, member1]) => { @@ -279,20 +318,30 @@ contract('AccountingOracle', ([admin, member1]) => { let legacyOracle context('Deployment and initial configuration', () => { + const updateInitialEpoch = async (consensus) => { + // pretend we're after the legacy oracle's last proc epoch but before the new oracle's initial epoch + const voteExecTime = GENESIS_TIME + (V1_ORACLE_LAST_COMPLETED_EPOCH + 1) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT + await consensus.setTime(voteExecTime) + await consensus.updateInitialEpoch(V1_ORACLE_LAST_COMPLETED_EPOCH + EPOCHS_PER_FRAME) + } + it('init fails if the chain config is different from the one of the legacy oracle', async () => { let deployed = await deployAccountingOracleSetup(admin, { getLegacyOracle: () => deployMockLegacyOracle({ slotsPerEpoch: SLOTS_PER_EPOCH + 1 }), }) + await updateInitialEpoch(deployed.consensus) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(0)') deployed = await deployAccountingOracleSetup(admin, { getLegacyOracle: () => deployMockLegacyOracle({ secondsPerSlot: SECONDS_PER_SLOT + 1 }), }) + await updateInitialEpoch(deployed.consensus) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(0)') deployed = await deployAccountingOracleSetup(admin, { getLegacyOracle: () => deployMockLegacyOracle({ genesisTime: GENESIS_TIME + 1 }), }) + await updateInitialEpoch(deployed.consensus) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(0)') }) @@ -300,26 +349,41 @@ contract('AccountingOracle', ([admin, member1]) => { const deployed = await deployAccountingOracleSetup(admin, { getLegacyOracle: () => deployMockLegacyOracle({ epochsPerFrame: EPOCHS_PER_FRAME - 1 }), }) + await updateInitialEpoch(deployed.consensus) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(1)') }) it(`init fails if the initial epoch of the new oracle is not the next frame's first epoch`, async () => { - const deployed = await deployAccountingOracleSetup(admin, { initialEpoch: 3 + 10 * EPOCHS_PER_FRAME }) + const deployed = await deployAccountingOracleSetup(admin) + + const voteExecTime = GENESIS_TIME + (V1_ORACLE_LAST_COMPLETED_EPOCH + 1) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT + await deployed.consensus.setTime(voteExecTime) + + const snapshot = new EvmSnapshot(ethers.provider) + await snapshot.make() - await deployed.legacyOracle.setLastCompletedEpochId(3 + 11 * EPOCHS_PER_FRAME) + await deployed.consensus.updateInitialEpoch(V1_ORACLE_LAST_COMPLETED_EPOCH + EPOCHS_PER_FRAME - 1) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(2)') + await snapshot.rollback() - await deployed.legacyOracle.setLastCompletedEpochId(3 + 10 * EPOCHS_PER_FRAME) + await deployed.consensus.updateInitialEpoch(V1_ORACLE_LAST_COMPLETED_EPOCH + EPOCHS_PER_FRAME + 1) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(2)') + await snapshot.rollback() - await deployed.legacyOracle.setLastCompletedEpochId(3 + 9 * EPOCHS_PER_FRAME + 1) + await deployed.consensus.updateInitialEpoch(V1_ORACLE_LAST_COMPLETED_EPOCH + 2 * EPOCHS_PER_FRAME) await assert.reverts(initAccountingOracle({ admin, ...deployed }), 'IncorrectOracleMigration(2)') + await snapshot.rollback() }) it('deployment and init finishes successfully otherwise', async () => { - const deployed = await deployAccountingOracleSetup(admin, { initialEpoch: 3 + 10 * EPOCHS_PER_FRAME }) - await deployed.legacyOracle.setLastCompletedEpochId(3 + 9 * EPOCHS_PER_FRAME) + const deployed = await deployAccountingOracleSetup(admin) + + const voteExecTime = GENESIS_TIME + (V1_ORACLE_LAST_COMPLETED_EPOCH + 1) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT + await deployed.consensus.setTime(voteExecTime) + await deployed.consensus.updateInitialEpoch(V1_ORACLE_LAST_COMPLETED_EPOCH + EPOCHS_PER_FRAME) + await initAccountingOracle({ admin, ...deployed }) + const refSlot = await deployed.oracle.getLastProcessingRefSlot() const epoch = await deployed.legacyOracle.getLastCompletedEpochId() assert.equals(refSlot, epoch.muln(SLOTS_PER_EPOCH)) @@ -355,8 +419,8 @@ contract('AccountingOracle', ([admin, member1]) => { assert.equals(await mockStakingRouter.totalCalls_reportExitedKeysByNodeOperator(), 0) assert.equals(await mockStakingRouter.totalCalls_reportStuckKeysByNodeOperator(), 0) - const updateBunkerModeLastCall = await mockWithdrawalQueue.lastCall__updateBunkerMode() - assert.equals(updateBunkerModeLastCall.callCount, 0) + const onOracleReportLastCall = await mockWithdrawalQueue.lastCall__onOracleReport() + assert.equals(onOracleReportLastCall.callCount, 0) }) it('the initial reference slot is greater than the last one of the legacy oracle', async () => { @@ -371,14 +435,14 @@ contract('AccountingOracle', ([admin, member1]) => { assert.equals(await oracle.SECONDS_PER_SLOT(), SECONDS_PER_SLOT) }) - it('reverts if lido locator address is zero', async () => { + it('constructor reverts if lido locator address is zero', async () => { await assert.reverts( deployAccountingOracleSetup(admin, { lidoLocatorAddr: ZERO_ADDRESS }), 'LidoLocatorCannotBeZero()' ) }) - it('reverts if legacy oracle address is zero', async () => { + it('constructor reverts if legacy oracle address is zero', async () => { await assert.reverts( deployAccountingOracleSetup(admin, { legacyOracleAddr: ZERO_ADDRESS }), 'LegacyOracleCannotBeZero()' @@ -386,26 +450,30 @@ contract('AccountingOracle', ([admin, member1]) => { }) it('initialize reverts if admin address is zero', async () => { - const { consensus } = await deployAccountingOracleSetup(admin) + const deployed = await deployAccountingOracleSetup(admin) + await updateInitialEpoch(deployed.consensus) await assert.reverts( - oracle.initialize(ZERO_ADDRESS, consensus.address, CONSENSUS_VERSION, { from: admin }), + deployed.oracle.initialize(ZERO_ADDRESS, deployed.consensus.address, CONSENSUS_VERSION, { from: admin }), 'AdminCannotBeZero()' ) }) it('initializeWithoutMigration reverts if admin address is zero', async () => { - const { consensus } = await deployAccountingOracleSetup(admin) - const { refSlot } = await consensus.getCurrentFrame() + const deployed = await deployAccountingOracleSetup(admin) + await updateInitialEpoch(deployed.consensus) + await assert.reverts( - oracle.initializeWithoutMigration(ZERO_ADDRESS, consensus.address, CONSENSUS_VERSION, refSlot, { from: admin }), + deployed.oracle.initializeWithoutMigration(ZERO_ADDRESS, deployed.consensus.address, CONSENSUS_VERSION, 0, { + from: admin, + }), 'AdminCannotBeZero()' ) }) - it('initializeWithoutMigration succeeds', async () => { + it('initializeWithoutMigration succeeds otherwise', async () => { const deployed = await deployAccountingOracleSetup(admin) - const { refSlot } = await deployed.consensus.getCurrentFrame() - await deployed.oracle.initializeWithoutMigration(admin, deployed.consensus.address, CONSENSUS_VERSION, refSlot, { + await updateInitialEpoch(deployed.consensus) + await deployed.oracle.initializeWithoutMigration(admin, deployed.consensus.address, CONSENSUS_VERSION, 0, { from: admin, }) }) diff --git a/test/0.8.9/oracle/accounting-oracle-happy-path.test.js b/test/0.8.9/oracle/accounting-oracle-happy-path.test.js index 539033bac..ee64fc827 100644 --- a/test/0.8.9/oracle/accounting-oracle-happy-path.test.js +++ b/test/0.8.9/oracle/accounting-oracle-happy-path.test.js @@ -1,6 +1,6 @@ const { contract } = require('hardhat') const { assert } = require('../../helpers/assert') -const { e9, e18, e27, hex } = require('../../helpers/utils') +const { e9, e18, e27, hex, toNum } = require('../../helpers/utils') const { SECONDS_PER_SLOT, @@ -15,8 +15,8 @@ const { EXTRA_DATA_FORMAT_EMPTY, EXTRA_DATA_FORMAT_LIST, deployAndConfigureAccountingOracle, - getReportDataItems, - calcReportDataHash, + getAccountingReportDataItems, + calcAccountingReportDataHash, encodeExtraDataItems, packExtraDataList, calcExtraDataListHash, @@ -119,16 +119,17 @@ contract('AccountingOracle', ([admin, member1, member2, member3, stranger]) => { numExitedValidatorsByStakingModule: [3], withdrawalVaultBalance: e18(1), elRewardsVaultBalance: e18(2), - lastWithdrawalRequestIdToFinalize: 1, - finalizationShareRate: e27(1), + sharesRequestedToBurn: e18(3), + withdrawalFinalizationBatches: [1], + simulatedShareRate: e27(1), isBunkerMode: true, extraDataFormat: EXTRA_DATA_FORMAT_LIST, extraDataHash, extraDataItemsCount: extraDataItems.length, } - reportItems = getReportDataItems(reportFields) - reportHash = calcReportDataHash(reportItems) + reportItems = getAccountingReportDataItems(reportFields) + reportHash = calcAccountingReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) }) @@ -174,8 +175,8 @@ contract('AccountingOracle', ([admin, member1, member2, member3, stranger]) => { it(`a data not matching the consensus hash cannot be submitted`, async () => { const invalidReport = { ...reportFields, numValidators: reportFields.numValidators + 1 } - const invalidReportItems = getReportDataItems(invalidReport) - const invalidReportHash = calcReportDataHash(invalidReportItems) + const invalidReportItems = getAccountingReportDataItems(invalidReport) + const invalidReportHash = calcAccountingReportDataHash(invalidReportItems) await assert.reverts( oracle.submitReportData(invalidReportItems, oracleVersion, { from: member1 }), `UnexpectedDataHash("${reportHash}", "${invalidReportHash}")` @@ -218,21 +219,18 @@ contract('AccountingOracle', ([admin, member1, member2, member3, stranger]) => { assert.equals(lastOracleReportCall.clBalance, e9(reportFields.clBalanceGwei)) assert.equals(lastOracleReportCall.withdrawalVaultBalance, reportFields.withdrawalVaultBalance) assert.equals(lastOracleReportCall.elRewardsVaultBalance, reportFields.elRewardsVaultBalance) - assert.equals( - lastOracleReportCall.lastWithdrawalRequestIdToFinalize, - reportFields.lastWithdrawalRequestIdToFinalize + assert.sameOrderedMembers( + toNum(lastOracleReportCall.withdrawalFinalizationBatches), + toNum(reportFields.withdrawalFinalizationBatches) ) - assert.equals(lastOracleReportCall.finalizationShareRate, reportFields.finalizationShareRate) + assert.equals(lastOracleReportCall.simulatedShareRate, reportFields.simulatedShareRate) }) it(`withdrawal queue got bunker mode report`, async () => { - const updateBunkerModeLastCall = await mockWithdrawalQueue.lastCall__updateBunkerMode() - assert.equals(updateBunkerModeLastCall.callCount, 1) - assert.equals(updateBunkerModeLastCall.isBunkerMode, reportFields.isBunkerMode) - assert.equal( - +updateBunkerModeLastCall.prevReportTimestamp, - GENESIS_TIME + prevProcessingRefSlot * SECONDS_PER_SLOT - ) + const onOracleReportLastCall = await mockWithdrawalQueue.lastCall__onOracleReport() + assert.equals(onOracleReportLastCall.callCount, 1) + assert.equals(onOracleReportLastCall.isBunkerMode, reportFields.isBunkerMode) + assert.equal(+onOracleReportLastCall.prevReportTimestamp, GENESIS_TIME + prevProcessingRefSlot * SECONDS_PER_SLOT) }) it(`Staking router got the exited keys report`, async () => { @@ -393,8 +391,8 @@ contract('AccountingOracle', ([admin, member1, member2, member3, stranger]) => { extraDataHash: ZERO_HASH, extraDataItemsCount: 0, } - reportItems = getReportDataItems(reportFields) - reportHash = calcReportDataHash(reportItems) + reportItems = getAccountingReportDataItems(reportFields) + reportHash = calcAccountingReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) @@ -407,9 +405,9 @@ contract('AccountingOracle', ([admin, member1, member2, member3, stranger]) => { assert.equal(lastOracleReportCall.callCount, 2) }) - it(`withdrawal queue got bunker mode report`, async () => { - const updateBunkerModeLastCall = await mockWithdrawalQueue.lastCall__updateBunkerMode() - assert.equals(updateBunkerModeLastCall.callCount, 2) + it(`withdrawal queue got their part of report`, async () => { + const onOracleReportLastCall = await mockWithdrawalQueue.lastCall__onOracleReport() + assert.equals(onOracleReportLastCall.callCount, 2) }) it(`Staking router got the exited keys report`, async () => { 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 3818656a4..7778696fb 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 @@ -1,17 +1,17 @@ const { contract, web3 } = require('hardhat') const { assert } = require('../../helpers/assert') -const { e9, e18, e27 } = require('../../helpers/utils') +const { e9, e18, e27, toNum } = require('../../helpers/utils') const AccountingOracleAbi = require('../../../lib/abi/AccountingOracle.json') const { CONSENSUS_VERSION, deployAndConfigureAccountingOracle, - getReportDataItems, + getAccountingReportDataItems, encodeExtraDataItems, packExtraDataList, calcExtraDataListHash, - calcReportDataHash, + calcAccountingReportDataHash, EXTRA_DATA_FORMAT_LIST, EXTRA_DATA_FORMAT_EMPTY, SLOTS_PER_FRAME, @@ -46,8 +46,9 @@ contract('AccountingOracle', ([admin, member1]) => { numExitedValidatorsByStakingModule: [3], withdrawalVaultBalance: e18(1), elRewardsVaultBalance: e18(2), - lastWithdrawalRequestIdToFinalize: 1, - finalizationShareRate: e27(1), + sharesRequestedToBurn: e18(3), + withdrawalFinalizationBatches: [1], + simulatedShareRate: e27(1), isBunkerMode: true, extraDataFormat: EXTRA_DATA_FORMAT_LIST, extraDataHash, @@ -77,8 +78,8 @@ contract('AccountingOracle', ([admin, member1]) => { reportFields = getReportFields({ refSlot: +refSlot, }) - reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await deployed.consensus.addMember(member1, 1, { from: admin }) await deployed.consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) @@ -97,8 +98,8 @@ contract('AccountingOracle', ([admin, member1]) => { async function prepareNextReport(newReportFields) { await consensus.setTime(deadline) - const newReportItems = getReportDataItems(newReportFields) - const reportHash = calcReportDataHash(newReportItems) + const newReportItems = getAccountingReportDataItems(newReportFields) + const reportHash = calcAccountingReportDataHash(newReportItems) await consensus.advanceTimeToNextFrameStart() await consensus.submitReport(newReportFields.refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) @@ -177,7 +178,7 @@ contract('AccountingOracle', ([admin, member1]) => { ...reportFields, refSlot: incorrectRefSlot, } - const reportItems = getReportDataItems(newReportFields) + const reportItems = getAccountingReportDataItems(newReportFields) await assert.reverts( oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), @@ -216,10 +217,10 @@ contract('AccountingOracle', ([admin, member1]) => { ...reportFields, consensusVersion: incorrectNextVersion, } - const reportItems = getReportDataItems(newReportFields) + const reportItems = getAccountingReportDataItems(newReportFields) const reportFieldsPrevVersion = { ...reportFields, consensusVersion: incorrectPrevVersion } - const reportItemsPrevVersion = getReportDataItems(reportFieldsPrevVersion) + const reportItemsPrevVersion = getAccountingReportDataItems(reportFieldsPrevVersion) await assert.reverts( oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), @@ -246,8 +247,8 @@ contract('AccountingOracle', ([admin, member1]) => { refSlot: nextRefSlot, consensusVersion: newConsensusVersion, } - const newReportItems = getReportDataItems(newReportFields) - const newReportHash = calcReportDataHash(newReportItems) + const newReportItems = getAccountingReportDataItems(newReportFields) + const newReportHash = calcAccountingReportDataHash(newReportItems) await oracle.setConsensusVersion(newConsensusVersion, { from: admin }) await consensus.advanceTimeToNextFrameStart() @@ -301,13 +302,13 @@ contract('AccountingOracle', ([admin, member1]) => { context('checks data hash', () => { it('reverts with UnexpectedDataHash', async () => { - const incorrectReportItems = getReportDataItems({ + const incorrectReportItems = getAccountingReportDataItems({ ...reportFields, numValidators: reportFields.numValidators - 1, }) - const correctDataHash = calcReportDataHash(reportItems) - const incorrectDataHash = calcReportDataHash(incorrectReportItems) + const correctDataHash = calcAccountingReportDataHash(reportItems) + const incorrectDataHash = calcAccountingReportDataHash(incorrectReportItems) await assert.reverts( oracle.submitReportData(incorrectReportItems, oracleVersion, { from: member1 }), @@ -438,11 +439,11 @@ contract('AccountingOracle', ([admin, member1]) => { assert.equals(lastOracleReportToLido.clBalance, reportFields.clBalanceGwei + '000000000') assert.equals(lastOracleReportToLido.withdrawalVaultBalance, reportFields.withdrawalVaultBalance) assert.equals(lastOracleReportToLido.elRewardsVaultBalance, reportFields.elRewardsVaultBalance) - assert.equals( - lastOracleReportToLido.lastWithdrawalRequestIdToFinalize, - reportFields.lastWithdrawalRequestIdToFinalize + assert.sameOrderedMembers( + toNum(lastOracleReportToLido.withdrawalFinalizationBatches), + toNum(reportFields.withdrawalFinalizationBatches) ) - assert.equals(lastOracleReportToLido.finalizationShareRate, reportFields.finalizationShareRate) + assert.equals(lastOracleReportToLido.simulatedShareRate, reportFields.simulatedShareRate) }) it('should call updateExitedValidatorsCountByStakingModule on StakingRouter', async () => { @@ -479,13 +480,15 @@ contract('AccountingOracle', ([admin, member1]) => { assert.equals(lastCall.clValidators, reportFields.numValidators) }) - it('should call updateBunkerMode on WithdrawalQueue', async () => { + it('should call onOracleReport on WithdrawalQueue', async () => { const prevProcessingRefSlot = +(await oracle.getLastProcessingRefSlot()) await oracle.submitReportData(reportItems, oracleVersion, { from: member1 }) - const lastCall = await mockWithdrawalQueue.lastCall__updateBunkerMode() + const currentProcessingRefSlot = +(await oracle.getLastProcessingRefSlot()) + const lastCall = await mockWithdrawalQueue.lastCall__onOracleReport() assert.equals(lastCall.callCount, 1) assert.equals(lastCall.isBunkerMode, reportFields.isBunkerMode) assert.equals(lastCall.prevReportTimestamp, GENESIS_TIME + prevProcessingRefSlot * SECONDS_PER_SLOT) + assert.equals(lastCall.currentReportTimestamp, GENESIS_TIME + currentProcessingRefSlot * SECONDS_PER_SLOT) }) }) @@ -516,13 +519,13 @@ contract('AccountingOracle', ([admin, member1]) => { await oracle.submitReportData(reportItems, oracleVersion, { from: member1 }) const nextRefSlot = reportFields.refSlot + SLOTS_PER_FRAME - const changedReportItems = getReportDataItems({ + const changedReportItems = getAccountingReportDataItems({ ...reportFields, refSlot: nextRefSlot, extraDataFormat: EXTRA_DATA_FORMAT_LIST + 1, }) - const changedReportHash = calcReportDataHash(changedReportItems) + const changedReportHash = calcAccountingReportDataHash(changedReportItems) await consensus.advanceTimeToNextFrameStart() await consensus.submitReport(nextRefSlot, changedReportHash, CONSENSUS_VERSION, { from: member1, @@ -541,8 +544,8 @@ contract('AccountingOracle', ([admin, member1]) => { refSlot: +refSlot, extraDataItemsCount: 0, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) await assert.revertsWithCustomError( oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), @@ -557,8 +560,8 @@ contract('AccountingOracle', ([admin, member1]) => { refSlot: +refSlot, extraDataHash: ZERO_HASH, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) await assert.revertsWithCustomError( oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), @@ -579,8 +582,8 @@ contract('AccountingOracle', ([admin, member1]) => { extraDataHash: nonZeroHash, extraDataItemsCount: 0, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) await assert.revertsWithCustomError( oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), @@ -598,8 +601,8 @@ contract('AccountingOracle', ([admin, member1]) => { extraDataHash: ZERO_HASH, extraDataItemsCount: 10, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) await assert.revertsWithCustomError( oracle.submitReportData(reportItems, oracleVersion, { from: member1 }), diff --git a/test/0.8.9/oracle/accounting-oracle-submit-report-extra-data.test.js b/test/0.8.9/oracle/accounting-oracle-submit-report-extra-data.test.js index abe68f109..2b32fd6a7 100644 --- a/test/0.8.9/oracle/accounting-oracle-submit-report-extra-data.test.js +++ b/test/0.8.9/oracle/accounting-oracle-submit-report-extra-data.test.js @@ -5,12 +5,12 @@ const { e9, e18, e27, hex } = require('../../helpers/utils') const { CONSENSUS_VERSION, deployAndConfigureAccountingOracle, - getReportDataItems, + getAccountingReportDataItems, encodeExtraDataItem, encodeExtraDataItems, packExtraDataList, calcExtraDataListHash, - calcReportDataHash, + calcAccountingReportDataHash, ZERO_HASH, EXTRA_DATA_FORMAT_EMPTY, EXTRA_DATA_FORMAT_LIST, @@ -37,8 +37,9 @@ const getDefaultReportFields = (overrides) => ({ numExitedValidatorsByStakingModule: [3], withdrawalVaultBalance: e18(1), elRewardsVaultBalance: e18(2), - lastWithdrawalRequestIdToFinalize: 1, - finalizationShareRate: e27(1), + sharesRequestedToBurn: e18(3), + withdrawalFinalizationBatches: [1], + simulatedShareRate: e27(1), isBunkerMode: true, extraDataFormat: EXTRA_DATA_FORMAT_LIST, // required override: refSlot, @@ -81,8 +82,8 @@ contract('AccountingOracle', ([admin, account1, account2, member1, member2, stra ...reportFieldsArg, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) return { extraData, @@ -533,8 +534,8 @@ contract('AccountingOracle', ([admin, account1, account2, member1, member2, stra refSlot, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getAccountingReportDataItems(reportFields) + const reportHash = calcAccountingReportDataHash(reportItems) await consensus.submitReport(reportFields.refSlot, reportHash, CONSENSUS_VERSION, { from: member1, diff --git a/test/0.8.9/oracle/base-oracle-access-control.test.js b/test/0.8.9/oracle/base-oracle-access-control.test.js index eb6a4ff73..ed526424e 100644 --- a/test/0.8.9/oracle/base-oracle-access-control.test.js +++ b/test/0.8.9/oracle/base-oracle-access-control.test.js @@ -57,8 +57,8 @@ contract('BaseOracle', ([admin, account1, account2, member1, member2]) => { SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME, - INITIAL_FAST_LANE_LENGTH_SLOTS, INITIAL_EPOCH, + INITIAL_FAST_LANE_LENGTH_SLOTS, admin, { from: admin } ) diff --git a/test/0.8.9/oracle/base-oracle-deploy.test.js b/test/0.8.9/oracle/base-oracle-deploy.test.js index 067039e90..126bd7bb7 100644 --- a/test/0.8.9/oracle/base-oracle-deploy.test.js +++ b/test/0.8.9/oracle/base-oracle-deploy.test.js @@ -21,6 +21,8 @@ const computeEpochFirstSlot = (epoch) => epoch * SLOTS_PER_EPOCH const computeEpochFirstSlotAt = (time) => computeEpochFirstSlot(computeEpochAt(time)) const computeTimestampAtEpoch = (epoch) => GENESIS_TIME + epoch * SECONDS_PER_EPOCH const computeTimestampAtSlot = (slot) => GENESIS_TIME + slot * SECONDS_PER_SLOT +const computeDeadlineFromRefSlot = (slot) => computeTimestampAtSlot(+slot + SLOTS_PER_FRAME) +const computeNextRefSlotFromRefSlot = (slot) => +slot + SLOTS_PER_FRAME const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' @@ -53,8 +55,8 @@ async function deployBaseOracle( secondsPerSlot, genesisTime, epochsPerFrame, - fastLaneLengthSlots, initialEpoch, + fastLaneLengthSlots, mockMember, { from: admin } ) @@ -85,6 +87,8 @@ module.exports = { computeEpochFirstSlotAt, computeTimestampAtSlot, computeTimestampAtEpoch, + computeNextRefSlotFromRefSlot, + computeDeadlineFromRefSlot, ZERO_HASH, HASH_1, HASH_2, diff --git a/test/0.8.9/oracle/base-oracle-set-consensus.test.js b/test/0.8.9/oracle/base-oracle-set-consensus.test.js index 28e857dee..eea85b00e 100644 --- a/test/0.8.9/oracle/base-oracle-set-consensus.test.js +++ b/test/0.8.9/oracle/base-oracle-set-consensus.test.js @@ -1,6 +1,7 @@ -const { contract, artifacts, web3 } = require('hardhat') +const { contract, artifacts, web3, ethers } = require('hardhat') const { assert } = require('../../helpers/assert') const { ZERO_ADDRESS } = require('../../helpers/constants') +const { EvmSnapshot } = require('../../helpers/blockchain') const MockConsensusContract = artifacts.require('MockConsensusContract') @@ -9,14 +10,16 @@ const { SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME, - SLOTS_PER_FRAME, HASH_1, HASH_2, CONSENSUS_VERSION, + computeEpochFirstSlotAt, + computeDeadlineFromRefSlot, deployBaseOracle, } = require('./base-oracle-deploy.test') contract('BaseOracle', ([admin, member, notMember]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) let consensus let baseOracle let initialRefSlot @@ -27,11 +30,16 @@ contract('BaseOracle', ([admin, member, notMember]) => { baseOracle = deployed.oracle await baseOracle.grantRole(web3.utils.keccak256('MANAGE_CONSENSUS_CONTRACT_ROLE'), admin, { from: admin }) await baseOracle.grantRole(web3.utils.keccak256('MANAGE_CONSENSUS_VERSION_ROLE'), admin, { from: admin }) - initialRefSlot = +(await baseOracle.getTime()) + const time = (await baseOracle.getTime()).toNumber() + initialRefSlot = computeEpochFirstSlotAt(time) + await evmSnapshot.make() } + const rollback = async () => evmSnapshot.rollback() + + before(deployContract) describe('setConsensusContract safely changes used consensus contract', () => { - before(deployContract) + before(rollback) it('reverts on zero address', async () => { await assert.revertsWithCustomError(baseOracle.setConsensusContract(ZERO_ADDRESS), 'AddressCannotBeZero()') @@ -52,7 +60,7 @@ contract('BaseOracle', ([admin, member, notMember]) => { SECONDS_PER_SLOT + 1, GENESIS_TIME + 1, EPOCHS_PER_FRAME, - 0, + 1, 0, admin, { from: admin } @@ -63,24 +71,28 @@ contract('BaseOracle', ([admin, member, notMember]) => { ) }) - it('reverts on consensus current frame behind current processing', async () => { + it('reverts on consensus initial ref slot behind currently processing', async () => { + const processingRefSlot = 100 + + await consensus.submitReportAsConsensus(HASH_1, processingRefSlot, +(await baseOracle.getTime()) + 1) + await baseOracle.startProcessing() + const wrongConsensusContract = await MockConsensusContract.new( SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME, - 0, + 1, 0, admin, { from: admin } ) - await wrongConsensusContract.setCurrentFrame(10, 1, 2000) - await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, initialRefSlot + SLOTS_PER_FRAME) - await baseOracle.startProcessing() + + await wrongConsensusContract.setInitialRefSlot(processingRefSlot - 1) await assert.revertsWithCustomError( baseOracle.setConsensusContract(wrongConsensusContract.address), - `RefSlotCannotBeLessThanProcessingOne(1, ${initialRefSlot})` + `InitialRefSlotCannotBeLessThanProcessingOne(${processingRefSlot - 1}, ${processingRefSlot})` ) }) @@ -90,12 +102,12 @@ contract('BaseOracle', ([admin, member, notMember]) => { SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME, - 0, + 1, 0, admin, { from: admin } ) - await newConsensusContract.setCurrentFrame(10, initialRefSlot + 1, initialRefSlot + SLOTS_PER_FRAME) + await newConsensusContract.setInitialRefSlot(initialRefSlot) const tx = await baseOracle.setConsensusContract(newConsensusContract.address) assert.emits(tx, 'ConsensusHashContractSet', { addr: newConsensusContract.address, prevAddr: consensus.address }) const addressAtStorage = await baseOracle.getConsensusContract() @@ -104,7 +116,7 @@ contract('BaseOracle', ([admin, member, notMember]) => { }) describe('setConsensusVersion updates contract state', () => { - before(deployContract) + before(rollback) it('reverts on same version', async () => { await assert.revertsWithCustomError(baseOracle.setConsensusVersion(CONSENSUS_VERSION), 'VersionCannotBeSame()') @@ -119,19 +131,22 @@ contract('BaseOracle', ([admin, member, notMember]) => { }) describe('_checkConsensusData checks provided data against internal state', () => { - before(deployContract) + before(rollback) + let deadline it('report is submitted', async () => { - await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, initialRefSlot + 10) + deadline = computeDeadlineFromRefSlot(initialRefSlot) + await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, deadline) }) it('deadline missed on current ref slot, reverts on any arguments', async () => { - await baseOracle.advanceTimeBy(11) + const oldTime = await baseOracle.getTime() + await baseOracle.setTime(deadline + 10) await assert.revertsWithCustomError( baseOracle.checkConsensusData(initialRefSlot, CONSENSUS_VERSION, HASH_1), - `ProcessingDeadlineMissed(${initialRefSlot + 10})` + `ProcessingDeadlineMissed(${deadline})` ) - await baseOracle.setTime(initialRefSlot) + await baseOracle.setTime(oldTime) }) it('reverts on mismatched slot', async () => { @@ -161,7 +176,7 @@ contract('BaseOracle', ([admin, member, notMember]) => { }) describe('_isConsensusMember correctly check address for consensus membership trough consensus contract', () => { - before(deployContract) + before(rollback) it('returns false on non member', async () => { const r = await baseOracle.isConsensusMember(notMember) @@ -175,7 +190,7 @@ contract('BaseOracle', ([admin, member, notMember]) => { }) describe('_getCurrentRefSlot correctly gets refSlot trough consensus contract', () => { - before(deployContract) + before(rollback) it('refSlot matches', async () => { const oracle_slot = await baseOracle.getCurrentRefSlot() diff --git a/test/0.8.9/oracle/base-oracle-submit-report.test.js b/test/0.8.9/oracle/base-oracle-submit-report.test.js index 82ce4ba84..1ebd65b75 100644 --- a/test/0.8.9/oracle/base-oracle-submit-report.test.js +++ b/test/0.8.9/oracle/base-oracle-submit-report.test.js @@ -1,11 +1,23 @@ -const { contract } = require('hardhat') +const { contract, ethers } = require('hardhat') const { assert } = require('../../helpers/assert') +const { EvmSnapshot } = require('../../helpers/blockchain') const baseOracleAbi = require('../../../lib/abi/BaseOracle.json') -const { SLOTS_PER_FRAME, ZERO_HASH, HASH_1, HASH_2, HASH_3, deployBaseOracle } = require('./base-oracle-deploy.test') +const { + ZERO_HASH, + HASH_1, + HASH_2, + HASH_3, + deployBaseOracle, + computeDeadlineFromRefSlot, + computeNextRefSlotFromRefSlot, + computeEpochFirstSlotAt, + SECONDS_PER_SLOT, +} = require('./base-oracle-deploy.test') contract('BaseOracle', ([admin]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) let consensus let baseOracle let initialRefSlot @@ -14,30 +26,40 @@ contract('BaseOracle', ([admin]) => { const deployed = await deployBaseOracle(admin, { initialEpoch: 1 }) consensus = deployed.consensusContract baseOracle = deployed.oracle - initialRefSlot = +(await baseOracle.getTime()) + const time = (await baseOracle.getTime()).toNumber() + initialRefSlot = computeEpochFirstSlotAt(time) + await evmSnapshot.make() } + before(deployContract) + describe('submitConsensusReport is called and changes the contract state', () => { context('submitConsensusReport passes pre-conditions', () => { - before(deployContract) + before(async () => { + await evmSnapshot.rollback() + }) it('only setConsensus contract can call submitConsensusReport', async () => { await assert.revertsWithCustomError( - baseOracle.submitConsensusReport(HASH_1, initialRefSlot, initialRefSlot + SLOTS_PER_FRAME), + baseOracle.submitConsensusReport(HASH_1, initialRefSlot, computeDeadlineFromRefSlot(initialRefSlot)), 'OnlyConsensusContractCanSubmitReport()' ) }) it('initial report is submitted and _handleConsensusReport is called', async () => { assert.equals((await baseOracle.getConsensusReportLastCall()).callCount, 0) - const tx = await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, initialRefSlot + SLOTS_PER_FRAME) + const tx = await consensus.submitReportAsConsensus( + HASH_1, + initialRefSlot, + computeDeadlineFromRefSlot(initialRefSlot) + ) assert.emits( tx, 'ReportSubmitted', { refSlot: initialRefSlot, hash: HASH_1, - processingDeadlineTime: initialRefSlot + SLOTS_PER_FRAME, + processingDeadlineTime: computeDeadlineFromRefSlot(initialRefSlot), }, { abi: baseOracleAbi } ) @@ -45,12 +67,12 @@ contract('BaseOracle', ([admin]) => { assert.equals(callCount, 1) assert.equal(report.hash, HASH_1) assert.equals(report.refSlot, initialRefSlot) - assert.equals(report.processingDeadlineTime, initialRefSlot + SLOTS_PER_FRAME) + assert.equals(report.processingDeadlineTime, computeDeadlineFromRefSlot(initialRefSlot)) }) it('older report cannot be submitted', async () => { await assert.revertsWithCustomError( - consensus.submitReportAsConsensus(HASH_1, initialRefSlot - 1, initialRefSlot + SLOTS_PER_FRAME), + consensus.submitReportAsConsensus(HASH_1, initialRefSlot - 1, computeDeadlineFromRefSlot(initialRefSlot)), `RefSlotCannotDecrease(${initialRefSlot - 1}, ${initialRefSlot})` ) }) @@ -61,29 +83,24 @@ contract('BaseOracle', ([admin]) => { it('consensus cannot resubmit already processing report', async () => { await assert.revertsWithCustomError( - consensus.submitReportAsConsensus(HASH_1, initialRefSlot, initialRefSlot + SLOTS_PER_FRAME), + consensus.submitReportAsConsensus(HASH_1, initialRefSlot, computeDeadlineFromRefSlot(initialRefSlot)), `RefSlotMustBeGreaterThanProcessingOne(${initialRefSlot}, ${initialRefSlot})` ) }) it('warning event is emitted when newer report is submitted and prev has not started processing yet', async () => { - const tx1 = await consensus.submitReportAsConsensus( - HASH_1, - initialRefSlot + 10, - initialRefSlot + SLOTS_PER_FRAME - ) + const RefSlot2 = computeNextRefSlotFromRefSlot(initialRefSlot) + const RefSlot3 = computeNextRefSlotFromRefSlot(RefSlot2) + + const tx1 = await consensus.submitReportAsConsensus(HASH_1, RefSlot2, computeDeadlineFromRefSlot(RefSlot2)) assert.equals((await baseOracle.getConsensusReportLastCall()).callCount, 2) assert.emits(tx1, 'ReportSubmitted', {}, { abi: baseOracleAbi }) - const tx2 = await consensus.submitReportAsConsensus( - HASH_1, - initialRefSlot + 20, - initialRefSlot + SLOTS_PER_FRAME - ) + const tx2 = await consensus.submitReportAsConsensus(HASH_1, RefSlot3, computeDeadlineFromRefSlot(RefSlot3)) assert.emits( tx2, 'WarnProcessingMissed', - { refSlot: initialRefSlot + 10 }, + { refSlot: RefSlot2 }, { abi: baseOracleAbi, } @@ -94,7 +111,12 @@ contract('BaseOracle', ([admin]) => { }) context('submitConsensusReport updates getConsensusReport', () => { - before(deployContract) + let nextRefSlot, nextRefSlotDeadline + before(async () => { + nextRefSlot = computeNextRefSlotFromRefSlot(initialRefSlot) + nextRefSlotDeadline = computeDeadlineFromRefSlot(nextRefSlot) + await evmSnapshot.rollback() + }) it('getConsensusReport at deploy returns empty state', async () => { const report = await baseOracle.getConsensusReport() @@ -105,17 +127,16 @@ contract('BaseOracle', ([admin]) => { }) it('initial report is submitted', async () => { - await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, initialRefSlot + SLOTS_PER_FRAME) + await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, computeDeadlineFromRefSlot(initialRefSlot)) const report = await baseOracle.getConsensusReport() assert.equal(report.hash, HASH_1) assert.equals(report.refSlot, initialRefSlot) - assert.equals(report.processingDeadlineTime, initialRefSlot + SLOTS_PER_FRAME) + assert.equals(report.processingDeadlineTime, computeDeadlineFromRefSlot(initialRefSlot)) assert(!report.processingStarted) }) it('next report is submitted, initial report is missed, warning event fired', async () => { - const nextRefSlot = initialRefSlot + 1 - const tx = await consensus.submitReportAsConsensus(HASH_2, nextRefSlot, nextRefSlot + SLOTS_PER_FRAME) + const tx = await consensus.submitReportAsConsensus(HASH_2, nextRefSlot, nextRefSlotDeadline) assert.emits( tx, 'WarnProcessingMissed', @@ -127,44 +148,51 @@ contract('BaseOracle', ([admin]) => { const report = await baseOracle.getConsensusReport() assert.equal(report.hash, HASH_2) assert.equals(report.refSlot, nextRefSlot) - assert.equals(report.processingDeadlineTime, nextRefSlot + SLOTS_PER_FRAME) + assert.equals(report.processingDeadlineTime, nextRefSlotDeadline) assert(!report.processingStarted) }) it('next report is re-agreed, no missed warning', async () => { - const nextRefSlot = initialRefSlot + 1 - const tx = await consensus.submitReportAsConsensus(HASH_3, nextRefSlot, nextRefSlot + SLOTS_PER_FRAME + 10) + const tx = await consensus.submitReportAsConsensus(HASH_3, nextRefSlot, nextRefSlotDeadline) assert.emitsNumberOfEvents(tx, 'WarnProcessingMissed', 0, { abi: baseOracleAbi, }) const report = await baseOracle.getConsensusReport() assert.equal(report.hash, HASH_3) assert.equals(report.refSlot, nextRefSlot) - assert.equals(report.processingDeadlineTime, nextRefSlot + SLOTS_PER_FRAME + 10) + assert.equals(report.processingDeadlineTime, nextRefSlotDeadline) assert(!report.processingStarted) }) it('report processing started for last report', async () => { - const nextRefSlot = initialRefSlot + 1 await baseOracle.startProcessing() const report = await baseOracle.getConsensusReport() assert.equal(report.hash, HASH_3) assert.equals(report.refSlot, nextRefSlot) - assert.equals(report.processingDeadlineTime, nextRefSlot + SLOTS_PER_FRAME + 10) + assert.equals(report.processingDeadlineTime, nextRefSlotDeadline) assert(report.processingStarted) }) }) }) describe('_startProcessing safely advances processing state', () => { - before(deployContract) + let refSlot1, refSlot2 + let refSlot1Deadline, refSlot2Deadline + before(async () => { + await evmSnapshot.rollback() + refSlot1 = computeNextRefSlotFromRefSlot(initialRefSlot) + refSlot1Deadline = computeDeadlineFromRefSlot(refSlot1) + + refSlot2 = computeNextRefSlotFromRefSlot(refSlot1) + refSlot2Deadline = computeDeadlineFromRefSlot(refSlot2) + }) it('initial contract state, no reports, cannot startProcessing', async () => { await assert.revertsWithCustomError(baseOracle.startProcessing(), 'ProcessingDeadlineMissed(0)') }) it('submit first report for initial slot', async () => { - await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, initialRefSlot + 20) + await consensus.submitReportAsConsensus(HASH_1, initialRefSlot, computeDeadlineFromRefSlot(initialRefSlot)) const tx = await baseOracle.startProcessing() assert.emits(tx, 'ProcessingStarted', { refSlot: initialRefSlot, hash: HASH_1 }) assert.emits(tx, 'MockStartProcessingResult', { prevProcessingRefSlot: '0' }) @@ -175,19 +203,18 @@ contract('BaseOracle', ([admin]) => { }) it('next report comes in, start processing, state advances', async () => { - await consensus.submitReportAsConsensus(HASH_2, initialRefSlot + 10, initialRefSlot + 20) + await consensus.submitReportAsConsensus(HASH_2, refSlot1, refSlot1Deadline) const tx = await baseOracle.startProcessing() - assert.emits(tx, 'ProcessingStarted', { refSlot: initialRefSlot + 10, hash: HASH_2 }) + assert.emits(tx, 'ProcessingStarted', { refSlot: refSlot1, hash: HASH_2 }) assert.emits(tx, 'MockStartProcessingResult', { prevProcessingRefSlot: String(initialRefSlot) }) const processingSlot = await baseOracle.getLastProcessingRefSlot() - assert.equals(processingSlot, initialRefSlot + 10) + assert.equals(processingSlot, refSlot1) }) it('another report but deadline is missed, reverts', async () => { - const nextSlot = initialRefSlot + 20 - await consensus.submitReportAsConsensus(HASH_3, nextSlot, nextSlot + 30) - await baseOracle.setTime(nextSlot + 40) - await assert.revertsWithCustomError(baseOracle.startProcessing(), `ProcessingDeadlineMissed(${nextSlot + 30})`) + await consensus.submitReportAsConsensus(HASH_3, refSlot2, refSlot2Deadline) + await baseOracle.setTime(refSlot2Deadline + SECONDS_PER_SLOT * 10) + await assert.revertsWithCustomError(baseOracle.startProcessing(), `ProcessingDeadlineMissed(${refSlot2Deadline})`) }) }) }) diff --git a/test/0.8.9/oracle/hash-consensus-access-control.test.js b/test/0.8.9/oracle/hash-consensus-access-control.test.js index 5a6577a6c..d3857ad13 100644 --- a/test/0.8.9/oracle/hash-consensus-access-control.test.js +++ b/test/0.8.9/oracle/hash-consensus-access-control.test.js @@ -1,6 +1,7 @@ const { ethers, contract, web3, artifacts } = require('hardhat') const { MaxUint256 } = require('@ethersproject/constants') +const { ZERO_BYTES32 } = require('../../helpers/constants') const { assert } = require('../../helpers/assert') const { EvmSnapshot } = require('../../helpers/blockchain') @@ -31,6 +32,42 @@ contract('HashConsensus', ([admin, account1, account2, member1, member2]) => { await snapshot.make() } + context('DEFAULT_ADMIN_ROLE', () => { + const DEFAULT_ADMIN_ROLE = ZERO_BYTES32 + + before(async () => { + await deploy({ initialEpoch: null }) + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + context('updateInitialEpoch', () => { + it('reverts when called without DEFAULT_ADMIN_ROLE', async () => { + assert.revertsOZAccessControl( + consensus.updateInitialEpoch(10, { from: account1 }), + account1, + 'DEFAULT_ADMIN_ROLE' + ) + + await consensus.grantRole(manageFrameConfigRoleKeccak156, account2) + + assert.revertsOZAccessControl( + consensus.updateInitialEpoch(10, { from: account2 }), + account2, + 'DEFAULT_ADMIN_ROLE' + ) + }) + + it('allows calling from a possessor of DEFAULT_ADMIN_ROLE role', async () => { + await consensus.grantRole(DEFAULT_ADMIN_ROLE, account2, { from: admin }) + await consensus.updateInitialEpoch(10, { from: account2 }) + assert.equals((await consensus.getFrameConfig()).initialEpoch, 10) + }) + }) + }) + context('deploying', () => { before(deploy) diff --git a/test/0.8.9/oracle/hash-consensus-deploy.test.js b/test/0.8.9/oracle/hash-consensus-deploy.test.js index 36885b604..fb67f085d 100644 --- a/test/0.8.9/oracle/hash-consensus-deploy.test.js +++ b/test/0.8.9/oracle/hash-consensus-deploy.test.js @@ -56,14 +56,16 @@ async function deployHashConsensus( secondsPerSlot, genesisTime, epochsPerFrame, - initialEpoch, fastLaneLengthSlots, admin, reportProcessor.address, { from: admin } ) - await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot) + if (initialEpoch !== null) { + await consensus.updateInitialEpoch(initialEpoch, { from: admin }) + await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot) + } await consensus.grantRole(await consensus.MANAGE_MEMBERS_AND_QUORUM_ROLE(), admin, { from: admin }) await consensus.grantRole(await consensus.DISABLE_CONSENSUS_ROLE(), admin, { from: admin }) @@ -104,7 +106,6 @@ module.exports = { contract('HashConsensus', ([admin, member1]) => { context('Deployment and initial configuration', () => { const INITIAL_EPOCH = 3 - let consensus it('deploying hash consensus', async () => { @@ -132,7 +133,6 @@ contract('HashConsensus', ([admin, member1]) => { SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME, - INITIAL_EPOCH, INITIAL_FAST_LANE_LENGTH_SLOTS, admin, ZERO_ADDRESS, @@ -150,7 +150,6 @@ contract('HashConsensus', ([admin, member1]) => { SECONDS_PER_SLOT, GENESIS_TIME, EPOCHS_PER_FRAME, - INITIAL_EPOCH, INITIAL_FAST_LANE_LENGTH_SLOTS, ZERO_ADDRESS, reportProcessor.address, diff --git a/test/0.8.9/oracle/hash-consensus-frames.test.js b/test/0.8.9/oracle/hash-consensus-frames.test.js index 8be4232da..d1120ccbd 100644 --- a/test/0.8.9/oracle/hash-consensus-frames.test.js +++ b/test/0.8.9/oracle/hash-consensus-frames.test.js @@ -1,9 +1,11 @@ const { contract } = require('hardhat') const { assert } = require('../../helpers/assert') +const { toBN } = require('../../helpers/utils') const { INITIAL_FAST_LANE_LENGTH_SLOTS, INITIAL_EPOCH, + GENESIS_TIME, EPOCHS_PER_FRAME, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, @@ -93,11 +95,65 @@ contract('HashConsensus', ([admin, member1, member2]) => { }) context('State before initial epoch', () => { - let consensus + let consensus, reportProcessor before(async () => { - const deployed = await deployHashConsensus(admin, { initialEpoch: TEST_INITIAL_EPOCH }) + const deployed = await deployHashConsensus(admin, { initialEpoch: null }) consensus = deployed.consensus + reportProcessor = deployed.reportProcessor + }) + + it(`after deploy, the initial epoch is far in the future`, async () => { + const maxTimestamp = toBN(2).pow(toBN(64)).subn(1) + const maxEpoch = maxTimestamp.subn(GENESIS_TIME).divn(SECONDS_PER_SLOT).divn(SLOTS_PER_EPOCH) + assert.equals((await consensus.getFrameConfig()).initialEpoch, maxEpoch) + + const initialRefSlot = await consensus.getInitialRefSlot() + assert.equals(initialRefSlot, maxEpoch.muln(SLOTS_PER_EPOCH).subn(1)) + }) + + it(`after deploy, one can update initial epoch`, async () => { + const tx = await consensus.updateInitialEpoch(TEST_INITIAL_EPOCH, { from: admin }) + + assert.emits(tx, 'FrameConfigSet', { + newEpochsPerFrame: EPOCHS_PER_FRAME, + newInitialEpoch: TEST_INITIAL_EPOCH, + }) + + const frameConfig = await consensus.getFrameConfig() + assert.equals(frameConfig.initialEpoch, TEST_INITIAL_EPOCH) + assert.equals(frameConfig.epochsPerFrame, EPOCHS_PER_FRAME) + assert.equals(frameConfig.fastLaneLengthSlots, INITIAL_FAST_LANE_LENGTH_SLOTS) + + const initialRefSlot = await consensus.getInitialRefSlot() + assert.equals(initialRefSlot, +frameConfig.initialEpoch * SLOTS_PER_EPOCH - 1) + }) + + it(`one cannot update initial epoch so that initial ref slot is less than processed one`, async () => { + await consensus.setTimeInEpochs(TEST_INITIAL_EPOCH - 2) + + const initialRefSlot = TEST_INITIAL_EPOCH * SLOTS_PER_EPOCH - 1 + await reportProcessor.setLastProcessingStartedRefSlot(initialRefSlot + 1) + + assert.reverts( + consensus.updateInitialEpoch(TEST_INITIAL_EPOCH, { from: admin }), + 'InitialEpochRefSlotCannotBeEarlierThanProcessingSlot()' + ) + + await reportProcessor.setLastProcessingStartedRefSlot(0) + }) + + it(`before the initial epoch arrives, one can update it freely`, async () => { + await consensus.setTimeInEpochs(TEST_INITIAL_EPOCH - 2) + + await consensus.updateInitialEpoch(TEST_INITIAL_EPOCH - 1, { from: admin }) + assert.equals((await consensus.getFrameConfig()).initialEpoch, TEST_INITIAL_EPOCH - 1) + + await consensus.updateInitialEpoch(TEST_INITIAL_EPOCH, { from: admin }) + assert.equals((await consensus.getFrameConfig()).initialEpoch, TEST_INITIAL_EPOCH) + + const initialRefSlot = await consensus.getInitialRefSlot() + assert.equals(initialRefSlot, TEST_INITIAL_EPOCH * SLOTS_PER_EPOCH - 1) }) it('before the initial epoch arrives, members can be added and queried, and quorum increased', async () => { @@ -158,6 +214,17 @@ contract('HashConsensus', ([admin, member1, member2]) => { const tx = await consensus.submitReport(frame.refSlot, HASH_1, CONSENSUS_VERSION, { from: member1 }) assert.emits(tx, 'ReportReceived', { refSlot: frame.refSlot, member: member1, report: HASH_1 }) }) + + it('after the initial epoch comes, updating it via updateInitialEpoch is not possible anymore', async () => { + await assert.reverts( + consensus.updateInitialEpoch(TEST_INITIAL_EPOCH + 1, { from: admin }), + 'InitialEpochAlreadyArrived()' + ) + await assert.reverts( + consensus.updateInitialEpoch(TEST_INITIAL_EPOCH - 1, { from: admin }), + 'InitialEpochAlreadyArrived()' + ) + }) }) context('Reporting interval manipulation', () => { diff --git a/test/0.8.9/oracle/validators-exit-bus-oracle-access-control.test.js b/test/0.8.9/oracle/validators-exit-bus-oracle-access-control.test.js index dca98c7f8..319e305f4 100644 --- a/test/0.8.9/oracle/validators-exit-bus-oracle-access-control.test.js +++ b/test/0.8.9/oracle/validators-exit-bus-oracle-access-control.test.js @@ -5,8 +5,8 @@ const { ZERO_ADDRESS } = require('../../helpers/constants') const { CONSENSUS_VERSION, DATA_FORMAT_LIST, - getReportDataItems, - calcReportDataHash, + getValidatorsExitBusReportDataItems, + calcValidatorsExitBusReportDataHash, encodeExitRequestsDataList, deployExitBusOracle, } = require('./validators-exit-bus-oracle-deploy.test') @@ -60,8 +60,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, account1 data: encodeExitRequestsDataList(exitRequests), }) - reportItems = getReportDataItems(reportFields) - reportHash = calcReportDataHash(reportItems) + reportItems = getValidatorsExitBusReportDataItems(reportFields) + reportHash = calcValidatorsExitBusReportDataHash(reportItems) await deployed.consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member1 }) await deployed.consensus.submitReport(refSlot, reportHash, CONSENSUS_VERSION, { from: member3 }) @@ -88,14 +88,12 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, account1 context('Admin is set at initialize', () => { it('should set admin at initialize', async () => { const DEFAULT_ADMIN_ROLE = await oracle.DEFAULT_ADMIN_ROLE() - await assert.emits(initTx, 'RoleGranted', { role: DEFAULT_ADMIN_ROLE, account: admin, sender: admin }) + assert.emits(initTx, 'RoleGranted', { role: DEFAULT_ADMIN_ROLE, account: admin, sender: admin }) }) it('should revert without admin address', async () => { - const pauser = ZERO_ADDRESS - const resumer = ZERO_ADDRESS await assert.reverts( - oracle.initialize(ZERO_ADDRESS, pauser, resumer, consensus.address, CONSENSUS_VERSION, 0, { + oracle.initialize(ZERO_ADDRESS, consensus.address, CONSENSUS_VERSION, 0, { from: admin, }), 'AdminCannotBeZero()' @@ -135,13 +133,13 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, account1 context('pause', () => { it('should revert without PAUSE_ROLE role', async () => { - await assert.revertsOZAccessControl(oracle.pause(0, { from: stranger }), stranger, 'PAUSE_ROLE') + await assert.revertsOZAccessControl(oracle.pauseFor(0, { from: stranger }), stranger, 'PAUSE_ROLE') }) it('should allow calling from a possessor of PAUSE_ROLE role', async () => { await oracle.grantRole(pauseRoleKeccak156, account1) - const tx = await oracle.pause(9999, { from: account1 }) + const tx = await oracle.pauseFor(9999, { from: account1 }) assert.emits(tx, 'Paused', { duration: 9999 }) }) }) @@ -152,13 +150,13 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, account1 context('resume', () => { it('should revert without RESUME_ROLE role', async () => { - await oracle.pause(9999, { from: admin }) + await oracle.pauseFor(9999, { from: admin }) await assert.revertsOZAccessControl(oracle.resume({ from: stranger }), stranger, 'RESUME_ROLE') }) it('should allow calling from a possessor of RESUME_ROLE role', async () => { - await oracle.pause(9999, { from: admin }) + await oracle.pauseFor(9999, { from: admin }) await oracle.grantRole(resumeRoleKeccak156, account1) const tx = await oracle.resume({ from: account1 }) diff --git a/test/0.8.9/oracle/validators-exit-bus-oracle-deploy.test.js b/test/0.8.9/oracle/validators-exit-bus-oracle-deploy.test.js index 69066b197..b69b28e8f 100644 --- a/test/0.8.9/oracle/validators-exit-bus-oracle-deploy.test.js +++ b/test/0.8.9/oracle/validators-exit-bus-oracle-deploy.test.js @@ -1,4 +1,4 @@ -const { contract, artifacts, web3 } = require('hardhat') +const { contract, artifacts } = require('hardhat') const { assert } = require('../../helpers/assert') const { hex, strip0x } = require('../../helpers/utils') const { ZERO_ADDRESS } = require('../../helpers/constants') @@ -23,17 +23,11 @@ const { deployHashConsensus, } = require('./hash-consensus-deploy.test') +const { calcValidatorsExitBusReportDataHash, getValidatorsExitBusReportDataItems } = require('../../helpers/reportData') + const ValidatorsExitBusOracle = artifacts.require('ValidatorsExitBusTimeTravellable') const DATA_FORMAT_LIST = 1 -function getReportDataItems(r) { - return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data] -} - -function calcReportDataHash(reportItems) { - const data = web3.eth.abi.encodeParameters(['(uint256,uint256,uint256,uint256,bytes)'], [reportItems]) - return web3.utils.keccak256(data) -} function encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey }) { const pubkeyHex = strip0x(valPubkey) @@ -72,8 +66,8 @@ module.exports = { ZERO_HASH, CONSENSUS_VERSION, DATA_FORMAT_LIST, - getReportDataItems, - calcReportDataHash, + getValidatorsExitBusReportDataItems, + calcValidatorsExitBusReportDataHash, encodeExitRequestHex, encodeExitRequestsDataList, deployExitBusOracle, @@ -100,13 +94,7 @@ async function deployOracleReportSanityCheckerForExitBus(lidoLocator, admin) { async function deployExitBusOracle( admin, - { - dataSubmitter = null, - lastProcessingRefSlot = 0, - resumeAfterDeploy = false, - pauser = ZERO_ADDRESS, - resumer = ZERO_ADDRESS, - } = {} + { dataSubmitter = null, lastProcessingRefSlot = 0, resumeAfterDeploy = false } = {} ) { const locator = (await deployLocatorWithDummyAddressesImplementation(admin)).address @@ -123,15 +111,9 @@ async function deployExitBusOracle( oracleReportSanityChecker: oracleReportSanityChecker.address, }) - const initTx = await oracle.initialize( - admin, - pauser, - resumer, - consensus.address, - CONSENSUS_VERSION, - lastProcessingRefSlot, - { from: admin } - ) + const initTx = await oracle.initialize(admin, consensus.address, CONSENSUS_VERSION, lastProcessingRefSlot, { + from: admin, + }) assert.emits(initTx, 'ContractVersionSet', { version: 1 }) @@ -198,7 +180,7 @@ contract('ValidatorsExitBusOracle', ([admin, member1]) => { assert.equal(await oracle.isPaused(), true) await oracle.resume() assert.equal(await oracle.isPaused(), false) - await oracle.pause(123) + await oracle.pauseFor(123) assert.equal(await oracle.isPaused(), true) }) }) diff --git a/test/0.8.9/oracle/validators-exit-bus-oracle-gas.test.js b/test/0.8.9/oracle/validators-exit-bus-oracle-gas.test.js index ddc91d4ef..8361c317a 100644 --- a/test/0.8.9/oracle/validators-exit-bus-oracle-gas.test.js +++ b/test/0.8.9/oracle/validators-exit-bus-oracle-gas.test.js @@ -8,8 +8,8 @@ const { ZERO_HASH, CONSENSUS_VERSION, DATA_FORMAT_LIST, - getReportDataItems, - calcReportDataHash, + getValidatorsExitBusReportDataItems, + calcValidatorsExitBusReportDataHash, encodeExitRequestsDataList, deployExitBusOracle, } = require('./validators-exit-bus-oracle-deploy.test') @@ -116,8 +116,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger data: encodeExitRequestsDataList(exitRequests.requests), } - reportItems = getReportDataItems(reportFields) - reportHash = calcReportDataHash(reportItems) + reportItems = getValidatorsExitBusReportDataItems(reportFields) + reportHash = calcValidatorsExitBusReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) }) diff --git a/test/0.8.9/oracle/validators-exit-bus-oracle-happy-path.test.js b/test/0.8.9/oracle/validators-exit-bus-oracle-happy-path.test.js index ae553285d..535872a04 100644 --- a/test/0.8.9/oracle/validators-exit-bus-oracle-happy-path.test.js +++ b/test/0.8.9/oracle/validators-exit-bus-oracle-happy-path.test.js @@ -9,8 +9,8 @@ const { ZERO_HASH, CONSENSUS_VERSION, DATA_FORMAT_LIST, - getReportDataItems, - calcReportDataHash, + getValidatorsExitBusReportDataItems, + calcValidatorsExitBusReportDataHash, encodeExitRequestsDataList, deployExitBusOracle, } = require('./validators-exit-bus-oracle-deploy.test') @@ -100,8 +100,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger data: encodeExitRequestsDataList(exitRequests), } - reportItems = getReportDataItems(reportFields) - reportHash = calcReportDataHash(reportItems) + reportItems = getValidatorsExitBusReportDataItems(reportFields) + reportHash = calcValidatorsExitBusReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) }) @@ -153,8 +153,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger it(`a data not matching the consensus hash cannot be submitted`, async () => { const invalidReport = { ...reportFields, requestsCount: reportFields.requestsCount + 1 } - const invalidReportItems = getReportDataItems(invalidReport) - const invalidReportHash = calcReportDataHash(invalidReportItems) + const invalidReportItems = getValidatorsExitBusReportDataItems(invalidReport) + const invalidReportHash = calcValidatorsExitBusReportDataHash(invalidReportItems) await assert.reverts( oracle.submitReportData(invalidReportItems, oracleVersion, { from: member1 }), `UnexpectedDataHash("${reportHash}", "${invalidReportHash}")` diff --git a/test/0.8.9/oracle/validators-exit-bus-oracle-submit-report-data.test.js b/test/0.8.9/oracle/validators-exit-bus-oracle-submit-report-data.test.js index f7140cb26..45f611c18 100644 --- a/test/0.8.9/oracle/validators-exit-bus-oracle-submit-report-data.test.js +++ b/test/0.8.9/oracle/validators-exit-bus-oracle-submit-report-data.test.js @@ -4,8 +4,8 @@ const { assert } = require('../../helpers/assert') const { CONSENSUS_VERSION, DATA_FORMAT_LIST, - getReportDataItems, - calcReportDataHash, + getValidatorsExitBusReportDataItems, + calcValidatorsExitBusReportDataHash, encodeExitRequestsDataList, deployExitBusOracle, computeTimestampAtSlot, @@ -84,8 +84,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger ...reportFieldsArg, }) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getValidatorsExitBusReportDataItems(reportFields) + const reportHash = calcValidatorsExitBusReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) @@ -146,8 +146,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger }) reportFields.data += 'aaaaaaaaaaaaaaaaaa' - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getValidatorsExitBusReportDataItems(reportFields) + const reportHash = calcValidatorsExitBusReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) await assert.reverts( @@ -166,8 +166,8 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger }) reportFields.data = reportFields.data.slice(0, reportFields.data.length - 18) - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const reportItems = getValidatorsExitBusReportDataItems(reportFields) + const reportHash = calcValidatorsExitBusReportDataHash(reportItems) await triggerConsensusOnHash(reportHash) await assert.reverts( @@ -429,7 +429,7 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger it('reverts on paused contract', async () => { await consensus.advanceTimeToNextFrameStart() const PAUSE_INFINITELY = await oracle.PAUSE_INFINITELY() - await oracle.pause(PAUSE_INFINITELY, { from: admin }) + await oracle.pauseFor(PAUSE_INFINITELY, { from: admin }) const report = await prepareReportAndSubmitHash() assert.reverts(oracle.submitReportData(report, oracleVersion, { from: member1 }), 'ResumedExpected()') }) @@ -448,10 +448,10 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger it('reverts on hash mismatch', async () => { const report = await prepareReportAndSubmitHash() - const actualReportHash = calcReportDataHash(report) + const actualReportHash = calcValidatorsExitBusReportDataHash(report) // mess with data field to change hash report[report.length - 1] = report[report.length - 1] + 'ff' - const changedReportHash = calcReportDataHash(report) + const changedReportHash = calcValidatorsExitBusReportDataHash(report) await assert.reverts( oracle.submitReportData(report, oracleVersion, { from: member1 }), `UnexpectedDataHash("${actualReportHash}", "${changedReportHash}")` @@ -531,7 +531,7 @@ contract('ValidatorsExitBusOracle', ([admin, member1, member2, member3, stranger { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }, { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[3] }, ]) - hash = calcReportDataHash(report) + hash = calcValidatorsExitBusReportDataHash(report) const state = await oracle.getProcessingState() assertEqualsObject(state, { currentFrameRefSlot: (await consensus.getCurrentFrame()).refSlot, diff --git a/test/0.8.9/pausable-until.test.js b/test/0.8.9/pausable-until.test.js index 136dab74d..97e966cc6 100644 --- a/test/0.8.9/pausable-until.test.js +++ b/test/0.8.9/pausable-until.test.js @@ -32,7 +32,7 @@ contract('PausableUntil', ([deployer]) => { assert.equals(await pausable.getResumeSinceTimestamp(), resumeSinceTimestamp) } - await assert.revertsWithCustomError(pausable.pause(12345), `ResumedExpected()`) + await assert.revertsWithCustomError(pausable.pauseFor(12345), `ResumedExpected()`) await assert.revertsWithCustomError(pausable.stubUnderModifierWhenResumed(), `ResumedExpected()`) } @@ -52,14 +52,16 @@ contract('PausableUntil', ([deployer]) => { }) it(`revert if pause for zero duration`, async () => { - await assert.revertsWithCustomError(pausable.pause(0), `ZeroPauseDuration()`) + await assert.revertsWithCustomError(pausable.pauseFor(0), `ZeroPauseDuration()`) }) it(`pause infinitely`, async () => { await assertResumedState() const MONTH_IN_SECS = 30 * 24 * 60 * 60 - await pausable.pause(PAUSE_INFINITELY) + const tx = await pausable.pauseFor(PAUSE_INFINITELY) + assert.emits(tx, 'Paused', { duration: PAUSE_INFINITELY }) + await assertPausedState(PAUSE_INFINITELY) await advanceChainTime(MONTH_IN_SECS) @@ -69,11 +71,13 @@ contract('PausableUntil', ([deployer]) => { await assertPausedState(PAUSE_INFINITELY) }) - it(`pause on specific pauseDuration`, async () => { + it(`pause for specific duration`, async () => { assert.isFalse(await pausable.isPaused()) const pauseDuration = 3 * 60 - await pausable.pause(pauseDuration) + const tx = await pausable.pauseFor(pauseDuration) + assert.emits(tx, 'Paused', { duration: pauseDuration }) + const resumeSinceTimestamp = (await getCurrentBlockTimestamp()) + pauseDuration await assertPausedState(resumeSinceTimestamp) @@ -81,7 +85,65 @@ contract('PausableUntil', ([deployer]) => { assert.isTrue(await pausable.isPaused()) await advanceChainTime(resumeSinceTimestamp - 1 - (await getCurrentBlockTimestamp())) assert.equals(await getCurrentBlockTimestamp(), resumeSinceTimestamp - 1) - // Check only view here because with revert transactions chain can pass more than 1 seconds + // Check only view here because with reverted transactions chain can pass more than 1 seconds + assert.isTrue(await pausable.isPaused()) + + await advanceChainTime(1) + assert.equals(await getCurrentBlockTimestamp(), resumeSinceTimestamp) + await assertResumedState() + }) + + it(`revert if pause until timestamp in past`, async () => { + const getNextTxBlockTimestamp = async () => { + return (await getCurrentBlockTimestamp()) + 1 + } + await assert.revertsWithCustomError( + pausable.pauseUntil((await getNextTxBlockTimestamp()) - 1), + `PauseUntilMustBeInFuture()` + ) + await assert.revertsWithCustomError( + pausable.pauseUntil(Math.floor((await getNextTxBlockTimestamp()) / 2)), + `PauseUntilMustBeInFuture()` + ) + await assert.revertsWithCustomError(pausable.pauseUntil(0), `PauseUntilMustBeInFuture()`) + + // But do not revert for the next tx timestamp (i.e., pause lasts for the one block) + const tx = await pausable.pauseUntil(await getNextTxBlockTimestamp()) + assert.emits(tx, 'Paused', { duration: 1 }) + }) + + it(`pause until infinity`, async () => { + await assertResumedState() + const MONTH_IN_SECS = 30 * 24 * 60 * 60 + + const tx = await pausable.pauseUntil(PAUSE_INFINITELY) + assert.emits(tx, 'Paused', { duration: PAUSE_INFINITELY }) + + await assertPausedState(PAUSE_INFINITELY) + + await advanceChainTime(MONTH_IN_SECS) + await assertPausedState(PAUSE_INFINITELY) + + await advanceChainTime(12 * MONTH_IN_SECS) + await assertPausedState(PAUSE_INFINITELY) + }) + + it(`pause until`, async () => { + assert.isFalse(await pausable.isPaused()) + const pauseDuration = 3 * 60 + const pauseUntilInclusive = (await getCurrentBlockTimestamp()) + pauseDuration + const resumeSinceTimestamp = pauseUntilInclusive + 1 + + const tx = await pausable.pauseUntil(pauseUntilInclusive) + assert.emits(tx, 'Paused', { duration: pauseDuration }) + + await assertPausedState(resumeSinceTimestamp) + + await advanceChainTime(Math.floor(pauseDuration / 2)) + assert.isTrue(await pausable.isPaused()) + await advanceChainTime(resumeSinceTimestamp - 1 - (await getCurrentBlockTimestamp())) + assert.equals(await getCurrentBlockTimestamp(), resumeSinceTimestamp - 1) + // Check only view here because with reverted transactions chain can pass more than 1 seconds assert.isTrue(await pausable.isPaused()) await advanceChainTime(1) @@ -90,12 +152,20 @@ contract('PausableUntil', ([deployer]) => { }) it(`resume`, async () => { - await pausable.pause(PAUSE_INFINITELY) - await pausable.resume() + let tx = await pausable.pauseFor(PAUSE_INFINITELY) + assert.emits(tx, 'Paused', { duration: PAUSE_INFINITELY }) + + tx = await pausable.resume() + assert.emits(tx, 'Resumed') + await assertResumedState() - await pausable.pause(123) - await pausable.resume() + tx = await pausable.pauseFor(123) + assert.emits(tx, 'Paused', { duration: 123 }) + + tx = await pausable.resume() + assert.emits(tx, 'Resumed') + await assertResumedState() }) }) diff --git a/test/0.8.9/staking-router-deposits.test.js b/test/0.8.9/staking-router-deposits.test.js deleted file mode 100644 index 51b7c3811..000000000 --- a/test/0.8.9/staking-router-deposits.test.js +++ /dev/null @@ -1,124 +0,0 @@ -const { contract, ethers, web3 } = require('hardhat') - -const { EvmSnapshot } = require('../helpers/blockchain') -const { setupNodeOperatorsRegistry } = require('../helpers/staking-modules') -const { deployProtocol } = require('../helpers/protocol') - -const { ETH, genKeys, toBN } = require('../helpers/utils') -const { assert } = require('../helpers/assert') - -const ADDRESS_1 = '0x0000000000000000000000000000000000000001' -const ADDRESS_2 = '0x0000000000000000000000000000000000000002' - -contract('StakingRouter', ([depositor, stranger]) => { - let snapshot - let depositContract, stakingRouter - let lido, operators, voting - - before(async () => { - const deployed = await deployProtocol({ - depositSecurityModuleFactory: async () => { - return { address: depositor } - }, - }) - - lido = deployed.pool - stakingRouter = deployed.stakingRouter - operators = await setupNodeOperatorsRegistry(deployed, true) - voting = deployed.voting.address - depositContract = deployed.depositContract - snapshot = new EvmSnapshot(ethers.provider) - await snapshot.make() - }) - - afterEach(async () => { - await snapshot.rollback() - }) - - describe('Make deposit', () => { - beforeEach(async () => { - await stakingRouter.addStakingModule( - 'Curated', - operators.address, - 10_000, // 100 % _targetShare - 1_000, // 10 % _moduleFee - 5_000, // 50 % _treasuryFee - { from: voting } - ) - }) - - it('Lido.deposit() :: check permissioness', async () => { - const maxDepositsCount = 150 - - await web3.eth.sendTransaction({ value: ETH(maxDepositsCount * 32), to: lido.address, from: stranger }) - assert.equals(await lido.getBufferedEther(), ETH(maxDepositsCount * 32 + 1)) - - const [curated] = await stakingRouter.getStakingModules() - - await assert.reverts(lido.deposit(maxDepositsCount, curated.id, '0x', { from: stranger }), 'APP_AUTH_DSM_FAILED') - await assert.reverts(lido.deposit(maxDepositsCount, curated.id, '0x', { from: voting }), 'APP_AUTH_DSM_FAILED') - - await assert.reverts( - stakingRouter.deposit(maxDepositsCount, curated.id, '0x', { from: voting }), - 'AppAuthLidoFailed()' - ) - }) - - it('Lido.deposit() :: check deposit with keys', async () => { - // balance are initial - assert.equals(await web3.eth.getBalance(lido.address), ETH(1)) - assert.equals(await web3.eth.getBalance(stakingRouter.address), 0) - - const sendEthForKeys = ETH(101 * 32 - 1) - const totalPooledEther = ETH(101 * 32) - const maxDepositsCount = 100 - - await web3.eth.sendTransaction({ value: sendEthForKeys, to: lido.address, from: stranger }) - assert.equals(await lido.getBufferedEther(), totalPooledEther) - - // updated balance are lido 100 && sr 0 - assert.equals(await web3.eth.getBalance(lido.address), totalPooledEther) - assert.equals(await web3.eth.getBalance(stakingRouter.address), 0) - - const [curated] = await stakingRouter.getStakingModules() - - // prepare node operators - await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) - await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) - - // add 150 keys to module - const keysAmount = 50 - const keys1 = genKeys(keysAmount) - await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) - await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) - await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) - - await operators.setNodeOperatorStakingLimit(0, 100000, { from: voting }) - await operators.setNodeOperatorStakingLimit(1, 100000, { from: voting }) - - const receipt = await lido.methods[`deposit(uint256,uint256,bytes)`](maxDepositsCount, curated.id, '0x', { - from: depositor, - }) - - assert.equals(await depositContract.totalCalls(), 100, 'invalid deposits count') - - // on deposit we return balance to Lido - assert.equals(await web3.eth.getBalance(lido.address), ETH(32), 'invalid lido balance') - assert.equals(await web3.eth.getBalance(stakingRouter.address), 0, 'invalid staking_router balance') - - assert.equals(await lido.getBufferedEther(), ETH(32), 'invalid total buffer') - - assert.emits(receipt, 'Unbuffered', { amount: ETH(maxDepositsCount * 32) }) - }) - - it('Lido.deposit() :: revert if stakingModuleId more than uint24', async () => { - const maxDepositsCount = 100 - const maxModuleId = toBN(2).pow(toBN(24)) - - await assert.reverts( - lido.methods[`deposit(uint256,uint256,bytes)`](maxDepositsCount, maxModuleId, '0x', { from: depositor }), - 'StakingModuleIdTooLarge()' - ) - }) - }) -}) diff --git a/test/0.8.9/staking-router.test.js b/test/0.8.9/staking-router.test.js deleted file mode 100644 index fd7e1fe3f..000000000 --- a/test/0.8.9/staking-router.test.js +++ /dev/null @@ -1,831 +0,0 @@ -const { artifacts, contract, ethers } = require('hardhat') -const { MaxUint256 } = require('@ethersproject/constants') -const { utils } = require('web3') -const { BN } = require('bn.js') -const { assert } = require('../helpers/assert') -const { EvmSnapshot } = require('../helpers/blockchain') -const { newDao, newApp } = require('../helpers/dao') -const { ETH } = require('../helpers/utils') - -const DepositContractMock = artifacts.require('DepositContractMock') -const StakingRouter = artifacts.require('StakingRouter.sol') -const StakingModuleMock = artifacts.require('StakingModuleMock.sol') - -const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000' -const MANAGE_WITHDRAWAL_CREDENTIALS_ROLE = utils.soliditySha3('MANAGE_WITHDRAWAL_CREDENTIALS_ROLE') -const STAKING_MODULE_PAUSE_ROLE = utils.soliditySha3('STAKING_MODULE_PAUSE_ROLE') -const STAKING_MODULE_RESUME_ROLE = utils.soliditySha3('STAKING_MODULE_RESUME_ROLE') -const STAKING_MODULE_MANAGE_ROLE = utils.soliditySha3('STAKING_MODULE_MANAGE_ROLE') - -const UINT24_MAX = new BN(2).pow(new BN(24)) - -const StakingModuleStatus = { - Active: 0, // deposits and rewards allowed - DepositsPaused: 1, // deposits NOT allowed, rewards allowed - Stopped: 2, // deposits and rewards NOT allowed -} - -contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { - let depositContract, app - const wc = '0x'.padEnd(66, '1234') - const snapshot = new EvmSnapshot(ethers.provider) - - describe('setup env', async () => { - before(async () => { - const { dao } = await newDao(appManager) - depositContract = await DepositContractMock.new() - const appBase = await StakingRouter.new(depositContract.address) - const proxyAddress = await newApp(dao, 'lido-pool', appBase.address, appManager) - app = await StakingRouter.at(proxyAddress) - }) - - it('init fails on wrong input', async () => { - await assert.revertsWithCustomError( - app.initialize(ZERO_ADDRESS, lido, wc, { from: deployer }), - 'ZeroAddress("_admin")' - ) - await assert.revertsWithCustomError( - app.initialize(admin, ZERO_ADDRESS, wc, { from: deployer }), - 'ZeroAddress("_lido")' - ) - }) - - it('initialized correctly', async () => { - const tx = await app.initialize(admin, lido, wc, { from: deployer }) - - assert.equals(await app.getContractVersion(), 1) - assert.equals(await app.getWithdrawalCredentials(), wc) - assert.equals(await app.getLido(), lido) - assert.equals(await app.getStakingModulesCount(), 0) - - assert.equals(await app.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 1) - assert.equals(await app.hasRole(DEFAULT_ADMIN_ROLE, admin), true) - - assert.equals(tx.logs.length, 3) - - await assert.emits(tx, 'ContractVersionSet', { version: 1 }) - await assert.emits(tx, 'RoleGranted', { role: DEFAULT_ADMIN_ROLE, account: admin, sender: deployer }) - await assert.emits(tx, 'WithdrawalCredentialsSet', { withdrawalCredentials: wc }) - }) - - it('second initialize reverts', async () => { - await assert.revertsWithCustomError( - app.initialize(admin, lido, wc, { from: deployer }), - 'NonZeroContractVersionOnInit()' - ) - }) - - it('stranger is not allowed to grant roles', async () => { - await assert.reverts( - app.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: stranger }), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}` - ) - }) - - it('grant role MANAGE_WITHDRAWAL_CREDENTIALS_ROLE', async () => { - const tx = await app.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: admin }) - assert.equals(await app.getRoleMemberCount(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE), 1) - assert.equals(await app.hasRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager), true) - - assert.equals(tx.logs.length, 1) - await assert.emits(tx, 'RoleGranted', { - role: MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, - account: appManager, - sender: admin, - }) - }) - - it('grant role STAKING_MODULE_PAUSE_ROLE', async () => { - const tx = await app.grantRole(STAKING_MODULE_PAUSE_ROLE, appManager, { from: admin }) - assert.equals(await app.getRoleMemberCount(STAKING_MODULE_PAUSE_ROLE), 1) - assert.equals(await app.hasRole(STAKING_MODULE_PAUSE_ROLE, appManager), true) - - assert.equals(tx.logs.length, 1) - await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_PAUSE_ROLE, account: appManager, sender: admin }) - }) - - it('grant role STAKING_MODULE_RESUME_ROLE', async () => { - const tx = await app.grantRole(STAKING_MODULE_RESUME_ROLE, appManager, { from: admin }) - assert.equals(await app.getRoleMemberCount(STAKING_MODULE_RESUME_ROLE), 1) - assert.equals(await app.hasRole(STAKING_MODULE_RESUME_ROLE, appManager), true) - - assert.equals(tx.logs.length, 1) - await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_RESUME_ROLE, account: appManager, sender: admin }) - }) - - it('grant role STAKING_MODULE_MANAGE_ROLE', async () => { - const tx = await app.grantRole(STAKING_MODULE_MANAGE_ROLE, appManager, { from: admin }) - assert.equals(await app.getRoleMemberCount(STAKING_MODULE_MANAGE_ROLE), 1) - assert.equals(await app.hasRole(STAKING_MODULE_MANAGE_ROLE, appManager), true) - - assert.equals(tx.logs.length, 1) - await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_MANAGE_ROLE, account: appManager, sender: admin }) - }) - - it('public constants', async () => { - assert.equals(await app.FEE_PRECISION_POINTS(), new BN('100000000000000000000')) - assert.equals(await app.TOTAL_BASIS_POINTS(), 10000) - assert.equals(await app.DEPOSIT_CONTRACT(), depositContract.address) - assert.equals(await app.DEFAULT_ADMIN_ROLE(), DEFAULT_ADMIN_ROLE) - assert.equals(await app.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), MANAGE_WITHDRAWAL_CREDENTIALS_ROLE) - assert.equals(await app.STAKING_MODULE_PAUSE_ROLE(), STAKING_MODULE_PAUSE_ROLE) - assert.equals(await app.STAKING_MODULE_RESUME_ROLE(), STAKING_MODULE_RESUME_ROLE) - assert.equals(await app.STAKING_MODULE_MANAGE_ROLE(), STAKING_MODULE_MANAGE_ROLE) - }) - - it('getDepositsAllocation', async () => { - const keysAllocation = await app.getDepositsAllocation(1000) - - assert.equals(keysAllocation.allocated, 0) - assert.equals(keysAllocation.allocations, []) - }) - }) - - describe('implementation', async () => { - let stakingRouterImplementation - - before(async () => { - await snapshot.make() - stakingRouterImplementation = await StakingRouter.new(depositContract.address, { from: deployer }) - }) - - after(async () => { - await snapshot.revert() - }) - - it('contract version is max uint256', async () => { - assert.equals(await stakingRouterImplementation.getContractVersion(), MaxUint256) - }) - - it('initialize reverts on implementation', async () => { - await assert.revertsWithCustomError( - stakingRouterImplementation.initialize(admin, lido, wc, { from: deployer }), - `NonZeroContractVersionOnInit()` - ) - }) - - it('has no granted roles', async () => { - assert.equals(await stakingRouterImplementation.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 0) - assert.equals(await stakingRouterImplementation.getRoleMemberCount(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE), 0) - assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_PAUSE_ROLE), 0) - assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_RESUME_ROLE), 0) - assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_MANAGE_ROLE), 0) - }) - - it('state is empty', async () => { - assert.equals(await stakingRouterImplementation.getWithdrawalCredentials(), ZERO_BYTES32) - assert.equals(await stakingRouterImplementation.getLido(), ZERO_ADDRESS) - assert.equals(await stakingRouterImplementation.getStakingModulesCount(), 0) - }) - - it('deposit fails', async () => { - await assert.revertsWithCustomError( - stakingRouterImplementation.deposit(100, 0, '0x00', { from: stranger }), - `AppAuthLidoFailed()` - ) - }) - }) - - describe('staking router', async () => { - let stakingModule - before(async () => { - await snapshot.make() - - stakingModule = await StakingModuleMock.new({ from: deployer }) - - await app.addStakingModule('Test module', stakingModule.address, 100, 1000, 2000, { - from: appManager, - }) - - await stakingModule.setAvailableKeysCount(100, { from: deployer }) - - assert.equals(await stakingModule.getAvailableValidatorsCount(), 100) - }) - - after(async () => { - await snapshot.revert() - }) - - it('reverts if module address exists', async () => { - await assert.revertsWithCustomError( - app.addStakingModule('Test', stakingModule.address, 100, 1000, 2000, { from: appManager }), - 'StakingModuleAddressExists()' - ) - }) - - it('set withdrawal credentials does not allowed without role', async () => { - const newWC = '0x'.padEnd(66, '5678') - await assert.reverts( - app.setWithdrawalCredentials(newWC, { from: stranger }), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${MANAGE_WITHDRAWAL_CREDENTIALS_ROLE}` - ) - }) - - it('set withdrawal credentials', async () => { - const newWC = '0x'.padEnd(66, '5678') - const tx = await app.setWithdrawalCredentials(newWC, { from: appManager }) - - await assert.emits(tx, 'WithdrawalCredentialsSet', { withdrawalCredentials: newWC }) - - assert.equals(await stakingModule.getAvailableValidatorsCount(), 0) - }) - - it('direct transfer fails', async () => { - const value = 100 - await assert.revertsWithCustomError(app.sendTransaction({ value, from: deployer }), `DirectETHTransfer()`) - }) - - it('getStakingModuleNonce', async () => { - await stakingModule.setNonce(100, { from: deployer }) - - assert.equals(await app.getStakingModuleNonce(1), 100) - }) - - it('getStakingModuleNonce reverts when staking module id too large', async () => { - await assert.revertsWithCustomError(app.getStakingModuleNonce(UINT24_MAX), 'StakingModuleIdTooLarge()') - }) - - it('getStakingModuleLastDepositBlock reverts when staking module id too large', async () => { - await assert.revertsWithCustomError(app.getStakingModuleLastDepositBlock(UINT24_MAX), 'StakingModuleIdTooLarge()') - }) - - it('getStakingModuleActiveValidatorsCount reverts when staking module id too large', async () => { - await assert.revertsWithCustomError( - app.getStakingModuleActiveValidatorsCount(UINT24_MAX), - 'StakingModuleIdTooLarge()' - ) - }) - - it('getStakingModuleActiveValidatorsCount', async () => { - await stakingModule.setActiveValidatorsCount(200, { from: deployer }) - - assert.equals(await app.getStakingModuleActiveValidatorsCount(1), 200) - }) - - it('getStakingRewardsDistribution', async () => { - const anotherStakingModule = await StakingModuleMock.new({ from: deployer }) - - await app.addStakingModule('Test module 2', anotherStakingModule.address, 100, 1000, 2000, { - from: appManager, - }) - - await app.getStakingRewardsDistribution() - }) - }) - - describe('staking modules limit', async () => { - before(async () => { - await snapshot.make() - }) - - after(async () => { - await snapshot.revert() - }) - it('staking modules limit is 32', async () => { - for (let i = 0; i < 32; i++) { - const stakingModule = await StakingModuleMock.new({ from: deployer }) - await app.addStakingModule('Test module', stakingModule.address, 100, 100, 100, { from: appManager }) - } - - const oneMoreStakingModule = await StakingModuleMock.new({ from: deployer }) - await assert.revertsWithCustomError( - app.addStakingModule('Test module', oneMoreStakingModule.address, 100, 100, 100, { from: appManager }), - `StakingModulesLimitExceeded()` - ) - }) - }) - - describe('manage staking modules', async () => { - let stakingModule1, stakingModule2 - - const stakingModulesParams = [ - { - name: 'Test module 1', - targetShare: 1000, - stakingModuleFee: 2000, - treasuryFee: 200, - expectedModuleId: 1, - address: null, - }, - { - name: 'Test module 1', - targetShare: 1000, - stakingModuleFee: 2000, - treasuryFee: 200, - expectedModuleId: 2, - address: null, - }, - ] - - before(async () => { - await snapshot.make() - - stakingModule1 = await StakingModuleMock.new({ from: deployer }) - stakingModule2 = await StakingModuleMock.new({ from: deployer }) - - stakingModulesParams[0].address = stakingModule1.address - stakingModulesParams[1].address = stakingModule2.address - }) - - after(async () => { - await snapshot.revert() - }) - - it('addStakingModule call is not allowed from stranger', async () => { - await assert.reverts( - app.addStakingModule( - stakingModulesParams[0].name, - stakingModule1.address, - stakingModulesParams[0].targetShare, - stakingModulesParams[0].stakingModuleFee, - stakingModulesParams[0].treasuryFee, - { from: stranger } - ), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` - ) - }) - - it('addStakingModule fails on share > 100%', async () => { - await assert.revertsWithCustomError( - app.addStakingModule( - stakingModulesParams[0].name, - stakingModule1.address, - 10001, - stakingModulesParams[0].stakingModuleFee, - stakingModulesParams[0].treasuryFee, - { from: appManager } - ), - `ValueOver100Percent("_targetShare")` - ) - }) - - it('addStakingModule fails on fees > 100%', async () => { - await assert.revertsWithCustomError( - app.addStakingModule( - stakingModulesParams[0].name, - stakingModule1.address, - stakingModulesParams[0].targetShare, - 5000, - 5001, - { - from: appManager, - } - ), - `ValueOver100Percent("_stakingModuleFee + _treasuryFee")` - ) - }) - - it('addStakingModule fails on zero address', async () => { - await assert.revertsWithCustomError( - app.addStakingModule( - stakingModulesParams[0].name, - ZERO_ADDRESS, - stakingModulesParams[0].targetShare, - stakingModulesParams[0].stakingModuleFee, - stakingModulesParams[0].treasuryFee, - { - from: appManager, - } - ), - `ZeroAddress("_stakingModuleAddress")` - ) - }) - - it('addStakingModule fails on incorrect module name', async () => { - // check zero length - await assert.revertsWithCustomError( - app.addStakingModule( - '', - stakingModule1.address, - stakingModulesParams[0].targetShare, - stakingModulesParams[0].stakingModuleFee, - stakingModulesParams[0].treasuryFee, - { - from: appManager, - } - ), - `StakingModuleWrongName()` - ) - - // check length > 32 symbols - await assert.revertsWithCustomError( - app.addStakingModule( - '#'.repeat(33), - stakingModule1.address, - stakingModulesParams[0].targetShare, - stakingModulesParams[0].stakingModuleFee, - stakingModulesParams[0].treasuryFee, - { - from: appManager, - } - ), - `StakingModuleWrongName()` - ) - }) - - it('add staking module', async () => { - const tx = await app.addStakingModule( - stakingModulesParams[0].name, - stakingModule1.address, - stakingModulesParams[0].targetShare, - stakingModulesParams[0].stakingModuleFee, - stakingModulesParams[0].treasuryFee, - { - from: appManager, - } - ) - assert.equals(tx.logs.length, 3) - await assert.emits(tx, 'StakingModuleAdded', { - stakingModuleId: stakingModulesParams[0].expectedModuleId, - stakingModule: stakingModule1.address, - name: stakingModulesParams[0].name, - createdBy: appManager, - }) - await assert.emits(tx, 'StakingModuleTargetShareSet', { - stakingModuleId: stakingModulesParams[0].expectedModuleId, - targetShare: stakingModulesParams[0].targetShare, - setBy: appManager, - }) - await assert.emits(tx, 'StakingModuleFeesSet', { - stakingModuleId: stakingModulesParams[0].expectedModuleId, - stakingModuleFee: stakingModulesParams[0].stakingModuleFee, - treasuryFee: stakingModulesParams[0].treasuryFee, - setBy: appManager, - }) - - assert.equals(await app.getStakingModulesCount(), 1) - assert.equals( - await app.getStakingModuleStatus(stakingModulesParams[0].expectedModuleId), - StakingModuleStatus.Active - ) - assert.equals(await app.getStakingModuleIsStopped(stakingModulesParams[0].expectedModuleId), false) - assert.equals(await app.getStakingModuleIsDepositsPaused(stakingModulesParams[0].expectedModuleId), false) - assert.equals(await app.getStakingModuleIsActive(stakingModulesParams[0].expectedModuleId), true) - - await assert.revertsWithCustomError(app.getStakingModule(UINT24_MAX), 'StakingModuleIdTooLarge()') - await assert.revertsWithCustomError(app.getStakingModuleStatus(UINT24_MAX), 'StakingModuleIdTooLarge()') - await assert.revertsWithCustomError(app.getStakingModuleIsStopped(UINT24_MAX), 'StakingModuleIdTooLarge()') - await assert.revertsWithCustomError(app.getStakingModuleIsDepositsPaused(UINT24_MAX), 'StakingModuleIdTooLarge()') - await assert.revertsWithCustomError(app.getStakingModuleIsActive(UINT24_MAX), 'StakingModuleIdTooLarge()') - - const module = await app.getStakingModule(stakingModulesParams[0].expectedModuleId) - - assert.equals(module.name, stakingModulesParams[0].name) - assert.equals(module.stakingModuleAddress, stakingModule1.address) - assert.equals(module.stakingModuleFee, stakingModulesParams[0].stakingModuleFee) - 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) - }) - - it('add another staking module', async () => { - const tx = await app.addStakingModule( - stakingModulesParams[1].name, - stakingModule2.address, - stakingModulesParams[1].targetShare, - stakingModulesParams[1].stakingModuleFee, - stakingModulesParams[1].treasuryFee, - { - from: appManager, - } - ) - - assert.equals(tx.logs.length, 3) - await assert.emits(tx, 'StakingModuleAdded', { - stakingModuleId: stakingModulesParams[1].expectedModuleId, - stakingModule: stakingModule2.address, - name: stakingModulesParams[1].name, - createdBy: appManager, - }) - await assert.emits(tx, 'StakingModuleTargetShareSet', { - stakingModuleId: stakingModulesParams[1].expectedModuleId, - targetShare: stakingModulesParams[1].targetShare, - setBy: appManager, - }) - await assert.emits(tx, 'StakingModuleFeesSet', { - stakingModuleId: stakingModulesParams[1].expectedModuleId, - stakingModuleFee: stakingModulesParams[1].stakingModuleFee, - treasuryFee: stakingModulesParams[1].treasuryFee, - setBy: appManager, - }) - - assert.equals(await app.getStakingModulesCount(), 2) - assert.equals( - await app.getStakingModuleStatus(stakingModulesParams[1].expectedModuleId), - StakingModuleStatus.Active - ) - assert.equals(await app.getStakingModuleIsStopped(stakingModulesParams[1].expectedModuleId), false) - assert.equals(await app.getStakingModuleIsDepositsPaused(stakingModulesParams[1].expectedModuleId), false) - assert.equals(await app.getStakingModuleIsActive(stakingModulesParams[1].expectedModuleId), true) - - const module = await app.getStakingModule(stakingModulesParams[1].expectedModuleId) - - assert.equals(module.name, stakingModulesParams[1].name) - assert.equals(module.stakingModuleAddress, stakingModule2.address) - assert.equals(module.stakingModuleFee, stakingModulesParams[1].stakingModuleFee) - 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) - }) - - it('get staking modules list', async () => { - const stakingModules = await app.getStakingModules() - - for (let i = 0; i < 2; i++) { - assert.equals(stakingModules[i].name, stakingModulesParams[i].name) - assert.equals(stakingModules[i].stakingModuleAddress, stakingModulesParams[i].address) - assert.equals(stakingModules[i].stakingModuleFee, stakingModulesParams[i].stakingModuleFee) - 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) - } - }) - - it('get staking module ids', async () => { - const stakingModules = await app.getStakingModules() - const stakingModuleIds = await app.getStakingModuleIds() - - for (let i = 0; i < stakingModules.length; i++) { - assert.equals(stakingModules[i].id, stakingModuleIds[i]) - } - }) - - it('update staking module does not allowed without role', async () => { - await assert.reverts( - app.updateStakingModule( - stakingModulesParams[0].expectedModuleId, - stakingModulesParams[0].targetShare + 1, - stakingModulesParams[0].stakingModuleFee + 1, - stakingModulesParams[0].treasuryFee + 1, - { - from: stranger, - } - ), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` - ) - }) - - it('update staking module reverts on large module id', async () => { - await assert.revertsWithCustomError( - app.updateStakingModule( - UINT24_MAX, - stakingModulesParams[0].targetShare + 1, - stakingModulesParams[0].stakingModuleFee + 1, - stakingModulesParams[0].treasuryFee + 1, - { - from: appManager, - } - ), - `StakingModuleIdTooLarge()` - ) - }) - - it('update staking module fails on target share > 100%', async () => { - await assert.revertsWithCustomError( - app.updateStakingModule( - stakingModulesParams[0].expectedModuleId, - 10001, - stakingModulesParams[0].stakingModuleFee + 1, - stakingModulesParams[0].treasuryFee + 1, - { - from: appManager, - } - ), - `ValueOver100Percent("_targetShare")` - ) - }) - - it('update staking module fails on fees > 100%', async () => { - await assert.revertsWithCustomError( - app.updateStakingModule( - stakingModulesParams[0].expectedModuleId, - stakingModulesParams[0].targetShare + 1, - 5000, - 5001, - { - from: appManager, - } - ), - `ValueOver100Percent("_stakingModuleFee + _treasuryFee")` - ) - }) - - it('update staking module', async () => { - const stakingModuleNewParams = { - id: stakingModulesParams[0].expectedModuleId, - targetShare: stakingModulesParams[0].targetShare + 1, - stakingModuleFee: stakingModulesParams[0].stakingModuleFee + 1, - treasuryFee: stakingModulesParams[0].treasuryFee + 1, - } - - const tx = await app.updateStakingModule( - stakingModuleNewParams.id, - stakingModuleNewParams.targetShare, - stakingModuleNewParams.stakingModuleFee, - stakingModuleNewParams.treasuryFee, - { - from: appManager, - } - ) - - assert.equals(tx.logs.length, 2) - - await assert.emits(tx, 'StakingModuleTargetShareSet', { - stakingModuleId: stakingModuleNewParams.id, - targetShare: stakingModuleNewParams.targetShare, - setBy: appManager, - }) - await assert.emits(tx, 'StakingModuleFeesSet', { - stakingModuleId: stakingModuleNewParams.id, - stakingModuleFee: stakingModuleNewParams.stakingModuleFee, - treasuryFee: stakingModuleNewParams.treasuryFee, - setBy: appManager, - }) - }) - - it('set staking module status does not allowed without role', async () => { - await assert.reverts( - app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { - from: stranger, - }), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` - ) - }) - - it('set staking module status reverts if staking module id too large', async () => { - await assert.revertsWithCustomError( - app.setStakingModuleStatus(UINT24_MAX, StakingModuleStatus.Stopped, { - from: appManager, - }), - `StakingModuleIdTooLarge()` - ) - }) - - it('set staking module status reverts if status is the same', async () => { - const module = await app.getStakingModule(stakingModulesParams[0].expectedModuleId) - await assert.revertsWithCustomError( - app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, module.status, { - from: appManager, - }), - `StakingModuleStatusTheSame()` - ) - }) - - it('set staking module status', async () => { - const tx = await app.setStakingModuleStatus( - stakingModulesParams[0].expectedModuleId, - StakingModuleStatus.Stopped, - { - from: appManager, - } - ) - - await assert.emits(tx, 'StakingModuleStatusSet', { - stakingModuleId: stakingModulesParams[0].expectedModuleId, - status: StakingModuleStatus.Stopped, - setBy: appManager, - }) - }) - - it('pause staking module does not allowed without role', async () => { - await assert.reverts( - app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { - from: stranger, - }), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_PAUSE_ROLE}` - ) - }) - - it('pause staking module reverts when staking module too large', async () => { - await assert.revertsWithCustomError( - app.pauseStakingModule(UINT24_MAX, { - from: appManager, - }), - `StakingModuleIdTooLarge()` - ) - }) - - it('pause staking module does not allowed at not active staking module', async () => { - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { - from: appManager, - }) - - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { - from: appManager, - }) - await assert.revertsWithCustomError( - app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { - from: appManager, - }), - `StakingModuleNotActive()` - ) - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.DepositsPaused, { - from: appManager, - }) - await assert.revertsWithCustomError( - app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { - from: appManager, - }), - `StakingModuleNotActive()` - ) - }) - - it('pause staking module', async () => { - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { - from: appManager, - }) - const tx = await app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { - from: appManager, - }) - - await assert.emits(tx, 'StakingModuleStatusSet', { - stakingModuleId: stakingModulesParams[0].expectedModuleId, - status: StakingModuleStatus.DepositsPaused, - setBy: appManager, - }) - }) - - it('deposit fails', async () => { - await assert.revertsWithCustomError( - app.deposit(100, UINT24_MAX, '0x00', { value: 100, from: lido }), - 'StakingModuleIdTooLarge()' - ) - }) - - it('deposit fails', async () => { - await assert.revertsWithCustomError( - app.deposit(100, stakingModulesParams[0].expectedModuleId, '0x00', { value: ETH(32 * 100), from: lido }), - 'StakingModuleNotActive()' - ) - }) - - it('getDepositsAllocation', async () => { - const keysAllocation = await app.getDepositsAllocation(1000) - - assert.equals(keysAllocation.allocated, 0) - assert.equals(keysAllocation.allocations, [0, 0]) - }) - - it('resume staking module does not allowed without role', async () => { - await assert.reverts( - app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { - from: stranger, - }), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_RESUME_ROLE}` - ) - }) - - it('resume staking module reverts when staking module id too large', async () => { - await assert.revertsWithCustomError( - app.resumeStakingModule(UINT24_MAX, { - from: appManager, - }), - `StakingModuleIdTooLarge()` - ) - }) - - it('resume staking module does not allowed at not paused staking module', async () => { - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { - from: appManager, - }) - await assert.revertsWithCustomError( - app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { - from: appManager, - }), - `StakingModuleNotPaused()` - ) - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { - from: appManager, - }) - await assert.revertsWithCustomError( - app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { - from: appManager, - }), - `StakingModuleNotPaused()` - ) - }) - - it('resume staking module', async () => { - await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.DepositsPaused, { - from: appManager, - }) - const tx = await app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { - from: appManager, - }) - - await assert.emits(tx, 'StakingModuleStatusSet', { - stakingModuleId: stakingModulesParams[0].expectedModuleId, - status: StakingModuleStatus.Active, - setBy: appManager, - }) - }) - }) -}) diff --git a/test/0.8.9/staking-router/digest.test.js b/test/0.8.9/staking-router/digest.test.js new file mode 100644 index 000000000..877eed769 --- /dev/null +++ b/test/0.8.9/staking-router/digest.test.js @@ -0,0 +1,238 @@ +const { artifacts, contract, ethers } = require('hardhat') +const { MaxUint256 } = require('@ethersproject/constants') +const { assert } = require('../../helpers/assert') +const { EvmSnapshot } = require('../../helpers/blockchain') +const { toNum } = require('../../helpers/utils') + +const OssifiableProxy = artifacts.require('OssifiableProxy.sol') +const DepositContractMock = artifacts.require('DepositContractMock') +const StakingRouter = artifacts.require('StakingRouter.sol') +const StakingModuleMock = artifacts.require('StakingModuleMock.sol') + +let depositContract, router +let module1, module2 + +contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) + + const snapshot = () => evmSnapshot.make() + const revert = () => evmSnapshot.revert() + + before(async () => { + depositContract = await DepositContractMock.new({ from: deployer }) + + const impl = await StakingRouter.new(depositContract.address, { from: deployer }) + const proxy = await OssifiableProxy.new(impl.address, deployer, '0x') + router = await StakingRouter.at(proxy.address) + ;[module1, module2] = await Promise.all([ + StakingModuleMock.new({ from: deployer }), + StakingModuleMock.new({ from: deployer }), + ]) + + const wc = '0x'.padEnd(66, '1234') + await router.initialize(admin, lido, wc, { from: deployer }) + }) + + describe('getNodeOperatorDigests() by module id and list of nopIds', async () => { + before(snapshot) + after(revert) + + let module1Id, module2Id + const nodeOperator1 = 0 + let StakingModuleDigest, StakingModuleDigest2 + + it('reverts if moduleId does not exists', async () => { + await assert.reverts(router.getNodeOperatorDigests(0, []), 'StakingModuleUnregistered()') + }) + + it('add one module', async () => { + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + }) + + it('add second module', async () => { + await router.addStakingModule( + 'module 2', + module2.address, + 9_000, // 100 % _targetShare + 2_000, // 10 % _moduleFee + 3_000, // 50 % _treasuryFee + { from: admin } + ) + module2Id = +(await router.getStakingModuleIds())[1] + }) + + it('get digest with empty nodeOperators', async () => { + const digests = await router.getNodeOperatorDigests(module1Id, []) + assert.equal(digests.length, 0) + }) + + it('add first node operator summary', async () => { + const summary = { + isTargetLimitActive: true, + targetValidatorsCount: 1, + stuckValidatorsCount: 2, + refundedValidatorsCount: 3, + stuckPenaltyEndTimestamp: 4, + totalExitedValidators: 5, + totalDepositedValidators: 6, + depositableValidatorsCount: 7, + } + await module1.setNodeOperatorSummary(nodeOperator1, summary) + + await module1.testing_setNodeOperatorsCount(1) + }) + + it('get digest with one nodeOperator', async () => { + const digests = await router.getNodeOperatorDigests(module1Id, [nodeOperator1]) + assert.equal(digests.length, 1) + + assert.equal(digests[0].id, 0) + assert.equal(digests[0].isActive, false) + assert.sameOrderedMembers(toNum(digests[0].summary), [1, 1, 2, 3, 4, 5, 6, 7]) + }) + + it('get digest with one nodeOperator and one non existi g', async () => { + const digests = await router.getNodeOperatorDigests(module1Id, [nodeOperator1, 123]) + assert.equal(digests.length, 2) + + assert.equal(digests[0].id, 0) + assert.equal(digests[0].isActive, false) + assert.sameOrderedMembers(toNum(digests[0].summary), [1, 1, 2, 3, 4, 5, 6, 7]) + + assert.equal(digests[1].id, 123) + assert.equal(digests[1].isActive, false) + assert.sameOrderedMembers(toNum(digests[1].summary), [0, 0, 0, 0, 0, 0, 0, 0]) + }) + + it('getNodeOperatorDigests(uint256,uint256,uint256) - reverts module unregistered', async () => { + await assert.reverts( + router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`](0, 0, 0), + 'StakingModuleUnregistered()' + ) + }) + it('getNodeOperatorDigests(uint256,uint256,uint256) - module2 without operators', async () => { + let digests = await router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`](module2Id, 0, 0) + assert.equal(digests.length, 0) + + digests = await router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`](module2Id, 0, 1) + assert.equal(digests.length, 0) + + digests = await router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`](module2Id, 0, MaxUint256) + assert.equal(digests.length, 0) + + digests = await router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`]( + module2Id, + MaxUint256, + MaxUint256 + ) + assert.equal(digests.length, 0) + }) + + it('getNodeOperatorDigests(uint256,uint256,uint256) - module1 with node operators', async () => { + let digests = await router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`](module1Id, 0, 0) + assert.equal(digests.length, 0) + + digests = await router.methods[`getNodeOperatorDigests(uint256,uint256,uint256)`](module1Id, 0, MaxUint256) + assert.equal(digests.length, 1) + + assert.equal(digests[0].id, 0) + assert.equal(digests[0].isActive, false) + assert.sameOrderedMembers(toNum(digests[0].summary), [1, 1, 2, 3, 4, 5, 6, 7]) + }) + + it('getAllNodeOperatorDigests(uint256) - module unregistered', async () => { + await assert.reverts(router.getAllNodeOperatorDigests(999), 'StakingModuleUnregistered()') + }) + it('getAllNodeOperatorDigests(uint256) - digests works', async () => { + const digests = await router.getAllNodeOperatorDigests(module1Id) + assert.equal(digests.length, 1) + + assert.equal(digests[0].id, 0) + assert.equal(digests[0].isActive, false) + assert.sameOrderedMembers(toNum(digests[0].summary), [1, 1, 2, 3, 4, 5, 6, 7]) + }) + + it('reverts getAllNodeOperatorDigests module unregistered', async () => { + await assert.reverts(router.getAllNodeOperatorDigests(0), 'StakingModuleUnregistered()') + }) + it('getStakingModuleDigests([]uint256) - reverts modules unregistered', async () => { + await assert.reverts(router.getStakingModuleDigests([0, 999]), 'StakingModuleUnregistered()') + }) + it('getStakingModuleDigests([]uint256) - digests works', async () => { + await module1.setTotalExitedValidatorsCount(11) + await module1.setActiveValidatorsCount(22) + await module1.setAvailableKeysCount(33) + + const digests = await router.getStakingModuleDigests([module1Id, module2Id]) + assert.equal(digests.length, 2) + + StakingModuleDigest = { + nodeOperatorsCount: '1', + activeNodeOperatorsCount: '0', + state: Object.values({ + id: module1Id.toString(), + stakingModuleAddress: module1.address, + stakingModuleFee: '1000', + treasuryFee: '5000', + targetShare: '10000', + status: '0', + name: 'module 1', + lastDepositAt: '0', + lastDepositBlock: '0', + exitedValidatorsCount: '0', + }), + summary: Object.values({ + totalExitedValidators: '11', + totalDepositedValidators: '22', + depositableValidatorsCount: '33', + }), + } + + assert.deepEqual(digests[0], Object.values(StakingModuleDigest)) + + StakingModuleDigest2 = { + nodeOperatorsCount: '0', + activeNodeOperatorsCount: '0', + state: Object.values({ + id: module2Id.toString(), + stakingModuleAddress: module2.address, + stakingModuleFee: '2000', + treasuryFee: '3000', + targetShare: '9000', + status: '0', + name: 'module 2', + lastDepositAt: '0', + lastDepositBlock: '0', + exitedValidatorsCount: '0', + }), + summary: Object.values({ + totalExitedValidators: '0', + totalDepositedValidators: '0', + depositableValidatorsCount: '0', + }), + } + assert.deepEqual(digests[1], Object.values(StakingModuleDigest2)) + + const digests2 = await router.getStakingModuleDigests([module1Id, module2Id]) + assert.equal(digests2.length, 2) + }) + + it('getAllStakingModuleDigests() works', async () => { + // + const digests2 = await router.getAllStakingModuleDigests() + assert.equal(digests2.length, 2) + + assert.deepEqual(digests2[0], Object.values(StakingModuleDigest)) + assert.deepEqual(digests2[1], Object.values(StakingModuleDigest2)) + }) + }) +}) 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 new file mode 100644 index 000000000..ff34a0db7 --- /dev/null +++ b/test/0.8.9/staking-router/report-exited-keys.test.js @@ -0,0 +1,456 @@ +const { contract, ethers } = require('hardhat') +const { assert } = require('../../helpers/assert') +const { EvmSnapshot } = require('../../helpers/blockchain') +const { hexConcat, hex, ETH } = require('../../helpers/utils') +const { deployProtocol } = require('../../helpers/protocol') +const { setupNodeOperatorsRegistry } = require('../../helpers/staking-modules') + +const ADDRESS_1 = '0x0000000000000000000000000000000000000001' + +let router, voting +let operators, module2 +let module1Id, module2Id +let maxDepositsPerModule + +contract('StakingRouter', ([admin, depositor]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) + + const snapshot = () => evmSnapshot.make() + const revert = () => evmSnapshot.revert() + + before(async () => { + const deployed = await deployProtocol({ + depositSecurityModuleFactory: async () => { + return { address: depositor } + }, + }) + + router = deployed.stakingRouter + voting = deployed.voting.address + operators = await setupNodeOperatorsRegistry(deployed, true) + module2 = await setupNodeOperatorsRegistry(deployed, true) + + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.grantRole(await router.REPORT_EXITED_VALIDATORS_ROLE(), admin, { from: admin }) + + // get max allocation per module + maxDepositsPerModule = async () => { + const modulesIds = await router.getStakingModuleIds() + const maxDepositsPerModule = [] + for (let i = 0; i < modulesIds.length; i++) { + const maxCount = +(await router.getStakingModuleMaxDepositsCount(modulesIds[i], ETH(1000000 * 32))) + maxDepositsPerModule.push(maxCount) + } + return maxDepositsPerModule + } + }) + + describe('Report exited keys by module changes stake allocation', async () => { + before(async () => { + // add modules + await router.addStakingModule( + 'Module 1', + operators.address, + 10_000, // 100 % _targetShare + 500, // 10 % _moduleFee + 500, // 50 % _treasuryFee + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + + await router.addStakingModule( + 'Module 2', + module2.address, + 10_000, // 100 % _targetShare + 500, // 10 % _moduleFee + 500, // 50 % _treasuryFee + { from: admin } + ) + module2Id = +(await router.getStakingModuleIds())[1] + + // add operators + await operators.testing_addNodeOperator( + 'Operator1', + ADDRESS_1, // config.rewardAddress, + 100, // totalSigningKeysCount, + 60, // vettedSigningKeysCount, + 50, // depositedSigningKeysCount, + 0 // exitedSigningKeysCount + ) + await operators.testing_addNodeOperator( + 'Operator1', + ADDRESS_1, // config.rewardAddress, + 100, // totalSigningKeysCount, + 60, // vettedSigningKeysCount, + 50, // depositedSigningKeysCount, + 0 // exitedSigningKeysCount + ) + + await module2.testing_addNodeOperator( + 'Operator1', + ADDRESS_1, // config.rewardAddress, + 20, // totalSigningKeysCount, + 15, // vettedSigningKeysCount, + 10, // depositedSigningKeysCount, + 0 // exitedSigningKeysCount + ) + }) + + beforeEach(snapshot) + afterEach(revert) + + it('check initial keys of operators', async () => { + const moduleSummary1 = await router.getNodeOperatorSummary(module1Id, 0) + assert.equal(moduleSummary1.isTargetLimitActive, false) + assert.equal(moduleSummary1.targetValidatorsCount, 0) + assert.equal(moduleSummary1.stuckValidatorsCount, 0) + assert.equal(moduleSummary1.refundedValidatorsCount, 0) + assert.equal(moduleSummary1.stuckPenaltyEndTimestamp, 0) + assert.equal(moduleSummary1.totalExitedValidators, 0) + assert.equal(moduleSummary1.totalDepositedValidators, 50) + assert.equal(moduleSummary1.depositableValidatorsCount, 10) + + const moduleSummary2 = await router.getNodeOperatorSummary(module2Id, 0) + assert.equal(moduleSummary2.isTargetLimitActive, false) + assert.equal(moduleSummary2.targetValidatorsCount, 0) + assert.equal(moduleSummary2.stuckValidatorsCount, 0) + assert.equal(moduleSummary2.refundedValidatorsCount, 0) + assert.equal(moduleSummary2.stuckPenaltyEndTimestamp, 0) + assert.equal(moduleSummary2.totalExitedValidators, 0) + assert.equal(moduleSummary2.totalDepositedValidators, 10) + assert.equal(moduleSummary2.depositableValidatorsCount, 5) + }) + + it('report exited keys should change rewards distribution', async () => { + // check exited validators before + let moduleSummary1 = await router.getStakingModuleSummary(module1Id) + assert.equal(moduleSummary1.totalExitedValidators, 0) + assert.equal(moduleSummary1.totalDepositedValidators, 100) + assert.equal(moduleSummary1.depositableValidatorsCount, 20) + + let distribution + + const { + totalDepositedValidators: totalDepositedValidators1Before, + totalExitedValidators: totalExitedValidators1Before, + } = await router.getNodeOperatorSummary(module1Id, 0) + const { + totalDepositedValidators: totalDepositedValidators2Before, + totalExitedValidators: totalExitedValidators2Before, + } = await router.getNodeOperatorSummary(module1Id, 1) + + const totalActiveValidators1Before = totalDepositedValidators1Before - totalExitedValidators1Before + const totalActiveValidators2Before = totalDepositedValidators2Before - totalExitedValidators2Before + const totalActiveValidatorsBefore = totalActiveValidators1Before + totalActiveValidators2Before + + const op1shareBefore = totalActiveValidators1Before / totalActiveValidatorsBefore // should be 0.5 + const op2shareBefore = totalActiveValidators2Before / totalActiveValidatorsBefore // should be 0.5 + + assert.equal(op1shareBefore, 0.5) + assert.equal(op2shareBefore, 0.5) + + const sharesDistribute = ETH(1) + distribution = await operators.getRewardsDistribution(sharesDistribute) + assert.equal(+distribution.shares[0], op1shareBefore * sharesDistribute) + assert.equal(+distribution.shares[1], op2shareBefore * sharesDistribute) + + // //update exited validators + const exitValidatorsCount = 20 + await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + + const nodeOpIds = [0] + const exitedValidatorsCounts = [exitValidatorsCount] + + const nodeOpIdsData = hexConcat(...nodeOpIds.map((i) => hex(i, 8))) + const keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) + + // report exited by module and node operator + await router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { + from: admin, + }) + + moduleSummary1 = await router.getStakingModuleSummary(module1Id) + assert.equal(moduleSummary1.totalExitedValidators, exitValidatorsCount) + assert.equal(moduleSummary1.totalDepositedValidators, 100) + assert.equal(moduleSummary1.depositableValidatorsCount, 20) + + // get distribution after exited keys + const { + totalDepositedValidators: totalDepositedValidators1After, + totalExitedValidators: totalExitedValidators1After, + } = await router.getNodeOperatorSummary(module1Id, 0) + const { + totalDepositedValidators: totalDepositedValidators2After, + totalExitedValidators: totalExitedValidators2After, + } = await router.getNodeOperatorSummary(module1Id, 1) + + const totalActiveValidators1After = totalDepositedValidators1After - totalExitedValidators1After + const totalActiveValidators2After = totalDepositedValidators2After - totalExitedValidators2After + const totalActiveValidatorsAfter = totalActiveValidators1After + totalActiveValidators2After + + assert.notEqual(totalActiveValidatorsBefore, totalActiveValidatorsAfter) + assert.notEqual(totalActiveValidators1Before, totalActiveValidators1After) + assert.equal(totalDepositedValidators2Before, totalDepositedValidators2After) + + const op1shareAfter = totalActiveValidators1After / totalActiveValidatorsAfter // should be 0.375 + const op2shareAfter = totalActiveValidators2After / totalActiveValidatorsAfter // should be 0.625 + + assert.equal(op1shareAfter, 0.375) + assert.equal(op2shareAfter, 0.625) + + assert(op1shareBefore > op1shareAfter) + assert(op2shareBefore < op2shareAfter) + assert(op1shareBefore < op2shareAfter) + + distribution = await operators.getRewardsDistribution(sharesDistribute) + assert.equal(+distribution.shares[0], op1shareAfter * sharesDistribute) + assert.equal(+distribution.shares[1], op2shareAfter * sharesDistribute) + }) + + it('report exited keys without target limit should not change allocation', async () => { + // check exited validators before + let moduleSummary1 = await router.getStakingModuleSummary(module1Id) + assert.equal(moduleSummary1.totalExitedValidators, 0) + assert.equal(moduleSummary1.totalDepositedValidators, 100) + assert.equal(moduleSummary1.depositableValidatorsCount, 20) + + const maxDepositsPerModuleBefore = await maxDepositsPerModule() + assert.deepEqual([20, 5], maxDepositsPerModuleBefore) + + // //update exited validators + const exitValidatorsCount = 20 + await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + + const nodeOpIds = [0] + const exitedValidatorsCounts = [exitValidatorsCount] + + const nodeOpIdsData = hexConcat(...nodeOpIds.map((i) => hex(i, 8))) + const keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) + + // report exited by module and node operator + await router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { + from: admin, + }) + + moduleSummary1 = await router.getStakingModuleSummary(module1Id) + assert.equal(moduleSummary1.totalExitedValidators, exitValidatorsCount) + assert.equal(moduleSummary1.totalDepositedValidators, 100) + assert.equal(moduleSummary1.depositableValidatorsCount, 20) + + const maxDepositsPerModuleAfter = await maxDepositsPerModule() + assert.deepEqual(maxDepositsPerModuleBefore, maxDepositsPerModuleAfter) + }) + + it('report exited keys with target limit should change allocation', async () => { + // check exited validators before + const moduleSummary1Before = await router.getStakingModuleSummary(module1Id) + assert.equal(moduleSummary1Before.totalExitedValidators, 0) + assert.equal(moduleSummary1Before.totalDepositedValidators, 100) + assert.equal(moduleSummary1Before.depositableValidatorsCount, 20) + + let keyStats = await router.getNodeOperatorSummary(module1Id, 0) + assert.equals(keyStats.isTargetLimitActive, false) + assert.equals(keyStats.targetValidatorsCount, 0) + assert.equals(keyStats.totalExitedValidators, 0) + assert.equals(keyStats.totalDepositedValidators, 50) + assert.equals(keyStats.depositableValidatorsCount, 10) + + // get max allocation before set target limit + const maxDepositsPerModuleBefore = await maxDepositsPerModule() + assert.deepEqual([20, 5], maxDepositsPerModuleBefore) + + await operators.updateTargetValidatorsLimits(0, true, 50, { from: voting }) + + // + keyStats = await router.getNodeOperatorSummary(module1Id, 0) + assert.equals(keyStats.isTargetLimitActive, true) + assert.equals(keyStats.targetValidatorsCount, 50) + assert.equals(keyStats.totalExitedValidators, 0) + assert.equals(keyStats.totalDepositedValidators, 50) + assert.equals(keyStats.depositableValidatorsCount, 0) + + // check exited validators after + let moduleSummary1After = await router.getStakingModuleSummary(module1Id) + assert.equal(moduleSummary1After.totalExitedValidators, 0) + assert.equal(moduleSummary1After.totalDepositedValidators, 100) + assert.equal(moduleSummary1After.depositableValidatorsCount, 10) + + // decreases + const maxDepositsPerModuleAfter = await maxDepositsPerModule() + assert.deepEqual([10, 5], maxDepositsPerModuleAfter) + + // increase target limit 50 -> 55 + await operators.updateTargetValidatorsLimits(0, true, 55, { from: voting }) + keyStats = await router.getNodeOperatorSummary(module1Id, 0) + moduleSummary1After = await router.getStakingModuleSummary(module1Id) + + assert.equals(keyStats.depositableValidatorsCount, 5) + assert.equal(moduleSummary1After.depositableValidatorsCount, 15) + assert.deepEqual([15, 5], await maxDepositsPerModule()) + + // update exited validators + let exitValidatorsCount = 1 + await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + + const nodeOpIds = [0] + let exitedValidatorsCounts = [exitValidatorsCount] + + const nodeOpIdsData = hexConcat(...nodeOpIds.map((i) => hex(i, 8))) + let keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) + + // report exited by module and node operator + await router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { + from: admin, + }) + + // check allocation + keyStats = await router.getNodeOperatorSummary(module1Id, 0) + moduleSummary1After = await router.getStakingModuleSummary(module1Id) + + assert.equals(keyStats.depositableValidatorsCount, 6) + assert.equal(moduleSummary1After.depositableValidatorsCount, 16) + + const maxDepositsPerModuleAfterAlloc = await maxDepositsPerModule() + assert.deepEqual([16, 5], maxDepositsPerModuleAfterAlloc) + + // update next exited validators + exitValidatorsCount = 30 + exitedValidatorsCounts = [exitValidatorsCount] + keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) + + await router.updateExitedValidatorsCountByStakingModule([module1Id], [exitValidatorsCount], { from: admin }) + // report exited by module and node operator + await router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { + from: admin, + }) + + // check allocation + keyStats = await router.getNodeOperatorSummary(module1Id, 0) + moduleSummary1After = await router.getStakingModuleSummary(module1Id) + + assert.equals(keyStats.depositableValidatorsCount, 10) // we can't change + assert.equal(moduleSummary1After.depositableValidatorsCount, 20) + + const maxDepositsPerModuleAfterReport = await maxDepositsPerModule() + assert.deepEqual([20, 5], maxDepositsPerModuleAfterReport) + + // small explanation: + // vetted - 60 keys + // deposited - 50 keys + // exited - 0 keys + // + // for allocation, we need to know how many keys are available for deposit - depositableKeys + // in common case, when targetLimit is not active `depositableKeys` = vetted - deposited + // + // but if targetLimit is active and targetLimit=50, then depositableKeys==0, + // because there are 50 active keys already. + // + // when we report exitedKeys=10, thats mean active keys decrease to 40 keys, and depositableKeys=10 now + // BUT depositableKeys cannot be more than vetted-deposited + // so if we report exitedKeys=10 depositableKeys should be still 10 + }) + + it('report stuck keys should not change rewards distribution, but return penalized table', async () => { + let distribution + + const { + totalDepositedValidators: totalDepositedValidators1Before, + totalExitedValidators: totalExitedValidators1Before, + } = await router.getNodeOperatorSummary(module1Id, 0) + const { + totalDepositedValidators: totalDepositedValidators2Before, + totalExitedValidators: totalExitedValidators2Before, + } = await router.getNodeOperatorSummary(module1Id, 1) + + const totalActiveValidators1Before = totalDepositedValidators1Before - totalExitedValidators1Before + const totalActiveValidators2Before = totalDepositedValidators2Before - totalExitedValidators2Before + const totalActiveValidatorsBefore = totalActiveValidators1Before + totalActiveValidators2Before + + const op1shareBefore = totalActiveValidators1Before / totalActiveValidatorsBefore // should be 0.5 + const op2shareBefore = totalActiveValidators2Before / totalActiveValidatorsBefore // should be 0.5 + + assert.equal(op1shareBefore, 0.5) + assert.equal(op2shareBefore, 0.5) + + const sharesDistribute = ETH(1) + distribution = await operators.getRewardsDistribution(sharesDistribute) + assert.equal(+distribution.shares[0], op1shareBefore * sharesDistribute) + assert.equal(+distribution.shares[1], op2shareBefore * sharesDistribute) + + // update stuck validators + const stuckValidatorsCount = 1 + + const nodeOpIds = [0] + const exitedValidatorsCounts = [stuckValidatorsCount] + + const nodeOpIdsData = hexConcat(...nodeOpIds.map((i) => hex(i, 8))) + const keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) + + // report stuck by module and node operator + await router.reportStakingModuleStuckValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { + from: admin, + }) + + // get distribution after exited keys + const { + totalDepositedValidators: totalDepositedValidators1After, + totalExitedValidators: totalExitedValidators1After, + } = await router.getNodeOperatorSummary(module1Id, 0) + const { + totalDepositedValidators: totalDepositedValidators2After, + totalExitedValidators: totalExitedValidators2After, + } = await router.getNodeOperatorSummary(module1Id, 1) + + const totalActiveValidators1After = totalDepositedValidators1After - totalExitedValidators1After + const totalActiveValidators2After = totalDepositedValidators2After - totalExitedValidators2After + const totalActiveValidatorsAfter = totalActiveValidators1After + totalActiveValidators2After + + assert.equal(totalActiveValidatorsBefore, totalActiveValidatorsAfter) + assert.equal(totalActiveValidators1Before, totalActiveValidators1After) + assert.equal(totalDepositedValidators2Before, totalDepositedValidators2After) + + const op1shareAfter = totalActiveValidators1After / totalActiveValidatorsAfter + const op2shareAfter = totalActiveValidators2After / totalActiveValidatorsAfter + + // when penalized shares still the same + assert.equal(op1shareBefore, op1shareAfter) + assert.equal(op2shareBefore, op2shareAfter) + + distribution = await operators.getRewardsDistribution(sharesDistribute) + assert.deepEqual([+distribution.shares[0], distribution.penalized[0]], [op1shareAfter * sharesDistribute, true]) + assert.deepEqual([+distribution.shares[1], distribution.penalized[1]], [op2shareAfter * sharesDistribute, false]) + }) + + it('report stuck keys should not affect stake allocation', async () => { + // get max allocation before + const maxDepositsPerModuleBefore = await maxDepositsPerModule() + assert.deepEqual([20, 5], maxDepositsPerModuleBefore) + + const moduleSummary1Before = await router.getNodeOperatorSummary(module1Id, 0) + assert.equal(moduleSummary1Before.stuckValidatorsCount, 0) + assert.equal(moduleSummary1Before.depositableValidatorsCount, 10) + + // update stuck validators + const stuckValidatorsCount = 20 + + const nodeOpIds = [0] + const exitedValidatorsCounts = [stuckValidatorsCount] + + const nodeOpIdsData = hexConcat(...nodeOpIds.map((i) => hex(i, 8))) + const keysData = hexConcat(...exitedValidatorsCounts.map((c) => hex(c, 16))) + + // report exited by module and node operator + await router.reportStakingModuleStuckValidatorsCountByNodeOperator(module1Id, nodeOpIdsData, keysData, { + from: admin, + }) + + // we remove allocation from operator, if he has stuck keys + const maxDepositsPerModuleAfter = await maxDepositsPerModule() + assert.deepEqual([10, 5], maxDepositsPerModuleAfter) + + const moduleSummary1After = await router.getNodeOperatorSummary(module1Id, 0) + assert.equal(moduleSummary1After.stuckValidatorsCount, 20) + assert.equal(moduleSummary1After.depositableValidatorsCount, 0) + }) + }) +}) diff --git a/test/0.8.9/staking-router/rewards-distribution.test.js b/test/0.8.9/staking-router/rewards-distribution.test.js new file mode 100644 index 000000000..cf94c64ed --- /dev/null +++ b/test/0.8.9/staking-router/rewards-distribution.test.js @@ -0,0 +1,392 @@ +const { contract, ethers } = require('hardhat') +const { BN } = require('bn.js') + +const { assert } = require('../../helpers/assert') +const { EvmSnapshot } = require('../../helpers/blockchain') +const { toNum } = require('../../helpers/utils') +const { deployProtocol } = require('../../helpers/protocol') + +const { setupNodeOperatorsRegistry } = require('../../helpers/staking-modules') + +const ADDRESS_1 = '0x0000000000000000000000000000000000000001' +const ADDRESS_2 = '0x0000000000000000000000000000000000000002' +const ADDRESS_3 = '0x0000000000000000000000000000000000000003' + +const StakingModuleStatus = { + Active: 0, // deposits and rewards allowed + DepositsPaused: 1, // deposits NOT allowed, rewards allowed + Stopped: 2, // deposits and rewards NOT allowed +} + +let router +let operators, solo1, solo2, solo3 +let module1Id, module3Id, module4Id +let config + +contract('StakingRouter', ([deployer, admin, depositor, stranger]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) + + const snapshot = () => evmSnapshot.make() + const revert = () => evmSnapshot.revert() + + before(async () => { + const deployed = await deployProtocol({ + depositSecurityModuleFactory: async () => { + return { address: depositor } + }, + }) + + router = deployed.stakingRouter + operators = await setupNodeOperatorsRegistry(deployed, true) + solo1 = await setupNodeOperatorsRegistry(deployed, true) + solo2 = await setupNodeOperatorsRegistry(deployed, true) + solo3 = await setupNodeOperatorsRegistry(deployed, true) + }) + + describe('getNodeOperatorDigests() by module id and list of nopIds', async () => { + before(snapshot) + after(revert) + + it('getStakingRewardsDistribution() - without modules', async () => { + const distribution = await router.getStakingRewardsDistribution() + + const lengthShouldBe = distribution.stakingModuleFees.length + assert.equal(distribution.recipients.length, lengthShouldBe) + assert.equal(distribution.stakingModuleIds.length, lengthShouldBe) + assert.equal(distribution.stakingModuleFees.length, lengthShouldBe) + + assert.equal(distribution.totalFee, 0) + assert.equal(+distribution.precisionPoints, new BN(10).pow(new BN(20))) // 100 * 10^18 + }) + + it('add one module', async () => { + await router.addStakingModule( + 'Curated', + operators.address, + 10_000, // 100 % _targetShare + 5000, // 50 % _moduleFee + 5000, // 50 % _treasuryFee + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + }) + + it('getStakingRewardsDistribution() - one module - no validators', async () => { + const distribution = await router.getStakingRewardsDistribution() + + const lengthShouldBe = distribution.stakingModuleFees.length + assert.equal(distribution.recipients.length, lengthShouldBe) + assert.equal(distribution.stakingModuleIds.length, lengthShouldBe) + assert.equal(distribution.stakingModuleFees.length, lengthShouldBe) + + assert.equal(distribution.totalFee, 0) + assert.equal(+distribution.precisionPoints, new BN(10).pow(new BN(20))) // 100 * 10^18 + }) + + it('prepare node operators', async () => { + const config = { + name: 'test', + rewardAddress: ADDRESS_1, + totalSigningKeysCount: 13, + vettedSigningKeysCount: 4, + depositedSigningKeysCount: 7, + exitedSigningKeysCount: 5, + } + + await operators.testing_addNodeOperator( + config.name, + config.rewardAddress, + config.totalSigningKeysCount, + config.vettedSigningKeysCount, + config.depositedSigningKeysCount, + config.exitedSigningKeysCount + ) + }) + + it('getStakingRewardsDistribution() - reverts if total fee >= 100%', async () => { + await assert.reverts(router.getStakingRewardsDistribution(), 'ValueOver100Percent("totalFee")') + }) + + it('update module - set fee and treasury fee', async () => { + await router.updateStakingModule(module1Id, 10_000, 500, 500, { from: admin }) + }) + + it('getStakingRewardsDistribution() - works for one module', async () => { + const distribution = await router.getStakingRewardsDistribution() + + const lengthShouldBe = distribution.stakingModuleFees.length + assert.equal(distribution.recipients.length, lengthShouldBe) + assert.equal(distribution.stakingModuleIds.length, lengthShouldBe) + assert.equal(distribution.stakingModuleFees.length, lengthShouldBe) + + // share = active 2 / totalActive = 2 == 1 + // moduleFee = share * moduleFee = 1 * 5 = 5 * 10^18 + + assert.deepEqual(distribution.recipients, [operators.address]) + assert.deepEqual(toNum(distribution.stakingModuleIds), [1]) + assert.deepEqual(toNum(distribution.stakingModuleFees), [5 * 10 ** 18]) + assert.equal(toNum(distribution.totalFee), 10 * 10 ** 18) + }) + + it('add 3 modules', async () => { + // add module withoud node operators + await router.addStakingModule( + 'Solo1', + solo1.address, + 3000, // 30 % _targetShare + 500, // 50 % _moduleFee + 500, // 50 % _treasuryFee + { from: admin } + ) + + // add solo2 with node operators + await router.addStakingModule( + 'Solo2', + solo2.address, + 2000, // 20 % _targetShare + 500, // 40 % _moduleFee + 500, // 40 % _treasuryFee + { from: admin } + ) + module3Id = +(await router.getStakingModuleIds())[2] + + config = { + name: 'Solo2', + rewardAddress: ADDRESS_2, + totalSigningKeysCount: 10, + vettedSigningKeysCount: 10, + depositedSigningKeysCount: 7, + exitedSigningKeysCount: 1, + } + + await solo2.testing_addNodeOperator( + config.name, + config.rewardAddress, + config.totalSigningKeysCount, + config.vettedSigningKeysCount, + config.depositedSigningKeysCount, + config.exitedSigningKeysCount + ) + /// + + // add solo3 with node operators, but stop this module + await router.addStakingModule( + 'Solo3', + solo3.address, + 2000, // 20 % _targetShare + 700, // 40 % _moduleFee + 300, // 40 % _treasuryFee + { from: admin } + ) + module4Id = +(await router.getStakingModuleIds())[3] + + config = { + name: 'Solo3', + rewardAddress: ADDRESS_3, + totalSigningKeysCount: 13, + vettedSigningKeysCount: 4, + depositedSigningKeysCount: 7, + exitedSigningKeysCount: 5, + } + + await solo3.testing_addNodeOperator( + config.name, + config.rewardAddress, + config.totalSigningKeysCount, + config.vettedSigningKeysCount, + config.depositedSigningKeysCount, + config.exitedSigningKeysCount + ) + }) + + it('getStakingRewardsDistribution() - skip one module without active validators', async () => { + await router.setStakingModuleStatus(module4Id, StakingModuleStatus.Stopped, { from: admin }) + + const distribution = await router.getStakingRewardsDistribution() + + const lengthShouldBe = distribution.stakingModuleFees.length + assert.equal(lengthShouldBe, 3) + assert.equal(distribution.recipients.length, lengthShouldBe) + assert.equal(distribution.stakingModuleIds.length, lengthShouldBe) + assert.equal(distribution.stakingModuleFees.length, lengthShouldBe) + + // totalActiveVal = 2 + 6 + 2 + 0 = 10 + // + // share1 = 2/10 = 0.2, fee1 = share1 * moduleFee1 = 0.2 * 0.05 = 0.01 + // share2 = 6/10 = 0.6, fee2 = share2 * moduleFee2 = 0.6 * 0.05 = 0.03 + // share3 = 0, fee3 = 0 + // share4 = 0, fee4 = 0 //module not active + // moduleFee = share * moduleFee = 1 * 5 = 5 * 10^18 + + assert.deepEqual(distribution.recipients, [operators.address, solo2.address, solo3.address]) + assert.deepEqual(toNum(distribution.stakingModuleIds), [module1Id, module3Id, module4Id]) + assert.deepEqual(toNum(distribution.stakingModuleFees), [1 * 10 ** 18, 3 * 10 ** 18, 0]) + assert.equal(toNum(distribution.totalFee), 10 * 10 ** 18) + }) + }) + + describe('getStakingFeeAggregateDistribution()', async () => { + before(snapshot) + after(revert) + + it('works with empty modules', async () => { + const distribution = await router.getStakingFeeAggregateDistribution() + + assert.equal(+distribution.modulesFee, 0) + assert.equal(+distribution.treasuryFee, 0) + assert.equal(+distribution.basePrecision, new BN(10).pow(new BN(20))) + }) + + it('add one module', async () => { + await router.addStakingModule( + 'Curated', + operators.address, + 10_000, // 100 % _targetShare + 500, // 50 % _moduleFee + 500, // 50 % _treasuryFee + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + }) + + it('prepare node operators', async () => { + const config = { + name: 'test', + rewardAddress: ADDRESS_1, + totalSigningKeysCount: 13, + vettedSigningKeysCount: 4, + depositedSigningKeysCount: 7, + exitedSigningKeysCount: 5, + } + + await operators.testing_addNodeOperator( + config.name, + config.rewardAddress, + config.totalSigningKeysCount, + config.vettedSigningKeysCount, + config.depositedSigningKeysCount, + config.exitedSigningKeysCount + ) + }) + + it('works with empty modules', async () => { + const distribution = await router.getStakingFeeAggregateDistribution() + + assert.equal(+distribution.modulesFee, 5 * 10 ** 18) + assert.equal(+distribution.treasuryFee, 5 * 10 ** 18) + assert.equal(+distribution.basePrecision, new BN(10).pow(new BN(20))) + }) + + it('add next module', async () => { + await router.addStakingModule( + 'Solo1', + solo1.address, + 10_000, // 100 % _targetShare + 500, // 50 % _moduleFee + 500, // 50 % _treasuryFee + { from: admin } + ) + }) + + it('works 2 active modules', async () => { + const distribution = await router.getStakingFeeAggregateDistribution() + + assert.equal(+distribution.modulesFee, 5 * 10 ** 18) + assert.equal(+distribution.treasuryFee, 5 * 10 ** 18) + assert.equal(+distribution.basePrecision, new BN(10).pow(new BN(20))) + }) + + it('add next module', async () => { + // add solo2 with node operators + await router.addStakingModule( + 'Solo2', + solo2.address, + 2000, // 20 % _targetShare + 500, // 40 % _moduleFee + 500, // 40 % _treasuryFee + { from: admin } + ) + module3Id = +(await router.getStakingModuleIds())[2] + + config = { + name: 'Solo2', + rewardAddress: ADDRESS_2, + totalSigningKeysCount: 10, + vettedSigningKeysCount: 10, + depositedSigningKeysCount: 7, + exitedSigningKeysCount: 1, + } + + await solo2.testing_addNodeOperator( + config.name, + config.rewardAddress, + config.totalSigningKeysCount, + config.vettedSigningKeysCount, + config.depositedSigningKeysCount, + config.exitedSigningKeysCount + ) + /// + + // add solo3 with node operators, but stop this module + await router.addStakingModule( + 'Solo3', + solo3.address, + 2000, // 20 % _targetShare + 700, // 40 % _moduleFee + 300, // 40 % _treasuryFee + { from: admin } + ) + module4Id = +(await router.getStakingModuleIds())[3] + + config = { + name: 'Solo3', + rewardAddress: ADDRESS_3, + totalSigningKeysCount: 13, + vettedSigningKeysCount: 4, + depositedSigningKeysCount: 7, + exitedSigningKeysCount: 5, + } + + await solo3.testing_addNodeOperator( + config.name, + config.rewardAddress, + config.totalSigningKeysCount, + config.vettedSigningKeysCount, + config.depositedSigningKeysCount, + config.exitedSigningKeysCount + ) + }) + + it('works 2 active modules and 1 stopped and 1 without active validators', async () => { + await router.setStakingModuleStatus(module4Id, StakingModuleStatus.Stopped, { from: admin }) + let distribution = await router.getStakingRewardsDistribution() + + const lengthShouldBe = distribution.stakingModuleFees.length + assert.equal(lengthShouldBe, 3) + assert.equal(distribution.recipients.length, lengthShouldBe) + assert.equal(distribution.stakingModuleIds.length, lengthShouldBe) + assert.equal(distribution.stakingModuleFees.length, lengthShouldBe) + + // m1 - 2 active, 5% + // m2 - 0 active, 5% + // m3 - 6 active, 5% + // m4 - 2 active, 7% stopped + + assert.deepEqual(distribution.recipients, [operators.address, solo2.address, solo3.address]) + assert.deepEqual(toNum(distribution.stakingModuleIds), [module1Id, module3Id, module4Id]) + assert.deepEqual(toNum(distribution.stakingModuleFees), [1 * 10 ** 18, 3 * 10 ** 18, 0]) + assert.equal(toNum(distribution.totalFee), 10 * 10 ** 18) + + distribution = await router.getStakingFeeAggregateDistribution() + assert.equal(+distribution.modulesFee, (1 + 3) * 10 ** 18) + assert.equal(+distribution.treasuryFee, 6 * 10 ** 18) + assert.equal(+distribution.basePrecision, new BN(10).pow(new BN(20))) + }) + + it('getTotalFeeE4Precision', async () => { + const totalFeeE4 = await router.getTotalFeeE4Precision() + const fee = await router.getStakingFeeAggregateDistributionE4Precision() + assert.equal(+totalFeeE4, +fee.modulesFee + +fee.treasuryFee) + }) + }) +}) diff --git a/test/0.8.9/staking-router-allocation-combinations.test.js b/test/0.8.9/staking-router/staking-router-allocation-combinations.test.js similarity index 70% rename from test/0.8.9/staking-router-allocation-combinations.test.js rename to test/0.8.9/staking-router/staking-router-allocation-combinations.test.js index 5913aa52b..e223a37f2 100644 --- a/test/0.8.9/staking-router-allocation-combinations.test.js +++ b/test/0.8.9/staking-router/staking-router-allocation-combinations.test.js @@ -1,5 +1,5 @@ const { artifacts, contract, ethers } = require('hardhat') -const { assert } = require('../helpers/assert') +const { assert } = require('../../helpers/assert') const { BigNumber } = require('ethers') const StakingRouter = artifacts.require('StakingRouterMock.sol') const StakingModuleMock = artifacts.require('StakingModuleMock.sol') @@ -10,7 +10,7 @@ const BASIS_POINTS_BASE = 100_00 contract('StakingRouter', (accounts) => { let evmSnapshotId let depositContract, stakingRouter - let StakingModule1, StakingModule2 + let StakingModule1, StakingModule2, StakingModule3 const [deployer, lido, admin] = accounts before(async () => { @@ -19,10 +19,12 @@ contract('StakingRouter', (accounts) => { const mocks = await Promise.all([ StakingModuleMock.new({ from: deployer }), StakingModuleMock.new({ from: deployer }), + StakingModuleMock.new({ from: deployer }), ]) StakingModule1 = mocks[0] StakingModule2 = mocks[1] + StakingModule3 = mocks[2] const wc = '0x'.padEnd(66, '1234') await stakingRouter.initialize(admin, lido, wc, { from: deployer }) @@ -56,6 +58,85 @@ contract('StakingRouter', (accounts) => { const MODULE_AVAILABLE_KEYS_CASES = [0, 1, 10_000] const MODULE_ACTIVE_KEYS_CASES = [0, 1, 10_000] + describe('Deposits allocation with paused modules', () => { + it('allocates correctly if some staking modules are paused', async () => { + // add staking module 1 + await stakingRouter.addStakingModule( + 'Module 1', + StakingModule1.address, + 100_00, // target share BP + 10_00, // staking module fee BP + 50_00, // treasury fee BP + { from: admin } + ) + + const sm1AvailableKeysCount = 100 + await StakingModule1.setAvailableKeysCount(sm1AvailableKeysCount) + assert.equals(await StakingModule1.getAvailableValidatorsCount(), sm1AvailableKeysCount) + + const sm1ActiveKeysCount = 15000 + await StakingModule1.setActiveValidatorsCount(sm1ActiveKeysCount) + assert.equals(await StakingModule1.getActiveValidatorsCount(), sm1ActiveKeysCount) + + // add staking module 2 + await stakingRouter.addStakingModule( + 'Module 2', + StakingModule2.address, + 10_00, // target share BP + 10_00, // staking module fee BP + 50_00, // treasury fee BP + { from: admin } + ) + + const sm2AvailableKeysCount = 500 + await StakingModule2.setAvailableKeysCount(sm2AvailableKeysCount) + assert.equals(await StakingModule2.getAvailableValidatorsCount(), sm2AvailableKeysCount) + + const sm2ActiveKeysCount = 100 + await StakingModule2.setActiveValidatorsCount(sm2ActiveKeysCount) + assert.equals(await StakingModule2.getActiveValidatorsCount(), sm2ActiveKeysCount) + // add staking module 3 + await stakingRouter.addStakingModule( + 'Module 3', + StakingModule3.address, + 7_00, // target share BP + 5_00, // staking module fee BP + 0, // treasury fee BP + { from: admin } + ) + + const sm3AvailableKeysCount = 300 + await StakingModule3.setAvailableKeysCount(sm3AvailableKeysCount) + assert.equals(await StakingModule3.getAvailableValidatorsCount(), sm3AvailableKeysCount) + + const sm3ActiveKeysCount = 150 + await StakingModule3.setActiveValidatorsCount(sm3ActiveKeysCount) + assert.equals(await StakingModule3.getActiveValidatorsCount(), sm3ActiveKeysCount) + + const { allocated: allocated1, allocations: allocations1 } = await stakingRouter.getDepositsAllocation(100) + assert.equals(allocated1, 100) + assert.equals(allocations1[0], 15000) + assert.equals(allocations1[1], 175) + assert.equals(allocations1[2], 175) + + await stakingRouter.pauseStakingModule(1, { from: admin }) + const { allocated: allocated2, allocations: allocations2 } = await stakingRouter.getDepositsAllocation(100) + + assert.equals(allocated2, 100) + assert.equals(allocations2[0], 15000) + assert.equals(allocations2[1], 175) + assert.equals(allocations2[2], 175) + + await stakingRouter.pauseStakingModule(2, { from: admin }) + const { allocated: allocated3, allocations: allocations3 } = await stakingRouter.getDepositsAllocation(100) + + assert.equals(allocated3, 100) + assert.equals(allocations3[0], 15000) + assert.equals(allocations3[1], 100) + assert.equals(allocations3[2], 250) + }) + }) + describe('Single staking module', function () { this.timeout(30_000, 'Test suite took too long') beforeEach(async () => { diff --git a/test/0.8.9/staking-router-deposits-allocation.test.js b/test/0.8.9/staking-router/staking-router-deposits-allocation.test.js similarity index 99% rename from test/0.8.9/staking-router-deposits-allocation.test.js rename to test/0.8.9/staking-router/staking-router-deposits-allocation.test.js index 8cffae23a..630a54445 100644 --- a/test/0.8.9/staking-router-deposits-allocation.test.js +++ b/test/0.8.9/staking-router/staking-router-deposits-allocation.test.js @@ -1,5 +1,5 @@ const { artifacts, contract, ethers } = require('hardhat') -const { assert } = require('../helpers/assert') +const { assert } = require('../../helpers/assert') const StakingRouter = artifacts.require('StakingRouterMock.sol') const StakingModuleMock = artifacts.require('StakingModuleMock.sol') diff --git a/test/0.8.9/staking-router/staking-router-deposits.test.js b/test/0.8.9/staking-router/staking-router-deposits.test.js new file mode 100644 index 000000000..57a7c3e30 --- /dev/null +++ b/test/0.8.9/staking-router/staking-router-deposits.test.js @@ -0,0 +1,244 @@ +const { contract, ethers, web3 } = require('hardhat') + +const { EvmSnapshot } = require('../../helpers/blockchain') +const { setupNodeOperatorsRegistry } = require('../../helpers/staking-modules') +const { deployProtocol } = require('../../helpers/protocol') + +const { ETH, genKeys } = require('../../helpers/utils') +const { assert } = require('../../helpers/assert') +const { ZERO_BYTES32 } = require('../../helpers/constants') + +const ADDRESS_1 = '0x0000000000000000000000000000000000000001' +const ADDRESS_2 = '0x0000000000000000000000000000000000000002' + +contract('StakingRouter', ([depositor, stranger]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) + let depositContract, router + let lido, operators, voting + let curatedModuleId + + const snapshot = () => evmSnapshot.make() + const revert = () => evmSnapshot.revert() + + before(async () => { + const deployed = await deployProtocol({ + depositSecurityModuleFactory: async () => { + return { address: depositor } + }, + }) + + lido = deployed.pool + router = deployed.stakingRouter + operators = await setupNodeOperatorsRegistry(deployed, true) + voting = deployed.voting.address + depositContract = deployed.depositContract + + // add role + await router.grantRole(await router.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), depositor, { from: depositor }) + }) + + describe('Make deposit', () => { + before(snapshot) + after(revert) + + it('add module', async () => { + await router.addStakingModule( + 'Curated', + operators.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: voting } + ) + curatedModuleId = +(await router.getStakingModuleIds())[0] + }) + + it('reverts if no DSM role', async () => { + const depositsCount = 150 + + await assert.reverts( + lido.deposit(depositsCount, curatedModuleId, '0x', { from: stranger }), + 'APP_AUTH_DSM_FAILED' + ) + await assert.reverts(lido.deposit(depositsCount, curatedModuleId, '0x', { from: voting }), 'APP_AUTH_DSM_FAILED') + }) + + it('reverts if deposit() not from lido address', async () => { + const depositsCount = 150 + await assert.reverts( + router.deposit(depositsCount, curatedModuleId, '0x', { from: voting }), + 'AppAuthLidoFailed()' + ) + }) + + it('add initial balance and keys', async () => { + // balance are initial + assert.equals(await web3.eth.getBalance(lido.address), ETH(1)) + assert.equals(await web3.eth.getBalance(router.address), 0) + + const sendEthForKeys = ETH(101 * 32 - 1) + const totalPooledEther = ETH(101 * 32) + + await web3.eth.sendTransaction({ value: sendEthForKeys, to: lido.address, from: stranger }) + assert.equals(await lido.getBufferedEther(), totalPooledEther) + + // updated balance are lido 100 && sr 0 + assert.equals(await web3.eth.getBalance(lido.address), totalPooledEther) + assert.equals(await web3.eth.getBalance(router.address), 0) + + // prepare node operators + await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) + await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) + + // add 150 keys to module + const keysAmount = 50 + const keys1 = genKeys(keysAmount) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + + await operators.setNodeOperatorStakingLimit(0, 100000, { from: voting }) + await operators.setNodeOperatorStakingLimit(1, 100000, { from: voting }) + }) + + it('can not deposit with unset withdrawalCredentials', async () => { + // old WC + const wcBefore = await router.getWithdrawalCredentials() + + // unset WC + const newWC = '0x' + const tx = await router.setWithdrawalCredentials(newWC, { from: voting }) + await assert.emits(tx, 'WithdrawalCredentialsSet', { withdrawalCredentials: ZERO_BYTES32 }) + assert.equal(await router.getWithdrawalCredentials(), ZERO_BYTES32) + + // add 150 keys to module + const keysAmount = 1 + const keys1 = genKeys(keysAmount) + await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.setNodeOperatorStakingLimit(0, 100000, { from: voting }) + + const depositsCount = 100 + await assert.reverts( + lido.deposit(depositsCount, curatedModuleId, '0x', { from: depositor }), + `EmptyWithdrawalsCredentials()` + ) + + const tx2 = await router.setWithdrawalCredentials(wcBefore, { from: voting }) + const wcAfter = await router.getWithdrawalCredentials() + await assert.emits(tx2, 'WithdrawalCredentialsSet', { withdrawalCredentials: wcBefore }) + assert.equal(await router.getWithdrawalCredentials(), wcBefore) + assert.equal(wcBefore, wcAfter) + }) + + it('Lido.deposit() :: check deposit with keys', async () => { + // prepare node operators + await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) + await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) + + // add 150 keys to module + const keysAmount = 50 + const keys1 = genKeys(keysAmount) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + + await operators.setNodeOperatorStakingLimit(0, 100000, { from: voting }) + await operators.setNodeOperatorStakingLimit(1, 100000, { from: voting }) + + const depositsCount = 100 + + const receipt = await lido.deposit(depositsCount, curatedModuleId, '0x', { + from: depositor, + }) + const currentBlockNumber = await web3.eth.getBlockNumber() + + assert.equals(await depositContract.totalCalls(), 100, 'invalid deposits count') + + // on deposit we return balance to Lido + assert.equals(await web3.eth.getBalance(lido.address), ETH(32), 'invalid lido balance') + assert.equals(await web3.eth.getBalance(router.address), 0, 'invalid staking_router balance') + + assert.equals(await lido.getBufferedEther(), ETH(32), 'invalid total buffer') + + assert.emits(receipt, 'Unbuffered', { amount: ETH(depositsCount * 32) }) + + const lastModuleBlock = await router.getStakingModuleLastDepositBlock(curatedModuleId) + assert.equal(currentBlockNumber, +lastModuleBlock) + }) + }) + + describe('test deposit from staking router directly', async () => { + before(snapshot) + after(revert) + + it('add module', async () => { + await router.addStakingModule( + 'Curated', + operators.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: voting } + ) + curatedModuleId = +(await router.getStakingModuleIds())[0] + }) + + it('prepare node operators', async () => { + // balance are initial + assert.equals(await web3.eth.getBalance(lido.address), ETH(1)) + assert.equals(await web3.eth.getBalance(router.address), 0) + + const sendEthForKeys = ETH(101 * 32 - 1) + const totalPooledEther = ETH(101 * 32) + + await web3.eth.sendTransaction({ value: sendEthForKeys, to: lido.address, from: stranger }) + assert.equals(await lido.getBufferedEther(), totalPooledEther) + + // updated balance are lido 100 && sr 0 + assert.equals(await web3.eth.getBalance(lido.address), totalPooledEther) + assert.equals(await web3.eth.getBalance(router.address), 0) + + // prepare node operators + await operators.addNodeOperator('1', ADDRESS_1, { from: voting }) + await operators.addNodeOperator('2', ADDRESS_2, { from: voting }) + + // add 150 keys to module + const keysAmount = 50 + const keys1 = genKeys(keysAmount) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + await operators.addSigningKeys(0, keysAmount, keys1.pubkeys, keys1.sigkeys, { from: voting }) + + await operators.setNodeOperatorStakingLimit(0, 100000, { from: voting }) + await operators.setNodeOperatorStakingLimit(1, 100000, { from: voting }) + }) + + it('zero deposits just updates module lastDepositBlock', async () => { + const depositsCount = 0 + // allow tx `StakingRouter.deposit()` from the Lido contract addr + await ethers.provider.send('hardhat_impersonateAccount', [lido.address]) + const value = ETH(0) + const receipt = await router.deposit(depositsCount, curatedModuleId, '0x', { from: lido.address, value }) + + assert.emits(receipt, 'StakingRouterETHDeposited', { stakingModuleId: curatedModuleId, amount: value }) + + const lastModuleBlock = await router.getStakingModuleLastDepositBlock(curatedModuleId) + const currentBlockNumber = await web3.eth.getBlockNumber() + assert.equal(currentBlockNumber, +lastModuleBlock) + }) + + it('deposits not work if depositValue != depositsCount * 32 ', async () => { + const depositsCount = 100 + + // allow tx `StakingRouter.deposit()` from the Lido contract addr + await ethers.provider.send('hardhat_impersonateAccount', [lido.address]) + + const value = ETH(1) + await assert.reverts( + router.deposit(depositsCount, curatedModuleId, '0x', { from: lido.address, value }), + `InvalidDepositsValue(${value}, ${depositsCount})` + ) + }) + }) +}) diff --git a/test/0.8.9/staking-router-keys-reporting.test.js b/test/0.8.9/staking-router/staking-router-keys-reporting.test.js similarity index 72% rename from test/0.8.9/staking-router-keys-reporting.test.js rename to test/0.8.9/staking-router/staking-router-keys-reporting.test.js index c2ee02f16..7f7a825f4 100644 --- a/test/0.8.9/staking-router-keys-reporting.test.js +++ b/test/0.8.9/staking-router/staking-router-keys-reporting.test.js @@ -1,13 +1,14 @@ const { artifacts, contract, ethers } = require('hardhat') -const { EvmSnapshot } = require('../helpers/blockchain') -const { assert } = require('../helpers/assert') -const { hex, hexConcat, toNum } = require('../helpers/utils') +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 StakingRouter = artifacts.require('StakingRouterMock.sol') const StakingModuleMock = artifacts.require('StakingModuleMock.sol') const DepositContractMock = artifacts.require('DepositContractMock.sol') -contract('StakingRouter', ([deployer, lido, admin]) => { +contract('StakingRouter', ([deployer, lido, admin, stranger]) => { const evmSnapshot = new EvmSnapshot(ethers.provider) let depositContract, router @@ -82,6 +83,24 @@ contract('StakingRouter', ([deployer, lido, admin]) => { assert.equals(totalExited, 0) }) + it('reverts total exited validators without REPORT_EXITED_VALIDATORS_ROLE', async () => { + await assert.revertsOZAccessControl( + router.updateExitedValidatorsCountByStakingModule([module1Id + 1], [1], { from: stranger }), + stranger, + 'REPORT_EXITED_VALIDATORS_ROLE' + ) + }) + + it('reverts when stakingModuleIds and exitedValidatorsCounts lengths mismatch', async () => { + const stakingModuleIds = [1, 2] + const exitedValidatorsCounts = [1, 2, 3] + await assert.reverts( + router.updateExitedValidatorsCountByStakingModule(stakingModuleIds, exitedValidatorsCounts, { from: admin }), + `ArraysLengthMismatch`, + [stakingModuleIds.length, exitedValidatorsCounts.length] + ) + }) + it('reporting total exited validators of a non-existent module reverts', async () => { await assert.reverts( router.updateExitedValidatorsCountByStakingModule([module1Id + 1], [1], { from: admin }), @@ -114,6 +133,14 @@ contract('StakingRouter', ([deployer, lido, admin]) => { assert.equal(callInfo.onExitedAndStuckValidatorsCountsUpdated.callCount, 0) }) + it('reverts without role onValidatorsCountsByNodeOperatorReportingFinished', async () => { + await assert.revertsOZAccessControl( + router.onValidatorsCountsByNodeOperatorReportingFinished({ from: stranger }), + stranger, + 'REPORT_EXITED_VALIDATORS_ROLE' + ) + }) + it(`calling onValidatorsCountsByNodeOperatorReportingFinished doesn't call anything on the module`, async () => { await router.onValidatorsCountsByNodeOperatorReportingFinished({ from: admin }) @@ -123,6 +150,22 @@ contract('StakingRouter', ([deployer, lido, admin]) => { assert.equal(callInfo.onExitedAndStuckValidatorsCountsUpdated.callCount, 0) }) + it('reverts without role reportStakingModuleStuckValidatorsCountByNodeOperator()', async () => { + const nonExistentModuleId = module1Id + 1 + const nodeOpIdsData = hexConcat(hex(1, 8)) + const validatorsCountsData = hexConcat(hex(1, 16)) + await assert.revertsOZAccessControl( + router.reportStakingModuleStuckValidatorsCountByNodeOperator( + nonExistentModuleId, + nodeOpIdsData, + validatorsCountsData, + { from: stranger } + ), + stranger, + 'REPORT_EXITED_VALIDATORS_ROLE' + ) + }) + it('reporting stuck validators by node op of a non-existent module reverts', async () => { const nonExistentModuleId = module1Id + 1 const nodeOpIdsData = hexConcat(hex(1, 8)) @@ -287,6 +330,14 @@ contract('StakingRouter', ([deployer, lido, admin]) => { ) }) + it('reverts reportStakingModuleExitedValidatorsCountByNodeOperator() without REPORT_EXITED_VALIDATORS_ROLE', async () => { + await assert.revertsOZAccessControl( + router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, '0x', '0x', { from: stranger }), + stranger, + 'REPORT_EXITED_VALIDATORS_ROLE' + ) + }) + it('passing empty data while reporting exited validators by node operator reverts', async () => { await assert.reverts( router.reportStakingModuleExitedValidatorsCountByNodeOperator(module1Id, '0x', '0x', { from: admin }), @@ -398,7 +449,8 @@ contract('StakingRouter', ([deployer, lido, admin]) => { }) // eslint-disable-next-line prettier/prettier - it(`now that exited validators totals in the router and in the module match, calling` + + it( + `now that exited validators totals in the router and in the module match, calling` + `onValidatorsCountsByNodeOperatorReportingFinished calls ` + `onExitedAndStuckValidatorsCountsUpdated on the module`, async () => { @@ -413,7 +465,8 @@ contract('StakingRouter', ([deployer, lido, admin]) => { ) // eslint-disable-next-line prettier/prettier - it(`calling onValidatorsCountsByNodeOperatorReportingFinished one more time calls ` + + it( + `calling onValidatorsCountsByNodeOperatorReportingFinished one more time calls ` + `onExitedAndStuckValidatorsCountsUpdated on the module again`, async () => { await router.onValidatorsCountsByNodeOperatorReportingFinished({ from: admin }) @@ -425,6 +478,30 @@ contract('StakingRouter', ([deployer, lido, admin]) => { assert.equal(callInfo.updateExitedValidatorsCount.callCount, 2) } ) + + 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, { + from: admin, + }) + const stakingModuleId = await router.getStakingModulesCount() + + const tx = await router.onValidatorsCountsByNodeOperatorReportingFinished({ from: admin }) + + assert.emits(tx, 'ExitedAndStuckValidatorsCountsUpdateFailed', { + stakingModuleId, + lowLevelRevertData: '0x4e487b710000000000000000000000000000000000000000000000000000000000000001', + }) + }) }) describe('two staking modules', async () => { @@ -481,6 +558,38 @@ contract('StakingRouter', ([deployer, lido, admin]) => { assert.equals(totalExited, 5) }) + it('revert on decreased exited keys for modules', async () => { + await assert.reverts( + router.updateExitedValidatorsCountByStakingModule(moduleIds, [2, 1], { from: admin }), + `ExitedValidatorsCountCannotDecrease()` + ) + }) + + it('emit StakingModuleExitedValidatorsIncompleteReporting() if module not update', async () => { + const { exitedValidatorsCount: prevReportedExitedValidatorsCount1 } = await router.getStakingModule( + moduleIds[0] + ) + const { exitedValidatorsCount: prevReportedExitedValidatorsCount2 } = await router.getStakingModule( + moduleIds[1] + ) + + assert.equal(prevReportedExitedValidatorsCount1, 3) + assert.equal(prevReportedExitedValidatorsCount2, 2) + + const { totalExitedValidators: totalExitedValidators1 } = await module1.getStakingModuleSummary() + const { totalExitedValidators: totalExitedValidators2 } = await module2.getStakingModuleSummary() + + const tx = await router.updateExitedValidatorsCountByStakingModule(moduleIds, [3, 2], { from: admin }) + assert.emits(tx, 'StakingModuleExitedValidatorsIncompleteReporting', { + stakingModuleId: moduleIds[0], + unreportedExitedValidatorsCount: prevReportedExitedValidatorsCount1 - totalExitedValidators1, + }) + assert.emits(tx, 'StakingModuleExitedValidatorsIncompleteReporting', { + stakingModuleId: moduleIds[1], + unreportedExitedValidatorsCount: prevReportedExitedValidatorsCount2 - totalExitedValidators2, + }) + }) + it('no functions were called on any module', async () => { const callInfo1 = await getCallInfo(module1) assert.equal(callInfo1.updateStuckValidatorsCount.callCount, 0) @@ -656,7 +765,8 @@ contract('StakingRouter', ([deployer, lido, admin]) => { }) // eslint-disable-next-line prettier/prettier - it(`now that router's view on exited validators total match the module 2's view,` + + it( + `now that router's view on exited validators total match the module 2's view,` + `calling onValidatorsCountsByNodeOperatorReportingFinished calls ` + `onExitedAndStuckValidatorsCountsUpdated on the module 2`, async () => { @@ -709,7 +819,8 @@ contract('StakingRouter', ([deployer, lido, admin]) => { }) // eslint-disable-next-line prettier/prettier - it(`now that router's view on exited validators total match the both modules' view,` + + it( + `now that router's view on exited validators total match the both modules' view,` + `calling onValidatorsCountsByNodeOperatorReportingFinished calls ` + `onExitedAndStuckValidatorsCountsUpdated on both modules`, async () => { @@ -730,4 +841,143 @@ contract('StakingRouter', ([deployer, lido, admin]) => { ) }) }) + + describe('unsafeSetExitedValidatorsCount()', async () => { + before(snapshot) + after(revert) + + let module1Id + + it('adding the only module', async () => { + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // target share 100 % + 1_000, // module fee 10 % + 5_000, // treasury fee 5 % + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + }) + + it('reverts without UNSAFE_SET_EXITED_VALIDATORS_ROLE role', async () => { + await assert.revertsOZAccessControl( + router.unsafeSetExitedValidatorsCount(0, 0, 0, [0, 0, 0, 0, 0, 0], { from: stranger }), + stranger, + 'UNSAFE_SET_EXITED_VALIDATORS_ROLE' + ) + }) + + it('reverts if module not exists', async () => { + await router.grantRole(await router.UNSAFE_SET_EXITED_VALIDATORS_ROLE(), admin, { from: admin }) + await assert.reverts( + router.unsafeSetExitedValidatorsCount(0, 0, 0, [0, 0, 0, 0, 0, 0], { from: admin }), + 'StakingModuleUnregistered()' + ) + }) + + it('reverts with UnexpectedCurrentValidatorsCount(0, 0, 0)', async () => { + await router.grantRole(await router.UNSAFE_SET_EXITED_VALIDATORS_ROLE(), admin, { from: admin }) + + const nodeOperatorId = 0 + const ValidatorsCountsCorrection = { + currentModuleExitedValidatorsCount: 0, + currentNodeOperatorExitedValidatorsCount: 0, + currentNodeOperatorStuckValidatorsCount: 0, + newModuleExitedValidatorsCount: 0, + newNodeOperatorExitedValidatorsCount: 0, + newNodeOperatorStuckValidatorsCount: 0, + } + + const summary = { + isTargetLimitActive: false, + targetValidatorsCount: 0, + stuckValidatorsCount: 0, + refundedValidatorsCount: 0, + stuckPenaltyEndTimestamp: 0, + totalExitedValidators: 0, + totalDepositedValidators: 0, + depositableValidatorsCount: 0, + } + + // first correction + await router.updateExitedValidatorsCountByStakingModule([module1Id], [10], { from: admin }) + await assert.reverts( + router.unsafeSetExitedValidatorsCount(module1Id, nodeOperatorId, false, ValidatorsCountsCorrection, { + from: admin, + }), + `UnexpectedCurrentValidatorsCount(10, 0, 0)` + ) + + ValidatorsCountsCorrection.currentModuleExitedValidatorsCount = 10 + ValidatorsCountsCorrection.newModuleExitedValidatorsCount = 11 + await router.unsafeSetExitedValidatorsCount(module1Id, 0, false, ValidatorsCountsCorrection, { from: admin }) + + let lastCall = await module1.lastCall_unsafeUpdateValidatorsCount() + assert.equal(+lastCall.callCount, 1) + assert.equal(+lastCall.nodeOperatorId, 0) + assert.equal(+lastCall.exitedValidatorsKeysCount, 0) + assert.equal(+lastCall.stuckValidatorsKeysCount, 0) + + let stats1 = await router.getStakingModule(module1Id) + assert.equal(+stats1.exitedValidatorsCount, 11) + + // second correction + ValidatorsCountsCorrection.currentModuleExitedValidatorsCount = 11 + ValidatorsCountsCorrection.newModuleExitedValidatorsCount = 11 + + ValidatorsCountsCorrection.currentNodeOperatorExitedValidatorsCount = 20 + summary.totalExitedValidators = 21 + await module1.setNodeOperatorSummary(nodeOperatorId, summary) + await assert.reverts( + router.unsafeSetExitedValidatorsCount(module1Id, 0, false, ValidatorsCountsCorrection, { from: admin }), + `UnexpectedCurrentValidatorsCount(11, 21, 0)` + ) + + ValidatorsCountsCorrection.currentModuleExitedValidatorsCount = 11 + ValidatorsCountsCorrection.newModuleExitedValidatorsCount = 11 + + ValidatorsCountsCorrection.currentNodeOperatorExitedValidatorsCount = 21 + ValidatorsCountsCorrection.newNodeOperatorExitedValidatorsCount = 22 + + await router.unsafeSetExitedValidatorsCount(module1Id, 0, false, ValidatorsCountsCorrection, { from: admin }) + lastCall = await module1.lastCall_unsafeUpdateValidatorsCount() + assert.equal(+lastCall.callCount, 2) + assert.equal(+lastCall.nodeOperatorId, 0) + assert.equal(+lastCall.exitedValidatorsKeysCount, 22) + assert.equal(+lastCall.stuckValidatorsKeysCount, 0) + + stats1 = await router.getStakingModule(module1Id) + assert.equal(+stats1.exitedValidatorsCount, 11) + + // // //check 3d condition + ValidatorsCountsCorrection.currentNodeOperatorExitedValidatorsCount = 22 + ValidatorsCountsCorrection.newNodeOperatorExitedValidatorsCount = 22 + + ValidatorsCountsCorrection.currentNodeOperatorStuckValidatorsCount = 30 + ValidatorsCountsCorrection.newNodeOperatorStuckValidatorsCount = 32 + + summary.totalExitedValidators = 22 + summary.stuckValidatorsCount = 31 + await module1.setNodeOperatorSummary(nodeOperatorId, summary) + await assert.reverts( + router.unsafeSetExitedValidatorsCount(module1Id, 0, false, ValidatorsCountsCorrection, { from: admin }), + `UnexpectedCurrentValidatorsCount(11, 22, 31)` + ) + + ValidatorsCountsCorrection.currentNodeOperatorStuckValidatorsCount = 31 + ValidatorsCountsCorrection.newNodeOperatorStuckValidatorsCount = 32 + + await router.unsafeSetExitedValidatorsCount(module1Id, 0, false, ValidatorsCountsCorrection, { from: admin }) + lastCall = await module1.lastCall_unsafeUpdateValidatorsCount() + assert.equal(+lastCall.callCount, 3) + assert.equal(+lastCall.nodeOperatorId, 0) + assert.equal(+lastCall.exitedValidatorsKeysCount, 22) + assert.equal(+lastCall.stuckValidatorsKeysCount, 32) + + assert.equal(+(await module1.callCount_onExitedAndStuckValidatorsCountsUpdated()), 0) + await router.unsafeSetExitedValidatorsCount(module1Id, 0, true, ValidatorsCountsCorrection, { from: admin }) + assert.equal(+(await module1.callCount_onExitedAndStuckValidatorsCountsUpdated()), 1) + }) + }) }) diff --git a/test/0.8.9/staking-router/staking-router.test.js b/test/0.8.9/staking-router/staking-router.test.js new file mode 100644 index 000000000..6fed47211 --- /dev/null +++ b/test/0.8.9/staking-router/staking-router.test.js @@ -0,0 +1,1164 @@ +const { artifacts, contract, ethers } = require('hardhat') +const { MaxUint256 } = require('@ethersproject/constants') +const { utils } = require('web3') +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 OssifiableProxy = artifacts.require('OssifiableProxy.sol') +const DepositContractMock = artifacts.require('DepositContractMock') +const StakingRouter = artifacts.require('StakingRouter.sol') +const StakingModuleMock = artifacts.require('StakingModuleMock.sol') + +const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000' +const MANAGE_WITHDRAWAL_CREDENTIALS_ROLE = utils.soliditySha3('MANAGE_WITHDRAWAL_CREDENTIALS_ROLE') +const STAKING_MODULE_PAUSE_ROLE = utils.soliditySha3('STAKING_MODULE_PAUSE_ROLE') +const STAKING_MODULE_RESUME_ROLE = utils.soliditySha3('STAKING_MODULE_RESUME_ROLE') +const STAKING_MODULE_MANAGE_ROLE = utils.soliditySha3('STAKING_MODULE_MANAGE_ROLE') + +const StakingModuleStatus = { + Active: 0, // deposits and rewards allowed + DepositsPaused: 1, // deposits NOT allowed, rewards allowed + Stopped: 2, // deposits and rewards NOT allowed +} + +contract('StakingRouter', ([deployer, lido, admin, appManager, stranger]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) + + const snapshot = () => evmSnapshot.make() + const revert = () => evmSnapshot.revert() + + let depositContract, router + let initialTx + let module1, module2 + const wc = '0x'.padEnd(66, '1234') + + before(async () => { + depositContract = await DepositContractMock.new({ from: deployer }) + + const impl = await StakingRouter.new(depositContract.address, { from: deployer }) + const proxy = await OssifiableProxy.new(impl.address, deployer, '0x') + router = await StakingRouter.at(proxy.address) + ;[module1, module2] = await Promise.all([ + StakingModuleMock.new({ from: deployer }), + StakingModuleMock.new({ from: deployer }), + ]) + + const wc = '0x'.padEnd(66, '1234') + initialTx = await router.initialize(admin, lido, 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 }) + }) + + describe('setup env', async () => { + it('initialized correctly', async () => { + assert.equals(await router.getContractVersion(), 1) + assert.equals(await router.getWithdrawalCredentials(), wc) + assert.equals(await router.getLido(), lido) + assert.equals(await router.getStakingModulesCount(), 0) + + assert.equals(await router.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 1) + assert.equals(await router.hasRole(DEFAULT_ADMIN_ROLE, admin), true) + + assert.equals(initialTx.logs.length, 3) + + await assert.emits(initialTx, 'ContractVersionSet', { version: 1 }) + await assert.emits(initialTx, 'RoleGranted', { role: DEFAULT_ADMIN_ROLE, account: admin, sender: deployer }) + await assert.emits(initialTx, 'WithdrawalCredentialsSet', { withdrawalCredentials: wc }) + }) + + it('init fails on wrong input', async () => { + await assert.revertsWithCustomError( + router.initialize(ZERO_ADDRESS, lido, wc, { from: deployer }), + 'ZeroAddress("_admin")' + ) + await assert.revertsWithCustomError( + router.initialize(admin, ZERO_ADDRESS, wc, { from: deployer }), + 'ZeroAddress("_lido")' + ) + }) + + it('second initialize reverts', async () => { + await assert.revertsWithCustomError( + router.initialize(admin, lido, wc, { from: deployer }), + 'NonZeroContractVersionOnInit()' + ) + }) + + it('stranger is not allowed to grant roles', async () => { + await assert.reverts( + router.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: stranger }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}` + ) + }) + + it('grant role MANAGE_WITHDRAWAL_CREDENTIALS_ROLE', async () => { + const tx = await router.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: admin }) + assert.equals(await router.getRoleMemberCount(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE), 1) + assert.equals(await router.hasRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { + role: MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, + account: appManager, + sender: admin, + }) + }) + + it('grant role STAKING_MODULE_PAUSE_ROLE', async () => { + const tx = await router.grantRole(STAKING_MODULE_PAUSE_ROLE, appManager, { from: admin }) + assert.equals(await router.getRoleMemberCount(STAKING_MODULE_PAUSE_ROLE), 1) + assert.equals(await router.hasRole(STAKING_MODULE_PAUSE_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_PAUSE_ROLE, account: appManager, sender: admin }) + }) + + it('grant role STAKING_MODULE_RESUME_ROLE', async () => { + const tx = await router.grantRole(STAKING_MODULE_RESUME_ROLE, appManager, { from: admin }) + assert.equals(await router.getRoleMemberCount(STAKING_MODULE_RESUME_ROLE), 1) + assert.equals(await router.hasRole(STAKING_MODULE_RESUME_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_RESUME_ROLE, account: appManager, sender: admin }) + }) + + it('grant role STAKING_MODULE_MANAGE_ROLE', async () => { + const tx = await router.grantRole(STAKING_MODULE_MANAGE_ROLE, appManager, { from: admin }) + assert.equals(await router.getRoleMemberCount(STAKING_MODULE_MANAGE_ROLE), 1) + assert.equals(await router.hasRole(STAKING_MODULE_MANAGE_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_MANAGE_ROLE, account: appManager, sender: admin }) + }) + + it('public constants', async () => { + assert.equals(await router.FEE_PRECISION_POINTS(), new BN('100000000000000000000')) + assert.equals(await router.TOTAL_BASIS_POINTS(), 10000) + assert.equals(await router.DEPOSIT_CONTRACT(), depositContract.address) + assert.equals(await router.DEFAULT_ADMIN_ROLE(), DEFAULT_ADMIN_ROLE) + assert.equals(await router.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), MANAGE_WITHDRAWAL_CREDENTIALS_ROLE) + assert.equals(await router.STAKING_MODULE_PAUSE_ROLE(), STAKING_MODULE_PAUSE_ROLE) + assert.equals(await router.STAKING_MODULE_RESUME_ROLE(), STAKING_MODULE_RESUME_ROLE) + assert.equals(await router.STAKING_MODULE_MANAGE_ROLE(), STAKING_MODULE_MANAGE_ROLE) + }) + + it('getDepositsAllocation', async () => { + const keysAllocation = await router.getDepositsAllocation(1000) + + assert.equals(keysAllocation.allocated, 0) + assert.equals(keysAllocation.allocations, []) + }) + }) + + describe('implementation', async () => { + let stakingRouterImplementation + + before(async () => { + await snapshot() + stakingRouterImplementation = await StakingRouter.new(depositContract.address, { from: deployer }) + }) + + after(async () => { + await revert() + }) + + it('contract version is max uint256', async () => { + assert.equals(await stakingRouterImplementation.getContractVersion(), MaxUint256) + }) + + it('initialize reverts on implementation', async () => { + await assert.revertsWithCustomError( + stakingRouterImplementation.initialize(admin, lido, wc, { from: deployer }), + `NonZeroContractVersionOnInit()` + ) + }) + + it('has no granted roles', async () => { + assert.equals(await stakingRouterImplementation.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_PAUSE_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_RESUME_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_MANAGE_ROLE), 0) + }) + + it('state is empty', async () => { + assert.equals(await stakingRouterImplementation.getWithdrawalCredentials(), ZERO_BYTES32) + assert.equals(await stakingRouterImplementation.getLido(), ZERO_ADDRESS) + assert.equals(await stakingRouterImplementation.getStakingModulesCount(), 0) + }) + + it('deposit fails without role', async () => { + await assert.revertsWithCustomError( + stakingRouterImplementation.deposit(100, 0, '0x00', { from: stranger }), + `AppAuthLidoFailed()` + ) + }) + }) + + describe('staking router', async () => { + let stakingModule + before(async () => { + await snapshot() + + stakingModule = await StakingModuleMock.new({ from: deployer }) + + await router.addStakingModule('Test module', stakingModule.address, 100, 1000, 2000, { + from: appManager, + }) + + await stakingModule.setAvailableKeysCount(100, { from: deployer }) + + assert.equals(await stakingModule.getAvailableValidatorsCount(), 100) + }) + + after(async () => { + await revert() + }) + + it('reverts if module address exists', async () => { + await assert.revertsWithCustomError( + router.addStakingModule('Test', stakingModule.address, 100, 1000, 2000, { from: appManager }), + 'StakingModuleAddressExists()' + ) + }) + + it('set withdrawal credentials does not allowed without role', async () => { + const newWC = '0x'.padEnd(66, '5678') + await assert.reverts( + router.setWithdrawalCredentials(newWC, { from: stranger }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${MANAGE_WITHDRAWAL_CREDENTIALS_ROLE}` + ) + }) + + it('set withdrawal credentials', async () => { + const newWC = '0x'.padEnd(66, '5678') + const tx = await router.setWithdrawalCredentials(newWC, { from: appManager }) + + await assert.emits(tx, 'WithdrawalCredentialsSet', { withdrawalCredentials: newWC }) + + assert.equals(await stakingModule.getAvailableValidatorsCount(), 0) + }) + + it('direct transfer fails', async () => { + const value = 100 + await assert.revertsWithCustomError(router.sendTransaction({ value, from: deployer }), `DirectETHTransfer()`) + }) + + it('getStakingModuleNonce', async () => { + await stakingModule.setNonce(100, { from: deployer }) + + assert.equals(await router.getStakingModuleNonce(1), 100) + }) + + it('getStakingModuleActiveValidatorsCount', async () => { + await stakingModule.setActiveValidatorsCount(200, { from: deployer }) + assert.equals(await router.getStakingModuleActiveValidatorsCount(1), 200) + }) + + it('getStakingRewardsDistribution - only one module has active keys', async () => { + await stakingModule.setActiveValidatorsCount(40, { from: deployer }) + + // no active keys for second module + const anotherStakingModule = await StakingModuleMock.new({ from: deployer }) + await router.addStakingModule('Test module 2', anotherStakingModule.address, 100, 1000, 2000, { + from: appManager, + }) + await anotherStakingModule.setAvailableKeysCount(50, { from: deployer }) + + const stakingModuleIds = await router.getStakingModuleIds() + let rewardDistribution = await router.getStakingRewardsDistribution() + + assert.equal(stakingModuleIds.length, 2) + // only one module in distribution + assert.equal(rewardDistribution.stakingModuleIds.length, 1) + assert.deepEqual(rewardDistribution.stakingModuleIds, [stakingModuleIds[0]]) + // expect(rewardDistribution.stakingModuleIds).to.deep.equal([stakingModuleIds[0]]) + const percentPoints = toBN(100) + // 10% = 10% from (100% of active validator) + assert.equal( + rewardDistribution.stakingModuleFees[0].mul(percentPoints).div(rewardDistribution.precisionPoints).toNumber(), + 10 + ) + + // 2nd module has active keys + await anotherStakingModule.setActiveValidatorsCount(10, { from: deployer }) + rewardDistribution = await router.getStakingRewardsDistribution() + assert.deepEqual(rewardDistribution.stakingModuleIds, stakingModuleIds) + // 8% = 10% from (80% of active validator) + assert.equal( + rewardDistribution.stakingModuleFees[0].mul(percentPoints).div(rewardDistribution.precisionPoints).toNumber(), + 8 + ) + // 2% = 10% from (20% of active validator) + assert.equal( + rewardDistribution.stakingModuleFees[1].mul(percentPoints).div(rewardDistribution.precisionPoints).toNumber(), + 2 + ) + }) + + 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, { + from: appManager, + }) + const stakingModuleId = await router.getStakingModulesCount() + assert.isFalse(await router.getStakingModuleIsDepositsPaused(stakingModuleId)) + + const newWC = '0x'.padEnd(66, '5678') + const tx = await router.setWithdrawalCredentials(newWC, { from: appManager }) + + assert.emits(tx, 'WithdrawalsCredentialsChangeFailed', { + stakingModuleId, + lowLevelRevertData: '0x4e487b710000000000000000000000000000000000000000000000000000000000000001', + }) + + assert.emits( + tx, + 'StakingModuleStatusSet', + { + status: 1, + stakingModuleId, + setBy: appManager, + }, + { abi: StakingRouter._json.abi } + ) + + assert.isTrue(await router.getStakingModuleIsDepositsPaused(stakingModuleId)) + }) + }) + + describe('staking modules limit', async () => { + before(snapshot) + after(revert) + + it('staking modules limit is 32', async () => { + for (let i = 0; i < 32; i++) { + const stakingModule = await StakingModuleMock.new({ from: deployer }) + await router.addStakingModule('Test module', stakingModule.address, 100, 100, 100, { from: appManager }) + } + + const oneMoreStakingModule = await StakingModuleMock.new({ from: deployer }) + await assert.revertsWithCustomError( + router.addStakingModule('Test module', oneMoreStakingModule.address, 100, 100, 100, { from: appManager }), + `StakingModulesLimitExceeded()` + ) + }) + }) + + describe('manage staking modules', async () => { + let stakingModule1, stakingModule2 + + const stakingModulesParams = [ + { + name: 'Test module 1', + targetShare: 1000, + stakingModuleFee: 2000, + treasuryFee: 200, + expectedModuleId: 1, + address: null, + }, + { + name: 'Test module 1', + targetShare: 1000, + stakingModuleFee: 2000, + treasuryFee: 200, + expectedModuleId: 2, + address: null, + }, + ] + + before(async () => { + await snapshot() + + stakingModule1 = await StakingModuleMock.new({ from: deployer }) + stakingModule2 = await StakingModuleMock.new({ from: deployer }) + + stakingModulesParams[0].address = stakingModule1.address + stakingModulesParams[1].address = stakingModule2.address + }) + + after(revert) + + it('addStakingModule call is not allowed from stranger', async () => { + await assert.reverts( + router.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { from: stranger } + ), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` + ) + }) + + it('addStakingModule fails on share > 100%', async () => { + await assert.revertsWithCustomError( + router.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + 10001, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { from: appManager } + ), + `ValueOver100Percent("_targetShare")` + ) + }) + + it('addStakingModule fails on fees > 100%', async () => { + await assert.revertsWithCustomError( + router.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + stakingModulesParams[0].targetShare, + 5000, + 5001, + { + from: appManager, + } + ), + `ValueOver100Percent("_stakingModuleFee + _treasuryFee")` + ) + }) + + it('addStakingModule fails on zero address', async () => { + await assert.revertsWithCustomError( + router.addStakingModule( + stakingModulesParams[0].name, + ZERO_ADDRESS, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { + from: appManager, + } + ), + `ZeroAddress("_stakingModuleAddress")` + ) + }) + + it('addStakingModule fails on incorrect module name', async () => { + // check zero length + await assert.revertsWithCustomError( + router.addStakingModule( + '', + stakingModule1.address, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { + from: appManager, + } + ), + `StakingModuleWrongName()` + ) + + // check length > 32 symbols + await assert.revertsWithCustomError( + router.addStakingModule( + '#'.repeat(33), + stakingModule1.address, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { + from: appManager, + } + ), + `StakingModuleWrongName()` + ) + }) + + it('add staking module', async () => { + const tx = await router.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { + from: appManager, + } + ) + assert.equals(tx.logs.length, 3) + await assert.emits(tx, 'StakingModuleAdded', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + stakingModule: stakingModule1.address, + name: stakingModulesParams[0].name, + createdBy: appManager, + }) + await assert.emits(tx, 'StakingModuleTargetShareSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + targetShare: stakingModulesParams[0].targetShare, + setBy: appManager, + }) + await assert.emits(tx, 'StakingModuleFeesSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + stakingModuleFee: stakingModulesParams[0].stakingModuleFee, + treasuryFee: stakingModulesParams[0].treasuryFee, + setBy: appManager, + }) + + assert.equals(await router.getStakingModulesCount(), 1) + assert.equals( + await router.getStakingModuleStatus(stakingModulesParams[0].expectedModuleId), + StakingModuleStatus.Active + ) + assert.equals(await router.getStakingModuleIsStopped(stakingModulesParams[0].expectedModuleId), false) + assert.equals(await router.getStakingModuleIsDepositsPaused(stakingModulesParams[0].expectedModuleId), false) + assert.equals(await router.getStakingModuleIsActive(stakingModulesParams[0].expectedModuleId), true) + + const module = await router.getStakingModule(stakingModulesParams[0].expectedModuleId) + + assert.equals(module.name, stakingModulesParams[0].name) + assert.equals(module.stakingModuleAddress, stakingModule1.address) + assert.equals(module.stakingModuleFee, stakingModulesParams[0].stakingModuleFee) + 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) + }) + + it('add another staking module', async () => { + const tx = await router.addStakingModule( + stakingModulesParams[1].name, + stakingModule2.address, + stakingModulesParams[1].targetShare, + stakingModulesParams[1].stakingModuleFee, + stakingModulesParams[1].treasuryFee, + { + from: appManager, + } + ) + + assert.equals(tx.logs.length, 3) + await assert.emits(tx, 'StakingModuleAdded', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + stakingModule: stakingModule2.address, + name: stakingModulesParams[1].name, + createdBy: appManager, + }) + await assert.emits(tx, 'StakingModuleTargetShareSet', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + targetShare: stakingModulesParams[1].targetShare, + setBy: appManager, + }) + await assert.emits(tx, 'StakingModuleFeesSet', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + stakingModuleFee: stakingModulesParams[1].stakingModuleFee, + treasuryFee: stakingModulesParams[1].treasuryFee, + setBy: appManager, + }) + + assert.equals(await router.getStakingModulesCount(), 2) + assert.equals( + await router.getStakingModuleStatus(stakingModulesParams[1].expectedModuleId), + StakingModuleStatus.Active + ) + assert.equals(await router.getStakingModuleIsStopped(stakingModulesParams[1].expectedModuleId), false) + assert.equals(await router.getStakingModuleIsDepositsPaused(stakingModulesParams[1].expectedModuleId), false) + assert.equals(await router.getStakingModuleIsActive(stakingModulesParams[1].expectedModuleId), true) + + const module = await router.getStakingModule(stakingModulesParams[1].expectedModuleId) + + assert.equals(module.name, stakingModulesParams[1].name) + assert.equals(module.stakingModuleAddress, stakingModule2.address) + assert.equals(module.stakingModuleFee, stakingModulesParams[1].stakingModuleFee) + 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) + }) + + it('get staking modules list', async () => { + const stakingModules = await router.getStakingModules() + + for (let i = 0; i < 2; i++) { + assert.equals(stakingModules[i].name, stakingModulesParams[i].name) + assert.equals(stakingModules[i].stakingModuleAddress, stakingModulesParams[i].address) + assert.equals(stakingModules[i].stakingModuleFee, stakingModulesParams[i].stakingModuleFee) + 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) + } + }) + + it('get staking module ids', async () => { + const stakingModules = await router.getStakingModules() + const stakingModuleIds = await router.getStakingModuleIds() + + for (let i = 0; i < stakingModules.length; i++) { + assert.equals(stakingModules[i].id, stakingModuleIds[i]) + } + }) + + it('update staking module does not allowed without role', async () => { + await assert.reverts( + router.updateStakingModule( + stakingModulesParams[0].expectedModuleId, + stakingModulesParams[0].targetShare + 1, + stakingModulesParams[0].stakingModuleFee + 1, + stakingModulesParams[0].treasuryFee + 1, + { + from: stranger, + } + ), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` + ) + }) + + it('update staking module fails on target share > 100%', async () => { + await assert.revertsWithCustomError( + router.updateStakingModule( + stakingModulesParams[0].expectedModuleId, + 10001, + stakingModulesParams[0].stakingModuleFee + 1, + stakingModulesParams[0].treasuryFee + 1, + { + from: appManager, + } + ), + `ValueOver100Percent("_targetShare")` + ) + }) + + it('update staking module fails on fees > 100%', async () => { + await assert.revertsWithCustomError( + router.updateStakingModule( + stakingModulesParams[0].expectedModuleId, + stakingModulesParams[0].targetShare + 1, + 5000, + 5001, + { + from: appManager, + } + ), + `ValueOver100Percent("_stakingModuleFee + _treasuryFee")` + ) + }) + + it('update staking module', async () => { + const stakingModuleNewParams = { + id: stakingModulesParams[0].expectedModuleId, + targetShare: stakingModulesParams[0].targetShare + 1, + stakingModuleFee: stakingModulesParams[0].stakingModuleFee + 1, + treasuryFee: stakingModulesParams[0].treasuryFee + 1, + } + + const tx = await router.updateStakingModule( + stakingModuleNewParams.id, + stakingModuleNewParams.targetShare, + stakingModuleNewParams.stakingModuleFee, + stakingModuleNewParams.treasuryFee, + { + from: appManager, + } + ) + + assert.equals(tx.logs.length, 2) + + await assert.emits(tx, 'StakingModuleTargetShareSet', { + stakingModuleId: stakingModuleNewParams.id, + targetShare: stakingModuleNewParams.targetShare, + setBy: appManager, + }) + await assert.emits(tx, 'StakingModuleFeesSet', { + stakingModuleId: stakingModuleNewParams.id, + stakingModuleFee: stakingModuleNewParams.stakingModuleFee, + treasuryFee: stakingModuleNewParams.treasuryFee, + setBy: appManager, + }) + }) + + it('set staking module status does not allowed without role', async () => { + await assert.reverts( + router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: stranger, + }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` + ) + }) + + it('set staking module status reverts if status is the same', async () => { + const module = await router.getStakingModule(stakingModulesParams[0].expectedModuleId) + await assert.revertsWithCustomError( + router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, module.status, { + from: appManager, + }), + `StakingModuleStatusTheSame()` + ) + }) + + it('set staking module status', async () => { + const tx = await router.setStakingModuleStatus( + stakingModulesParams[0].expectedModuleId, + StakingModuleStatus.Stopped, + { + from: appManager, + } + ) + + await assert.emits(tx, 'StakingModuleStatusSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + status: StakingModuleStatus.Stopped, + setBy: appManager, + }) + }) + + it('pause staking module does not allowed without role', async () => { + await assert.reverts( + router.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: stranger, + }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_PAUSE_ROLE}` + ) + }) + + it('pause staking module does not allowed at not active staking module', async () => { + await router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { + from: appManager, + }) + + await router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: appManager, + }) + await assert.revertsWithCustomError( + router.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager, + }), + `StakingModuleNotActive()` + ) + await router.setStakingModuleStatus( + stakingModulesParams[0].expectedModuleId, + StakingModuleStatus.DepositsPaused, + { + from: appManager, + } + ) + await assert.revertsWithCustomError( + router.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager, + }), + `StakingModuleNotActive()` + ) + }) + + it('pause staking module', async () => { + await router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { + from: appManager, + }) + const tx = await router.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager, + }) + + await assert.emits(tx, 'StakingModuleStatusSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + status: StakingModuleStatus.DepositsPaused, + setBy: appManager, + }) + }) + + it('deposit fails when module is not active', async () => { + await assert.revertsWithCustomError( + router.deposit(100, stakingModulesParams[0].expectedModuleId, '0x00', { value: ETH(32 * 100), from: lido }), + 'StakingModuleNotActive()' + ) + }) + + it('getDepositsAllocation', async () => { + const keysAllocation = await router.getDepositsAllocation(1000) + + assert.equals(keysAllocation.allocated, 0) + assert.equals(keysAllocation.allocations, [0, 0]) + }) + + it('resume staking module does not allowed without role', async () => { + await assert.reverts( + router.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: stranger, + }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_RESUME_ROLE}` + ) + }) + + it('resume staking module does not allowed at not paused staking module', async () => { + await router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: appManager, + }) + await assert.revertsWithCustomError( + router.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager, + }), + `StakingModuleNotPaused()` + ) + await router.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { + from: appManager, + }) + await assert.revertsWithCustomError( + router.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager, + }), + `StakingModuleNotPaused()` + ) + }) + + it('resume staking module', async () => { + await router.setStakingModuleStatus( + stakingModulesParams[0].expectedModuleId, + StakingModuleStatus.DepositsPaused, + { + from: appManager, + } + ) + const tx = await router.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager, + }) + + await assert.emits(tx, 'StakingModuleStatusSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + status: StakingModuleStatus.Active, + setBy: appManager, + }) + }) + }) + + describe('report rewards minted', async () => { + before(snapshot) + after(revert) + + it('reverts if no REPORT_REWARDS_MINTED_ROLE role', async () => { + const stakingModuleIds = [1, 2] + const totalShares = [300, 400] + + await assert.revertsOZAccessControl( + router.reportRewardsMinted(stakingModuleIds, totalShares, { from: stranger }), + stranger, + 'REPORT_REWARDS_MINTED_ROLE' + ) + }) + + it('reverts if stakingModuleIds and totalShares lengths mismatch', async () => { + const stakingModuleIds = [1, 2, 3] + const totalShares = [300, 400] + + await router.grantRole(await router.REPORT_REWARDS_MINTED_ROLE(), admin, { from: admin }) + await assert.reverts( + router.reportRewardsMinted(stakingModuleIds, totalShares, { from: admin }), + `ArraysLengthMismatch`, + [stakingModuleIds.length, totalShares.length] + ) + }) + + it('reverts if modules are not registered', async () => { + const stakingModuleIds = [1, 2] + const totalShares = [300, 400] + + await router.grantRole(await router.REPORT_REWARDS_MINTED_ROLE(), admin, { from: admin }) + await assert.reverts( + router.reportRewardsMinted(stakingModuleIds, totalShares, { from: admin }), + `StakingModuleUnregistered()` + ) + }) + + it('reverts if modules are not registered', async () => { + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: admin } + ) + await router.addStakingModule( + 'module 2', + module2.address, + 200, // 2 % _targetShare + 5_000, // 50 % _moduleFee + 0, // 0 % _treasuryFee + { from: admin } + ) + + const stakingModuleIds = [1, 2] + const totalShares = [300, 400] + + await router.grantRole(await router.REPORT_REWARDS_MINTED_ROLE(), admin, { from: admin }) + await router.reportRewardsMinted(stakingModuleIds, totalShares, { from: admin }) + + const module1lastcall = await module1.lastCall_onRewardsMinted() + assert.equal(+module1lastcall.callCount, 1) + assert.equal(+module1lastcall.totalShares, 300) + + const module2lastcall = await module2.lastCall_onRewardsMinted() + assert.equal(+module2lastcall.callCount, 1) + assert.equal(+module2lastcall.totalShares, 400) + }) + + it("doesn't call onRewardsMinted() on staking module when its share is equal to zero", async () => { + const stakingModuleIds = [1, 2] + const totalShares = [500, 0] + + await router.reportRewardsMinted(stakingModuleIds, totalShares, { from: admin }) + + const module1lastcall = await module1.lastCall_onRewardsMinted() + assert.equal(+module1lastcall.callCount, 2) + assert.equal(+module1lastcall.totalShares, 800) + + const module2lastcall = await module2.lastCall_onRewardsMinted() + assert.equal(+module2lastcall.callCount, 1) + assert.equal(+module2lastcall.totalShares, 400) + }) + + 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, { + from: admin, + }) + const stakingModuleWithBugId = await router.getStakingModulesCount() + + const stakingModuleIds = [1, 2, stakingModuleWithBugId] + const totalShares = [300, 400, 500] + await router.grantRole(await router.REPORT_REWARDS_MINTED_ROLE(), admin, { from: admin }) + const tx = await router.reportRewardsMinted(stakingModuleIds, totalShares, { from: admin }) + + const errorMethodId = '0x08c379a0' + const errorMessageEncoded = [ + '0000000000000000000000000000000000000000000000000000000000000020', + '000000000000000000000000000000000000000000000000000000000000000f', + '554e48414e444c45445f4552524f520000000000000000000000000000000000', + ] + + assert.emits(tx, 'RewardsMintedReportFailed', { + stakingModuleId: stakingModuleWithBugId, + lowLevelRevertData: [errorMethodId, ...errorMessageEncoded].join(''), + }) + }) + }) + + describe('updateTargetValidatorsLimits()', () => { + before(snapshot) + after(revert) + + it('reverts if no STAKING_MODULE_MANAGE_ROLE role', async () => { + const moduleId = 1 + const nodeOperatorId = 1 + const isTargetLimitActive = true + const targetLimit = 3 + + await assert.revertsOZAccessControl( + router.updateTargetValidatorsLimits(moduleId, nodeOperatorId, isTargetLimitActive, targetLimit, { + from: stranger, + }), + stranger, + 'STAKING_MODULE_MANAGE_ROLE' + ) + }) + + it('reverts if module not register', async () => { + const moduleId = 1 + const nodeOperatorId = 1 + const isTargetLimitActive = true + const targetLimit = 3 + + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await assert.reverts( + router.updateTargetValidatorsLimits(moduleId, nodeOperatorId, isTargetLimitActive, targetLimit, { + from: admin, + }), + 'StakingModuleUnregistered()' + ) + }) + + it('update target validators limits works', async () => { + const moduleId = 1 + const nodeOperatorId = 1 + const isTargetLimitActive = true + const targetLimit = 3 + + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: admin } + ) + + let lastCall = await module1.lastCall_updateTargetValidatorsLimits() + assert.equals(lastCall.nodeOperatorId, 0) + assert.equals(lastCall.isTargetLimitActive, false) + assert.equals(lastCall.targetLimit, 0) + assert.equals(lastCall.callCount, 0) + + await router.updateTargetValidatorsLimits(moduleId, nodeOperatorId, isTargetLimitActive, targetLimit, { + from: admin, + }) + + lastCall = await module1.lastCall_updateTargetValidatorsLimits() + assert.equals(lastCall.nodeOperatorId, 1) + assert.equals(lastCall.isTargetLimitActive, true) + assert.equals(lastCall.targetLimit, targetLimit) + assert.equals(lastCall.callCount, 1) + }) + }) + + describe('updateRefundedValidatorsCount()', async () => { + before(snapshot) + after(revert) + + it('reverts if no STAKING_MODULE_MANAGE_ROLE role', async () => { + const moduleId = 1 + const nodeOperatorId = 1 + const refundedValidatorsCount = 3 + + await assert.revertsOZAccessControl( + router.updateRefundedValidatorsCount(moduleId, nodeOperatorId, refundedValidatorsCount, { from: stranger }), + stranger, + 'STAKING_MODULE_MANAGE_ROLE' + ) + }) + + it('reverts if module not register', async () => { + const moduleId = 1 + const nodeOperatorId = 1 + const refundedValidatorsCount = 3 + + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await assert.reverts( + router.updateRefundedValidatorsCount(moduleId, nodeOperatorId, refundedValidatorsCount, { from: admin }), + 'StakingModuleUnregistered()' + ) + }) + + it('update refunded validators works', async () => { + const moduleId = 1 + const nodeOperatorId = 1 + const refundedValidatorsCount = 3 + + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: admin } + ) + + let lastCall = await module1.lastCall_updateRefundedValidatorsCount() + assert.equal(+lastCall.nodeOperatorId, 0) + assert.equal(+lastCall.refundedValidatorsCount, 0) + assert.equal(+lastCall.callCount, 0) + + await router.updateRefundedValidatorsCount(moduleId, nodeOperatorId, refundedValidatorsCount, { from: admin }) + + lastCall = await module1.lastCall_updateRefundedValidatorsCount() + assert.equal(+lastCall.nodeOperatorId, nodeOperatorId) + assert.equal(+lastCall.refundedValidatorsCount, refundedValidatorsCount) + assert.equal(+lastCall.callCount, 1) + }) + }) + + describe('getStakingModuleSummary()', async () => { + before(snapshot) + after(revert) + + let module1Id + + it('reverts if moduleId does not exists', async () => { + await assert.reverts(router.getStakingModuleSummary(0), 'StakingModuleUnregistered()') + }) + + it('module id summary works', async () => { + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + + await module1.setTotalExitedValidatorsCount(11) + await module1.setActiveValidatorsCount(22) + await module1.setAvailableKeysCount(33) + + const summary = await router.getStakingModuleSummary(module1Id) + assert.equal(summary.totalExitedValidators, 11) + assert.equal(summary.totalDepositedValidators, 22) + assert.equal(summary.depositableValidatorsCount, 33) + }) + }) + + describe('getNodeOperatorSummary()', async () => { + before(snapshot) + after(revert) + + let module1Id + + it('reverts if moduleId does not exists', async () => { + await assert.reverts(router.getNodeOperatorSummary(0, 0), 'StakingModuleUnregistered()') + }) + + it('node operator summary by moduleId works', async () => { + await router.grantRole(await router.STAKING_MODULE_MANAGE_ROLE(), admin, { from: admin }) + await router.addStakingModule( + 'module 1', + module1.address, + 10_000, // 100 % _targetShare + 1_000, // 10 % _moduleFee + 5_000, // 50 % _treasuryFee + { from: admin } + ) + module1Id = +(await router.getStakingModuleIds())[0] + + const summary = { + isTargetLimitActive: true, + targetValidatorsCount: 1, + stuckValidatorsCount: 2, + refundedValidatorsCount: 3, + stuckPenaltyEndTimestamp: 4, + totalExitedValidators: 5, + totalDepositedValidators: 6, + depositableValidatorsCount: 7, + } + const nodeOperatorId = 0 + await module1.setNodeOperatorSummary(nodeOperatorId, summary) + + const moduleSummary = await router.getNodeOperatorSummary(module1Id, nodeOperatorId) + assert.equal(moduleSummary.isTargetLimitActive, true) + assert.equal(moduleSummary.targetValidatorsCount, 1) + assert.equal(moduleSummary.stuckValidatorsCount, 2) + assert.equal(moduleSummary.refundedValidatorsCount, 3) + assert.equal(moduleSummary.stuckPenaltyEndTimestamp, 4) + assert.equal(moduleSummary.totalExitedValidators, 5) + assert.equal(moduleSummary.totalDepositedValidators, 6) + assert.equal(moduleSummary.depositableValidatorsCount, 7) + }) + }) +}) diff --git a/test/0.8.9/utils/access-control-enumerable.test.js b/test/0.8.9/utils/access-control-enumerable.test.js new file mode 100644 index 000000000..84b18dc57 --- /dev/null +++ b/test/0.8.9/utils/access-control-enumerable.test.js @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 +// +// Adopted AccessControl tests from: +// https://github.com/OpenZeppelin/openzeppelin-contracts/tree/dad73159df3d3053c72b5e430fa8164330f18068/test/access +// + +const { makeInterfaceId } = require('@openzeppelin/test-helpers') +const { artifacts, contract, web3, ethers } = require('hardhat') + +const { assert } = require('../../helpers/assert') +const { EvmSnapshot } = require('../../helpers/blockchain') + +const AccessControlEnumerableMock = artifacts.require('AccessControlEnumerableMock.sol') + +const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000' +const ROLE = web3.utils.soliditySha3('ROLE') +const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE') + +const AccessControlInterface = [ + 'hasRole(bytes32,address)', + 'getRoleAdmin(bytes32)', + 'grantRole(bytes32,address)', + 'revokeRole(bytes32,address)', + 'renounceRole(bytes32,address)', +] + +const AccessControlEnumerableInterface = [ + 'hasRole(bytes32,address)', + 'getRoleAdmin(bytes32)', + 'grantRole(bytes32,address)', + 'revokeRole(bytes32,address)', + 'renounceRole(bytes32,address)', +] + +const deployAccessControlEnumerable = async ({ owner }) => { + const ac = await AccessControlEnumerableMock.new({ from: owner }) + return ac +} + +contract('AccessControlEnumerable', ([admin, authorized, other, otherAdmin, otherAuthorized]) => { + let ac + const snapshot = new EvmSnapshot(ethers.provider) + + before('deploy', async () => { + ac = await deployAccessControlEnumerable({ admin }) + await snapshot.make() + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + it('supports interfaces', async () => { + assert(await ac.supportsInterface(makeInterfaceId.ERC165(AccessControlInterface))) + assert(await ac.supportsInterface(makeInterfaceId.ERC165(AccessControlEnumerableInterface))) + }) + + describe('default admin', function () { + it('deployer has default admin role', async function () { + assert(await ac.hasRole(DEFAULT_ADMIN_ROLE, admin)) + }) + + it("other roles's admin is the default admin role", async function () { + assert.equals(await ac.getRoleAdmin(admin), DEFAULT_ADMIN_ROLE) + }) + + it("default admin role's admin is itself", async function () { + assert.equals(await ac.getRoleAdmin(DEFAULT_ADMIN_ROLE), DEFAULT_ADMIN_ROLE) + }) + }) + + describe('granting', function () { + beforeEach(async function () { + await ac.grantRole(ROLE, authorized, { from: admin }) + }) + + it('non-admin cannot grant role to other accounts', async function () { + await assert.reverts( + ac.grantRole(ROLE, authorized, { from: other }), + `AccessControl: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}` + ) + }) + + it('accounts can be granted a role multiple times', async function () { + await ac.grantRole(ROLE, authorized, { from: admin }) + const receipt = await ac.grantRole(ROLE, authorized, { from: admin }) + assert.notEmits(receipt, 'RoleGranted') + }) + }) + + describe('renouncing', function () { + it('roles that are not had can be renounced', async function () { + const receipt = await ac.renounceRole(ROLE, authorized, { from: authorized }) + assert.notEmits(receipt, 'RoleRevoked') + }) + + context('with granted role', function () { + beforeEach(async function () { + await ac.grantRole(ROLE, authorized, { from: admin }) + }) + + it('bearer can renounce role', async function () { + const receipt = await ac.renounceRole(ROLE, authorized, { from: authorized }) + assert.emits(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: authorized }) + + assert(!(await ac.hasRole(ROLE, authorized))) + }) + + it('only the sender can renounce their roles', async function () { + await assert.reverts( + ac.renounceRole(ROLE, authorized, { from: admin }), + 'AccessControl: can only renounce roles for self' + ) + }) + + it('a role can be renounced multiple times', async function () { + await ac.renounceRole(ROLE, authorized, { from: authorized }) + + const receipt = await ac.renounceRole(ROLE, authorized, { from: authorized }) + assert.notEmits(receipt, 'RoleRevoked') + }) + }) + }) + + describe('setting role admin', function () { + beforeEach(async function () { + const receipt = await ac.setRoleAdmin(ROLE, OTHER_ROLE) + assert.emits(receipt, 'RoleAdminChanged', { + role: ROLE, + previousAdminRole: DEFAULT_ADMIN_ROLE, + newAdminRole: OTHER_ROLE, + }) + + await ac.grantRole(OTHER_ROLE, otherAdmin, { from: admin }) + }) + + it("a role's admin role can be changed", async function () { + assert.equals(await ac.getRoleAdmin(ROLE), OTHER_ROLE) + }) + + it('the new admin can grant roles', async function () { + const receipt = await ac.grantRole(ROLE, authorized, { from: otherAdmin }) + assert.emits(receipt, 'RoleGranted', { account: authorized, role: ROLE, sender: otherAdmin }) + }) + + it('the new admin can revoke roles', async function () { + await ac.grantRole(ROLE, authorized, { from: otherAdmin }) + const receipt = await ac.revokeRole(ROLE, authorized, { from: otherAdmin }) + assert.emits(receipt, 'RoleRevoked', { account: authorized, role: ROLE, sender: otherAdmin }) + }) + + it("a role's previous admins no longer grant roles", async function () { + await assert.reverts( + ac.grantRole(ROLE, authorized, { from: admin }), + `AccessControl: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}` + ) + }) + + it("a role's previous admins no longer revoke roles", async function () { + await assert.reverts( + ac.revokeRole(ROLE, authorized, { from: admin }), + `AccessControl: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}` + ) + }) + }) + + describe('onlyRole modifier', function () { + beforeEach(async function () { + await ac.grantRole(ROLE, authorized, { from: admin }) + }) + + it('do not revert if sender has role', async function () { + await ac.senderProtected(ROLE, { from: authorized }) + }) + + it("revert if sender doesn't have role #1", async function () { + await assert.reverts( + ac.senderProtected(ROLE, { from: other }), + `AccessControl: account ${other.toLowerCase()} is missing role ${ROLE}` + ) + }) + + it("revert if sender doesn't have role #2", async function () { + await assert.reverts( + ac.senderProtected(OTHER_ROLE, { from: authorized }), + `AccessControl: account ${authorized.toLowerCase()} is missing role ${OTHER_ROLE}` + ) + }) + }) + + describe('enumerating', function () { + it('role bearers can be enumerated', async function () { + await ac.grantRole(ROLE, authorized, { from: admin }) + await ac.grantRole(ROLE, other, { from: admin }) + await ac.grantRole(ROLE, otherAuthorized, { from: admin }) + await ac.revokeRole(ROLE, other, { from: admin }) + + const memberCount = await ac.getRoleMemberCount(ROLE) + assert.equals(memberCount, 2) + + const bearers = [] + for (let i = 0; i < memberCount; ++i) { + bearers.push(await ac.getRoleMember(ROLE, i)) + } + + assert(bearers.includes(authorized)) + assert(bearers.includes(otherAuthorized)) + }) + it('role enumeration should be in sync after renounceRole call', async function () { + assert.equals(await ac.getRoleMemberCount(ROLE), 0) + await ac.grantRole(ROLE, admin, { from: admin }) + assert.equals(await ac.getRoleMemberCount(ROLE), 1) + await ac.renounceRole(ROLE, admin, { from: admin }) + assert.equals(await ac.getRoleMemberCount(ROLE), 0) + }) + }) +}) diff --git a/test/0.8.9/versioned.test.js b/test/0.8.9/versioned.test.js new file mode 100644 index 000000000..314e891d9 --- /dev/null +++ b/test/0.8.9/versioned.test.js @@ -0,0 +1,92 @@ +const { contract, artifacts } = require('hardhat') +const { assert } = require('../helpers/assert') + +async function deployBehindOssifiableProxy(artifactName, proxyOwner, constructorArgs = []) { + const Contract = await artifacts.require(artifactName) + const implementation = await Contract.new(...constructorArgs, { from: proxyOwner }) + const OssifiableProxy = await artifacts.require('OssifiableProxy') + const proxy = await OssifiableProxy.new(implementation.address, proxyOwner, [], { from: proxyOwner }) + const proxied = await Contract.at(proxy.address) + return { implementation, proxy, proxied } +} + +contract('Versioned', ([admin, proxyOwner, account2, member1, member2]) => { + let versionedImpl + let versionedProxied + const VERSION_INIT = 1 + const VERSION_ZERO = 0 + + before('Deploy', async () => { + const deployed = await deployBehindOssifiableProxy( + 'contracts/0.8.9/test_helpers/VersionedMock.sol:VersionedMock', + proxyOwner, + [] + ) + versionedImpl = deployed.implementation + versionedProxied = deployed.proxied + }) + + describe('raw implementation', async () => { + it('default version is petrified', async () => { + const versionPetrified = await versionedImpl.getPetrifiedVersionMark() + assert.equals(await versionedImpl.getContractVersion(), versionPetrified) + await assert.reverts( + versionedImpl.checkContractVersion(VERSION_ZERO), + `UnexpectedContractVersion(${String(versionPetrified)}, ${VERSION_ZERO})` + ) + }) + + it('reverts if trying to initialize', async () => { + await versionedImpl.getContractVersion() + await assert.reverts(versionedImpl.initializeContractVersionTo(1), 'NonZeroContractVersionOnInit()') + }) + }) + + describe('behind proxy', () => { + it('default version is zero', async () => { + const version = await versionedProxied.getContractVersion() + assert.equals(version, VERSION_ZERO) + await versionedProxied.checkContractVersion(VERSION_ZERO) + await assert.reverts( + versionedProxied.checkContractVersion(VERSION_INIT), + `UnexpectedContractVersion(${VERSION_ZERO}, ${VERSION_INIT})` + ) + }) + + it('initialize sets version and emits event', async () => { + const tx = await versionedProxied.initializeContractVersionTo(VERSION_INIT) + assert.emits(tx, 'ContractVersionSet', { version: VERSION_INIT }) + assert.equals(await versionedProxied.getContractVersion(), VERSION_INIT) + await versionedProxied.checkContractVersion(VERSION_INIT) + await assert.reverts( + versionedProxied.checkContractVersion(VERSION_ZERO), + `UnexpectedContractVersion(${VERSION_INIT}, ${VERSION_ZERO})` + ) + }) + + it('reverts if trying to repeat initialize', async () => { + await assert.reverts(versionedProxied.initializeContractVersionTo(1), 'NonZeroContractVersionOnInit()') + }) + + it('version can be incremented by value 1 at time', async () => { + const prevVersion = +(await versionedProxied.getContractVersion()) + const nextVersion = prevVersion + 1 + const tx = await versionedProxied.updateContractVersion(nextVersion) + assert.emits(tx, 'ContractVersionSet', { version: nextVersion }) + await versionedProxied.checkContractVersion(nextVersion) + await assert.reverts( + versionedProxied.checkContractVersion(prevVersion), + `UnexpectedContractVersion(${nextVersion}, ${prevVersion})` + ) + const newVersion = +(await versionedProxied.getContractVersion()) + assert.equals(newVersion, nextVersion) + }) + + it('reverts if trying to update version with incorrect value', async () => { + const prevVersion = +(await versionedProxied.getContractVersion()) + await assert.reverts(versionedProxied.updateContractVersion(prevVersion - 1), 'InvalidContractVersionIncrement()') + await assert.reverts(versionedProxied.updateContractVersion(prevVersion), 'InvalidContractVersionIncrement()') + await assert.reverts(versionedProxied.updateContractVersion(prevVersion + 2), 'InvalidContractVersionIncrement()') + }) + }) +}) diff --git a/test/0.8.9/withdrawal-queue-deploy.test.js b/test/0.8.9/withdrawal-queue-deploy.test.js new file mode 100644 index 000000000..075d93ed3 --- /dev/null +++ b/test/0.8.9/withdrawal-queue-deploy.test.js @@ -0,0 +1,174 @@ +const { artifacts, contract } = require('hardhat') +const { ZERO_ADDRESS, MAX_UINT256 } = require('../helpers/constants') + +const { ETH, toBN } = require('../helpers/utils') +const withdrawals = require('../helpers/withdrawals') +const { assert } = require('../helpers/assert') + +const StETHMock = artifacts.require('StETHPermitMock.sol') +const WstETH = artifacts.require('WstETHMock.sol') +const EIP712StETH = artifacts.require('EIP712StETH') +const NFTDescriptorMock = artifacts.require('NFTDescriptorMock.sol') + +const QUEUE_NAME = 'Unsteth nft' +const QUEUE_SYMBOL = 'UNSTETH' +const NFT_DESCRIPTOR_BASE_URI = 'https://exampleDescriptor.com/' + +async function deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queuePauser, + queueResumer, + queueFinalizer, + queueOracle, + queueName = QUEUE_NAME, + symbol = QUEUE_SYMBOL, + doResume = true, +}) { + const nftDescriptor = await NFTDescriptorMock.new(NFT_DESCRIPTOR_BASE_URI) + const steth = await StETHMock.new({ value: ETH(1), from: stethOwner }) + const wsteth = await WstETH.new(steth.address, { from: stethOwner }) + const eip712StETH = await EIP712StETH.new(steth.address, { from: stethOwner }) + await steth.initializeEIP712StETH(eip712StETH.address) + + const { queue: withdrawalQueue, impl: withdrawalQueueImplementation } = await withdrawals.deploy( + queueAdmin, + wsteth.address, + queueName, + symbol + ) + + const initTx = await withdrawalQueue.initialize(queueAdmin) + + await withdrawalQueue.grantRole(await withdrawalQueue.FINALIZE_ROLE(), queueFinalizer || steth.address, { + from: queueAdmin, + }) + await withdrawalQueue.grantRole(await withdrawalQueue.PAUSE_ROLE(), queuePauser || queueAdmin, { from: queueAdmin }) + await withdrawalQueue.grantRole(await withdrawalQueue.RESUME_ROLE(), queueResumer || queueAdmin, { from: queueAdmin }) + await withdrawalQueue.grantRole(await withdrawalQueue.ORACLE_ROLE(), queueOracle || steth.address, { + from: queueAdmin, + }) + + if (doResume) { + await withdrawalQueue.resume({ from: queueResumer || queueAdmin }) + } + + return { + initTx, + steth, + wsteth, + withdrawalQueue, + nftDescriptor, + withdrawalQueueImplementation, + } +} + +module.exports = { + deployWithdrawalQueue, + QUEUE_NAME, + QUEUE_SYMBOL, + NFT_DESCRIPTOR_BASE_URI, +} + +contract( + 'WithdrawalQueue', + ([stethOwner, queueAdmin, queuePauser, queueResumer, queueFinalizer, queueBunkerReporter]) => { + context('initialization', () => { + it('is paused right after deploy', async () => { + const { withdrawalQueue } = await deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queuePauser, + queueResumer, + doResume: false, + }) + assert.equals(await withdrawalQueue.isPaused(), true) + }) + + it('bunker mode is disabled by default', async () => { + const { withdrawalQueue } = await deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queuePauser, + queueResumer, + }) + const BUNKER_MODE_DISABLED_TIMESTAMP = await withdrawalQueue.BUNKER_MODE_DISABLED_TIMESTAMP() + const isBunkerModeActive = await withdrawalQueue.isBunkerModeActive() + const bunkerModeSinceTimestamp = await withdrawalQueue.bunkerModeSinceTimestamp() + + assert.equals(isBunkerModeActive, false) + assert.equals(+bunkerModeSinceTimestamp, +BUNKER_MODE_DISABLED_TIMESTAMP) + }) + + it('emits InitializedV1', async () => { + const { initTx } = await deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queuePauser, + queueResumer, + queueFinalizer, + queueBunkerReporter, + }) + assert.emits(initTx, 'InitializedV1', { + _admin: queueAdmin, + }) + }) + + it('initial queue and checkpoint items', async () => { + const { withdrawalQueue } = await deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queuePauser, + queueResumer, + }) + + const queueId = await withdrawalQueue.getLastRequestId() + const queueItem = await withdrawalQueue.getQueueItem(queueId) + + const checkpointIndex = await withdrawalQueue.getLastCheckpointIndex() + const checkpointItem = await withdrawalQueue.getCheckpointItem(checkpointIndex) + + assert.equals(queueItem.cumulativeStETH, 0) + assert.equals(queueItem.cumulativeShares, 0) + assert.equals(queueItem.owner, ZERO_ADDRESS) + assert.equals(queueItem.claimed, true) + + assert.equals(checkpointItem.fromRequestId, 0) + assert.equals(checkpointItem.maxShareRate, 0) + }) + + it('check if pauser is zero', async () => { + await assert.reverts( + deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queueName: '', + }), + 'ZeroMetadata()' + ) + await assert.reverts( + deployWithdrawalQueue({ + stethOwner, + queueAdmin, + symbol: '', + }), + 'ZeroMetadata()' + ) + }) + + it('implementation is petrified', async () => { + const { withdrawalQueueImplementation } = await deployWithdrawalQueue({ + stethOwner, + queueAdmin, + queuePauser, + queueResumer, + doResume: false, + }) + + assert.equals(await withdrawalQueueImplementation.getContractVersion(), toBN(MAX_UINT256)) + + await assert.reverts(withdrawalQueueImplementation.initialize(queueAdmin), 'NonZeroContractVersionOnInit()') + }) + }) + } +) diff --git a/test/0.8.9/withdrawal-queue-nft.test.js b/test/0.8.9/withdrawal-queue-nft.test.js new file mode 100644 index 000000000..552a23c21 --- /dev/null +++ b/test/0.8.9/withdrawal-queue-nft.test.js @@ -0,0 +1,631 @@ +const { contract, ethers, web3, artifacts } = require('hardhat') +const { ZERO_ADDRESS } = require('@aragon/contract-helpers-test') + +const { ETH, StETH, shareRate, shares } = require('../helpers/utils') +const { assert } = require('../helpers/assert') +const { EvmSnapshot, setBalance } = require('../helpers/blockchain') + +const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock') + +const { + deployWithdrawalQueue, + QUEUE_NAME, + QUEUE_SYMBOL, + NFT_DESCRIPTOR_BASE_URI, +} = require('./withdrawal-queue-deploy.test') + +contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, tokenUriManager, recipient]) => { + let withdrawalQueue, steth, nftDescriptor, erc721ReceiverMock + + const manageTokenUriRoleKeccak156 = web3.utils.keccak256('MANAGE_TOKEN_URI_ROLE') + const snapshot = new EvmSnapshot(ethers.provider) + + before('Deploy', async () => { + const deployed = await deployWithdrawalQueue({ + stethOwner: owner, + queueAdmin: daoAgent, + queuePauser: daoAgent, + queueResumer: daoAgent, + queueFinalizer: daoAgent, + }) + + steth = deployed.steth + withdrawalQueue = deployed.withdrawalQueue + nftDescriptor = deployed.nftDescriptor + 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 + await setBalance(steth.address, ETH(600 + 1)) + await steth.mintShares(user, shares(1)) + await steth.approve(withdrawalQueue.address, StETH(300), { from: user }) + await withdrawalQueue.grantRole(manageTokenUriRoleKeccak156, tokenUriManager, { from: daoAgent }) + + await snapshot.make() + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + it('Initial properties', async () => { + assert.equals(await withdrawalQueue.isPaused(), false) + assert.equals(await withdrawalQueue.getLastRequestId(), 0) + assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), 0) + assert.equals(await withdrawalQueue.getLastCheckpointIndex(), 0) + assert.equals(await withdrawalQueue.unfinalizedStETH(), StETH(0)) + assert.equals(await withdrawalQueue.unfinalizedRequestNumber(), 0) + assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(0)) + }) + + context('constructor', function () { + it('should set name and symbol', async function () { + assert.equals(await withdrawalQueue.name(), QUEUE_NAME) + assert.equals(await withdrawalQueue.symbol(), QUEUE_SYMBOL) + }) + }) + + context('supportsInterface', async () => { + it('supports ERC165', async () => { + assert.isTrue(await withdrawalQueue.supportsInterface('0x01ffc9a7')) + }) + + it('supports ERC721', async () => { + assert.isTrue(await withdrawalQueue.supportsInterface('0x80ac58cd')) + }) + + it('supports ERC721Metadata', async () => { + assert.isTrue(await withdrawalQueue.supportsInterface('0x5b5e139f')) + }) + + it('not supports interface not supported', async () => { + assert.isFalse(await withdrawalQueue.supportsInterface('0x12345678')) + }) + }) + + context('name', async () => { + it('returns name', async () => { + assert.equals(await withdrawalQueue.name(), QUEUE_NAME) + }) + }) + + context('symbol', async () => { + it('returns symbol', async () => { + assert.equals(await withdrawalQueue.symbol(), QUEUE_SYMBOL) + }) + }) + + context('tokenURI', async () => { + const requestId = 1 + const baseTokenUri = 'https://example.com/' + + beforeEach(async function () { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + }) + + it('returns tokenURI without nftDescriptor', async () => { + await withdrawalQueue.setBaseURI(baseTokenUri, { from: tokenUriManager }) + assert.equals(await withdrawalQueue.tokenURI(1), `${baseTokenUri}${requestId}`) + }) + + it('returns tokenURI without nftDescriptor and baseUri', async () => { + assert.equals(await withdrawalQueue.tokenURI(1), '') + }) + + it('returns tokenURI with nftDescriptor', async () => { + await withdrawalQueue.setNFTDescriptorAddress(nftDescriptor.address, { from: tokenUriManager }) + + assert.equals(await withdrawalQueue.tokenURI(1), `${NFT_DESCRIPTOR_BASE_URI}${requestId}`) + }) + + it('revert on invalid token id', async () => { + await assert.reverts(withdrawalQueue.tokenURI(0), 'InvalidRequestId(0)') + }) + + it('should set baseURI and return', async () => { + await withdrawalQueue.setBaseURI(baseTokenUri, { from: tokenUriManager }) + assert.equals(await withdrawalQueue.getBaseURI(), baseTokenUri) + }) + + it('should set nftDescriptorAddress and return', async () => { + await withdrawalQueue.setNFTDescriptorAddress(nftDescriptor.address, { from: tokenUriManager }) + assert.equals(await withdrawalQueue.getNFTDescriptorAddress(), nftDescriptor.address) + }) + }) + + context('balanceOf', () => { + it('should return 0 for not existing', async () => { + assert.equals(await withdrawalQueue.balanceOf(stranger), 0) + }) + + it('should return 1 after request', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + }) + + it('should return 2 after request', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 2) + }) + + it('should return 0 after claim', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + const batch = await withdrawalQueue.prefinalize([1], shareRate(1)) + await withdrawalQueue.finalize([1], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + await withdrawalQueue.claimWithdrawal(1, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 0) + }) + + it('should revert with ZeroAddress', async () => { + await assert.reverts(withdrawalQueue.balanceOf(ZERO_ADDRESS), `InvalidOwnerAddress("${ZERO_ADDRESS}")`) + }) + }) + + context('ownerOf', () => { + it('should revert when token id is 0', async () => { + await assert.reverts(withdrawalQueue.ownerOf(0), `InvalidRequestId(0)`) + }) + + it('should revert with not existing', async () => { + await assert.reverts(withdrawalQueue.ownerOf(1), 'InvalidRequestId(1)') + }) + it('should return owner after request', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.ownerOf(1), user) + }) + + it('should revert after claim', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.ownerOf(1), user) + + const batch = await withdrawalQueue.prefinalize([1], shareRate(1)) + await withdrawalQueue.finalize([1], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + await withdrawalQueue.claimWithdrawal(1, { from: user }) + + await assert.reverts(withdrawalQueue.ownerOf(1), 'RequestAlreadyClaimed(1)') + }) + }) + + context('approve()', async () => { + let tokenId1 + beforeEach(async () => { + await snapshot.rollback() + const requestIds = await withdrawalQueue.requestWithdrawals.call([ETH(25), ETH(25)], user, { from: user }) + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + tokenId1 = requestIds[0] + }) + + it('reverts with message "ApprovalToOwner()" when approval for owner address', async () => { + await assert.reverts(withdrawalQueue.approve(user, tokenId1, { from: user }), 'ApprovalToOwner()') + }) + + it('reverts with message "NotOwnerOrApprovedForAll()" when called noy by owner', async () => { + await assert.reverts( + withdrawalQueue.approve(recipient, tokenId1, { from: stranger }), + `NotOwnerOrApprovedForAll("${stranger}")` + ) + }) + + it('sets approval for address and transfer by approved', async () => { + const tx = await withdrawalQueue.approve(recipient, tokenId1, { from: user }) + assert.equal(await withdrawalQueue.getApproved(tokenId1), recipient) + + assert.emits(tx, 'Approval', { owner: user, approved: recipient, tokenId: tokenId1 }) + + await withdrawalQueue.transferFrom(user, recipient, tokenId1, { from: recipient }) + assert.equals(await withdrawalQueue.ownerOf(tokenId1), recipient) + }) + }) + + context('getApproved', () => { + it('should revert with invalid request id', async () => { + await assert.reverts(withdrawalQueue.getApproved(1), 'InvalidRequestId(1)') + }) + + it('should return zero address for not approved', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.getApproved(1), ZERO_ADDRESS) + }) + + it('should return approved address', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25)], user, { from: user }) + await withdrawalQueue.approve(stranger, 1, { from: user }) + assert.equals(await withdrawalQueue.getApproved(1), stranger) + }) + }) + + context('setApprovalForAll()', async () => { + let tokenId1, tokenId2 + beforeEach(async () => { + await snapshot.rollback() + const requestIds = await withdrawalQueue.requestWithdrawals.call([ETH(25), ETH(25)], user, { + from: user, + }) + tokenId1 = requestIds[0] + tokenId2 = requestIds[1] + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + }) + + it('reverts with message "ApproveToCaller()" when owner equal to operator', async () => { + await assert.reverts(withdrawalQueue.setApprovalForAll(user, true, { from: user }), 'ApproveToCaller()') + }) + + it('approvalForAll allows transfer', async () => { + const tx = await withdrawalQueue.setApprovalForAll(recipient, true, { from: user }) + assert.emits(tx, 'ApprovalForAll', { owner: user, operator: recipient, approved: true }) + assert.isTrue(await withdrawalQueue.isApprovedForAll(user, recipient)) + + await withdrawalQueue.transferFrom(user, recipient, tokenId1, { from: recipient }) + await withdrawalQueue.transferFrom(user, recipient, tokenId2, { from: recipient }) + + assert.equals(await withdrawalQueue.ownerOf(tokenId1), recipient) + assert.equals(await withdrawalQueue.ownerOf(tokenId2), recipient) + }) + }) + + context('isApprovedForAll', () => { + it('should return false for not approved', async () => { + assert.isFalse(await withdrawalQueue.isApprovedForAll(user, stranger)) + }) + + it('should return true for approved', async () => { + await withdrawalQueue.setApprovalForAll(stranger, true, { from: user }) + assert.isTrue(await withdrawalQueue.isApprovedForAll(user, stranger)) + }) + }) + + context('safeTransferFrom(address,address,uint256)', async () => { + let requestIds + beforeEach(async () => { + requestIds = await withdrawalQueue.requestWithdrawals.call([ETH(25), ETH(25)], user, { + from: user, + }) + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + }) + + it('reverts with message "NotOwnerOrApproved()" when approvalNotSet and not owner', async () => { + await assert.reverts( + withdrawalQueue.safeTransferFrom(user, recipient, requestIds[0], { + from: stranger, + }), + `NotOwnerOrApproved("${stranger}")` + ) + }) + + it('transfers if called by owner', async () => { + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + await withdrawalQueue.safeTransferFrom(user, recipient, requestIds[0], { + from: user, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + }) + + it('transfers if token approval set', async () => { + await withdrawalQueue.approve(recipient, requestIds[0], { from: user }) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + await withdrawalQueue.safeTransferFrom(user, recipient, requestIds[0], { + from: recipient, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + }) + + it('transfers if operator approval set', async () => { + await withdrawalQueue.setApprovalForAll(recipient, true, { from: user }) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[1]), recipient) + await withdrawalQueue.safeTransferFrom(user, recipient, requestIds[0], { + from: recipient, + }) + await withdrawalQueue.safeTransferFrom(user, recipient, requestIds[1], { + from: recipient, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + assert.equal(await withdrawalQueue.ownerOf(requestIds[1]), recipient) + }) + + it('reverts with message "TransferToNonIERC721Receiver()" when transfer to contract that not implements IERC721Receiver interface', async () => { + await assert.reverts( + withdrawalQueue.safeTransferFrom(user, steth.address, requestIds[0], { + from: user, + }), + `TransferToNonIERC721Receiver("${steth.address}")` + ) + }) + + it('reverts with propagated error message when recipient contract implements ERC721Receiver and reverts on onERC721Received call', async () => { + await erc721ReceiverMock.setDoesAcceptTokens(false, { from: owner }) + await assert.reverts( + withdrawalQueue.safeTransferFrom(user, erc721ReceiverMock.address, requestIds[0], { + from: user, + }), + 'ERC721_NOT_ACCEPT_TOKENS' + ) + }) + + it("doesn't revert when recipient contract implements ERC721Receiver interface and accepts tokens", async () => { + await erc721ReceiverMock.setDoesAcceptTokens(true, { from: owner }) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), erc721ReceiverMock.address) + await withdrawalQueue.safeTransferFrom(user, erc721ReceiverMock.address, requestIds[0], { + from: user, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), erc721ReceiverMock.address) + }) + }) + + describe('transferFrom()', async () => { + let requestIds + + beforeEach(async () => { + requestIds = await withdrawalQueue.requestWithdrawals.call([ETH(25), ETH(25)], user, { + from: user, + }) + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + }) + + it('reverts with message "NotOwnerOrApproved()" when approvalNotSet and not owner', async () => { + await assert.reverts( + withdrawalQueue.transferFrom(user, recipient, requestIds[0], { from: stranger }), + `NotOwnerOrApproved("${stranger}")` + ) + }) + + it('reverts when transfer to the same address', async () => { + await assert.reverts( + withdrawalQueue.transferFrom(user, user, requestIds[0], { + from: user, + }), + 'TransferToThemselves()' + ) + }) + + it('reverts with error "RequestAlreadyClaimed()" when called on claimed request', async () => { + const batch = await withdrawalQueue.prefinalize([2], shareRate(1)) + await withdrawalQueue.finalize([2], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + + await withdrawalQueue.methods['claimWithdrawal(uint256)'](requestIds[0], { + from: user, + }) + + await assert.reverts( + withdrawalQueue.transferFrom(user, recipient, requestIds[0], { + from: user, + }), + `RequestAlreadyClaimed(${requestIds[0]})` + ) + }) + + it('transfers if called by owner', async () => { + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + await withdrawalQueue.transferFrom(user, recipient, requestIds[0], { + from: user, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + }) + + it('transfers if token approval set', async () => { + await withdrawalQueue.approve(recipient, requestIds[0], { from: user }) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + await withdrawalQueue.transferFrom(user, recipient, requestIds[0], { + from: recipient, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + }) + + it('transfers if operator approval set', async () => { + await withdrawalQueue.setApprovalForAll(recipient, true, { from: user }) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + assert.notEqual(await withdrawalQueue.ownerOf(requestIds[1]), recipient) + await withdrawalQueue.transferFrom(user, recipient, requestIds[0], { + from: recipient, + }) + await withdrawalQueue.transferFrom(user, recipient, requestIds[1], { + from: recipient, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + assert.equal(await withdrawalQueue.ownerOf(requestIds[1]), recipient) + }) + + it('can claim request after transfer', async () => { + await withdrawalQueue.transferFrom(user, recipient, requestIds[0], { + from: user, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), recipient) + + const batch = await withdrawalQueue.prefinalize([2], shareRate(1)) + await withdrawalQueue.finalize([2], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + + await withdrawalQueue.methods['claimWithdrawal(uint256)'](requestIds[0], { + from: recipient, + }) + }) + + it("doesn't reverts when transfer to contract that not implements IERC721Receiver interface", async () => { + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), user) + await withdrawalQueue.transferFrom(user, steth.address, requestIds[0], { + from: user, + }) + assert.equal(await withdrawalQueue.ownerOf(requestIds[0]), steth.address) + }) + }) + + context('mint', async () => { + it('should mint', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.tokenURI(1), '') + }) + + it('should mint with tokenURI', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + await withdrawalQueue.setBaseURI('https://example.com/', { from: tokenUriManager }) + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.tokenURI(1), 'https://example.com/1') + }) + + it('should mint with nftDescriptor', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + await withdrawalQueue.setNFTDescriptorAddress(nftDescriptor.address, { from: tokenUriManager }) + nftDescriptor.setBaseTokenURI('https://nftDescriptor.com/') + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.tokenURI(1), 'https://nftDescriptor.com/1') + }) + + it('should mint more after request', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 4) + assert.equals(await withdrawalQueue.ownerOf(3), user) + assert.equals(await withdrawalQueue.ownerOf(4), user) + }) + }) + + context('burn', async () => { + it('should burn', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + const batch = await withdrawalQueue.prefinalize.call([1], shareRate(1)) + await withdrawalQueue.finalize([1], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + await withdrawalQueue.claimWithdrawal(1, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.ownerOf(2), user) + await assert.reverts(withdrawalQueue.ownerOf(1), 'RequestAlreadyClaimed(1)') + }) + + it('revert on claim not owner', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + const batch = await withdrawalQueue.prefinalize.call([1], shareRate(1)) + await withdrawalQueue.finalize([1], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + + await assert.reverts(withdrawalQueue.claimWithdrawal(1, { from: stranger }), `NotOwner("${stranger}", "${user}")`) + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + }) + + it('revert on claim not existing', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + + await assert.reverts(withdrawalQueue.claimWithdrawal(1, { from: user }), 'RequestNotFoundOrNotFinalized(1)') + }) + + it('should burn more after request', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + const batch = await withdrawalQueue.prefinalize.call([2], shareRate(1)) + await withdrawalQueue.finalize([2], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + await withdrawalQueue.claimWithdrawal(1, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.ownerOf(2), user) + await assert.reverts(withdrawalQueue.ownerOf(1), 'RequestAlreadyClaimed(1)') + + await withdrawalQueue.claimWithdrawal(2, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 0) + }) + + it('should burn after transfer', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + await withdrawalQueue.transferFrom(user, stranger, 1, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.ownerOf(2), user) + assert.equals(await withdrawalQueue.ownerOf(1), stranger) + + const batch = await withdrawalQueue.prefinalize.call([2], shareRate(1)) + await withdrawalQueue.finalize([2], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + await withdrawalQueue.claimWithdrawal(2, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 0) + }) + + it('should revert on transfer himself', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + await assert.reverts(withdrawalQueue.transferFrom(user, user, 1, { from: user }), 'TransferToThemselves()') + }) + + it('should revert on transfer not owner', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + + await assert.reverts( + withdrawalQueue.transferFrom(user, stranger, 1, { from: stranger }), + `NotOwnerOrApproved("${stranger}")` + ) + }) + + it('should burn after approve and transfer ', async () => { + await withdrawalQueue.requestWithdrawals([ETH(25), ETH(25), ETH(25)], user, { from: user }) + assert.equals(await withdrawalQueue.balanceOf(user), 3) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + assert.equals(await withdrawalQueue.ownerOf(3), user) + + await withdrawalQueue.approve(stranger, 2, { from: user }) + await withdrawalQueue.approve(stranger, 3, { from: user }) + + assert.equals(await withdrawalQueue.balanceOf(user), 3) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + assert.equals(await withdrawalQueue.ownerOf(3), user) + + await withdrawalQueue.transferFrom(user, stranger, 3, { from: stranger }) + + assert.equals(await withdrawalQueue.balanceOf(user), 2) + assert.equals(await withdrawalQueue.balanceOf(stranger), 1) + assert.equals(await withdrawalQueue.ownerOf(1), user) + assert.equals(await withdrawalQueue.ownerOf(2), user) + assert.equals(await withdrawalQueue.ownerOf(3), stranger) + + const batch = await withdrawalQueue.prefinalize.call([3], shareRate(1)) + await withdrawalQueue.finalize([3], shareRate(1), { from: daoAgent, value: batch.ethToLock }) + await withdrawalQueue.claimWithdrawal(1, { from: user }) + await withdrawalQueue.claimWithdrawal(3, { from: stranger }) + + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(stranger), 0) + + await assert.reverts(withdrawalQueue.claimWithdrawal(2, { from: stranger }), `NotOwner("${stranger}", "${user}")`) + + assert.equals(await withdrawalQueue.balanceOf(user), 1) + }) + }) +}) diff --git a/test/0.8.9/withdrawal-queue-requests-finalization.test.js b/test/0.8.9/withdrawal-queue-requests-finalization.test.js new file mode 100644 index 000000000..a350d1085 --- /dev/null +++ b/test/0.8.9/withdrawal-queue-requests-finalization.test.js @@ -0,0 +1,372 @@ +const { contract, ethers } = require('hardhat') +const { itParam } = require('mocha-param') + +const { StETH, shareRate, e18, e27, toBN } = require('../helpers/utils') +const { assert } = require('../helpers/assert') +const { MAX_UINT256 } = require('../helpers/constants') +const { EvmSnapshot } = require('../helpers/blockchain') + +const { deployWithdrawalQueue } = require('./withdrawal-queue-deploy.test') + +contract('WithdrawalQueue', ([owner, daoAgent, user, anotherUser]) => { + let withdrawalQueue, steth + + const snapshot = new EvmSnapshot(ethers.provider) + + let rebaseCounter = 0 + const setShareRate = async (rate) => { + const totalShares = await steth.getTotalShares() + await withdrawalQueue.onOracleReport(false, rebaseCounter, ++rebaseCounter, { from: daoAgent }) + await steth.setTotalPooledEther(totalShares.mul(toBN(e18(rate))).div(toBN(e18(1)))) + } + + const finalizeRequests = async ({ finalizationShareRate, maxTimeStamp, budget, expectedBatches }) => { + const calculatedBatches = await withdrawalQueue.calculateFinalizationBatches( + finalizationShareRate, + maxTimeStamp, + 1000, + [budget, false, Array(36).fill(0), 0] + ) + + assert.isTrue(calculatedBatches.finished) + assert.equalsDelta(calculatedBatches.remainingEthBudget, 0, 2) + const batches = calculatedBatches.batches.slice(0, calculatedBatches.batchesLength) + assert.equals(batches, expectedBatches) + + const batch = await withdrawalQueue.prefinalize(batches, finalizationShareRate) + + assert.equalsDelta(batch.ethToLock, budget, 2) + + await withdrawalQueue.finalize(batches, finalizationShareRate, { + from: daoAgent, + value: batch.ethToLock, + }) + + return { batch } + } + + before('Deploy', async () => { + const deployed = await deployWithdrawalQueue({ + stethOwner: owner, + queueAdmin: daoAgent, + queuePauser: daoAgent, + queueResumer: daoAgent, + queueFinalizer: daoAgent, + queueOracle: daoAgent, + }) + + steth = deployed.steth + withdrawalQueue = deployed.withdrawalQueue + + await steth.mintShares(user, e18(10)) + await steth.approve(withdrawalQueue.address, StETH(10), { from: user }) + await steth.mintShares(anotherUser, e18(10)) + await steth.approve(withdrawalQueue.address, StETH(10), { from: anotherUser }) + + await setShareRate(1) + await snapshot.make() + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + context('1 request', () => { + itParam('same rate ', [0.25, 0.5, 1], async (postFinalizationRate) => { + const finalizationShareRate = shareRate(1) + const userRequestAmount = e18(1) + + await withdrawalQueue.requestWithdrawals([userRequestAmount], user, { from: user }) + assert.equals(await withdrawalQueue.unfinalizedStETH(), userRequestAmount) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + await finalizeRequests({ + finalizationShareRate, + maxTimeStamp: MAX_UINT256, + budget: userRequestAmount, + expectedBatches: [1], + }) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), 0) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + await setShareRate(postFinalizationRate) + + const userBalanceBefore = await ethers.provider.getBalance(user) + // gasPrice:0 hack for coverage + await withdrawalQueue.claimWithdrawal(1, { from: user, gasPrice: 0 }) + assert.equals(await ethers.provider.getBalance(user), userBalanceBefore.add(userRequestAmount)) + + assert.equals(await ethers.provider.getBalance(withdrawalQueue.address), 0) + }) + + itParam('finalization rate is lower', [0.25, 0.5, 1, 2], async (postFinalizationRate) => { + const finalizationShareRate = shareRate(0.5) + const userRequestAmount = e18(1) + const budget = toBN(userRequestAmount).div(toBN(2)).toString() + + await withdrawalQueue.requestWithdrawals([userRequestAmount], user, { from: user }) + assert.equals(await withdrawalQueue.unfinalizedStETH(), userRequestAmount) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + await setShareRate(0.5) + + await finalizeRequests({ + finalizationShareRate, + maxTimeStamp: MAX_UINT256, + budget, + expectedBatches: [1], + }) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), 0) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + await setShareRate(postFinalizationRate) + + const userBalanceBefore = await ethers.provider.getBalance(user) + await withdrawalQueue.claimWithdrawal(1, { from: user, gasPrice: 0 }) + assert.equals(await ethers.provider.getBalance(user), userBalanceBefore.add(e18(0.5))) + + assert.equals(await ethers.provider.getBalance(withdrawalQueue.address), 0) + }) + + itParam('finalization rate is higher', [0.25, 0.5, 1, 2, 4], async (postFinalizationRate) => { + const finalizationShareRate = shareRate(2) + const userRequestAmount = e18(1) + + await withdrawalQueue.requestWithdrawals([userRequestAmount], user, { from: user }) + assert.equals(await withdrawalQueue.unfinalizedStETH(), userRequestAmount) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + await setShareRate(2) + + await finalizeRequests({ + finalizationShareRate, + maxTimeStamp: MAX_UINT256, + budget: userRequestAmount, + expectedBatches: [1], + }) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), 0) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + + await setShareRate(postFinalizationRate) + + const userBalanceBefore = await ethers.provider.getBalance(user) + await withdrawalQueue.claimWithdrawal(1, { from: user, gasPrice: 0 }) + assert.equals(await ethers.provider.getBalance(user), userBalanceBefore.add(e18(1))) + + assert.equals(await ethers.provider.getBalance(withdrawalQueue.address), 0) + }) + }) + + context('2 users, 1 batch', () => { + ;[0.7].forEach(async (firstRequestRate) => { + ;[0.4, 0.7, 1].forEach(async (secondRequestRate) => { + ;[firstRequestRate, secondRequestRate, secondRequestRate - 0.1, secondRequestRate + 0.1].forEach( + async (finalizationRate) => { + ;[ + firstRequestRate, + firstRequestRate - 0.1, + firstRequestRate + 0.1, + secondRequestRate, + finalizationRate, + finalizationRate - 0.1, + finalizationRate + 0.1, + ].forEach(async (postFinalizationRate) => { + it(`rates: first request = ${firstRequestRate}, second request = ${secondRequestRate}, finalization = ${finalizationRate}, claim = ${postFinalizationRate}`, async () => { + await setShareRate(firstRequestRate) + const userRequestAmount = e18(1) + await withdrawalQueue.requestWithdrawals([userRequestAmount], user, { from: user }) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), userRequestAmount) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(anotherUser), 0) + + await setShareRate(secondRequestRate) + const anotherUserRequestAmount = e18(2) + await withdrawalQueue.requestWithdrawals([anotherUserRequestAmount], anotherUser, { + from: anotherUser, + }) + + const userExpectedEthAmount = + finalizationRate >= firstRequestRate + ? toBN(userRequestAmount) + : toBN(userRequestAmount) + .mul(toBN(e27(finalizationRate))) + .div(toBN(e27(firstRequestRate))) + + const anotherUserExpectedEthAmount = + finalizationRate >= secondRequestRate + ? toBN(anotherUserRequestAmount) + : toBN(anotherUserRequestAmount) + .mul(toBN(e27(finalizationRate))) + .div(toBN(e27(secondRequestRate))) + + const totalRequestedAmount = userExpectedEthAmount.add(anotherUserExpectedEthAmount) + + const stETHRequested = toBN(userRequestAmount).add(toBN(anotherUserRequestAmount)) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), stETHRequested) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(anotherUser), 1) + + await setShareRate(finalizationRate) + + let expectedBatches = + (firstRequestRate <= finalizationRate && secondRequestRate <= finalizationRate) || + (firstRequestRate > finalizationRate && secondRequestRate > finalizationRate) + ? [2] + : [1, 2] + + // handling math accuracy + if (firstRequestRate === finalizationRate && secondRequestRate > finalizationRate) { + expectedBatches = [2] + } else if (firstRequestRate === finalizationRate && secondRequestRate < finalizationRate) { + expectedBatches = [1, 2] + } + + await finalizeRequests({ + finalizationShareRate: shareRate(finalizationRate), + maxTimeStamp: MAX_UINT256, + budget: totalRequestedAmount, + expectedBatches, + }) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), 0) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(anotherUser), 1) + + await setShareRate(postFinalizationRate) + + const userBalanceBefore = await ethers.provider.getBalance(user) + await withdrawalQueue.claimWithdrawal(1, { from: user, gasPrice: 0 }) + assert.equalsDelta( + await ethers.provider.getBalance(user), + toBN(userBalanceBefore).add(userExpectedEthAmount), + 1 + ) + + const anotherUserBalanceBefore = await ethers.provider.getBalance(anotherUser) + await withdrawalQueue.claimWithdrawal(2, { from: anotherUser, gasPrice: 0 }) + assert.equalsDelta( + await ethers.provider.getBalance(anotherUser), + toBN(anotherUserBalanceBefore).add(anotherUserExpectedEthAmount), + 1 + ) + + assert.equalsDelta(await ethers.provider.getBalance(withdrawalQueue.address), 0, 2) + }) + }) + } + ) + }) + }) + }) + + context('2 users, 2 batch', () => { + ;[0.7].forEach(async (firstRequestRate) => { + ;[0.4, 0.7, 1].forEach(async (secondRequestRate) => { + ;[firstRequestRate, secondRequestRate, secondRequestRate - 0.1, secondRequestRate + 0.1].forEach( + async (firstFinalizationRate) => { + ;[firstRequestRate, secondRequestRate, secondRequestRate - 0.1, secondRequestRate + 0.1].forEach( + async (secondFinalizationRate) => { + ;[ + firstRequestRate, + firstRequestRate - 0.1, + firstRequestRate + 0.1, + secondRequestRate, + firstFinalizationRate, + firstFinalizationRate - 0.1, + firstFinalizationRate + 0.1, + secondFinalizationRate, + ].forEach(async (postFinalizationRate) => { + it(`rates: first request = ${firstRequestRate}, second request = ${secondRequestRate}, secondFinalizationRate = ${secondFinalizationRate}, firstFinalization = ${firstFinalizationRate}, claim = ${postFinalizationRate}`, async () => { + await setShareRate(firstRequestRate) + const userRequestAmount = e18(1) + await withdrawalQueue.requestWithdrawals([userRequestAmount], user, { from: user }) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), userRequestAmount) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(anotherUser), 0) + + await setShareRate(secondRequestRate) + const anotherUserRequestAmount = e18(2) + await withdrawalQueue.requestWithdrawals([anotherUserRequestAmount], anotherUser, { + from: anotherUser, + }) + + assert.equals( + await withdrawalQueue.unfinalizedStETH(), + toBN(userRequestAmount).add(toBN(anotherUserRequestAmount)) + ) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(anotherUser), 1) + + const userExpectedEthAmount = + firstFinalizationRate >= firstRequestRate + ? toBN(userRequestAmount) + : toBN(userRequestAmount) + .mul(toBN(e27(firstFinalizationRate))) + .div(toBN(e27(firstRequestRate))) + + const anotherUserExpectedEthAmount = + secondFinalizationRate >= secondRequestRate + ? toBN(anotherUserRequestAmount) + : toBN(anotherUserRequestAmount) + .mul(toBN(e27(secondFinalizationRate))) + .div(toBN(e27(secondRequestRate))) + + const stETHRequested = toBN(userRequestAmount).add(toBN(anotherUserRequestAmount)) + + assert.equals(await withdrawalQueue.unfinalizedStETH(), stETHRequested) + assert.equals(await withdrawalQueue.balanceOf(user), 1) + assert.equals(await withdrawalQueue.balanceOf(anotherUser), 1) + + await setShareRate(firstFinalizationRate) + + await finalizeRequests({ + finalizationShareRate: shareRate(firstFinalizationRate), + maxTimeStamp: MAX_UINT256, + budget: userExpectedEthAmount, + expectedBatches: [1], + }) + + await setShareRate(secondFinalizationRate) + + await finalizeRequests({ + finalizationShareRate: shareRate(secondFinalizationRate), + maxTimeStamp: MAX_UINT256, + budget: anotherUserExpectedEthAmount, + expectedBatches: [2], + }) + + await setShareRate(postFinalizationRate) + + const userBalanceBefore = await ethers.provider.getBalance(user) + await withdrawalQueue.claimWithdrawal(1, { from: user, gasPrice: 0 }) + assert.equalsDelta( + await ethers.provider.getBalance(user), + toBN(userBalanceBefore).add(userExpectedEthAmount), + 1 + ) + + const anotherUserBalanceBefore = await ethers.provider.getBalance(anotherUser) + await withdrawalQueue.claimWithdrawal(2, { from: anotherUser, gasPrice: 0 }) + assert.equalsDelta( + await ethers.provider.getBalance(anotherUser), + toBN(anotherUserBalanceBefore).add(anotherUserExpectedEthAmount), + 1 + ) + + assert.equalsDelta(await ethers.provider.getBalance(withdrawalQueue.address), 0, 2) + }) + }) + } + ) + } + ) + }) + }) + }) +}) diff --git a/test/0.8.9/withdrawal-queue-share-rate-changes.test.js b/test/0.8.9/withdrawal-queue-share-rate-changes.test.js new file mode 100644 index 000000000..606c64e07 --- /dev/null +++ b/test/0.8.9/withdrawal-queue-share-rate-changes.test.js @@ -0,0 +1,187 @@ +const { contract, ethers } = require('hardhat') + +const { assert } = require('../helpers/assert') +const { e18, e27, toBN, getFirstEventArgs } = require('../helpers/utils') +const { MAX_UINT256 } = require('../helpers/constants') +const { EvmSnapshot } = require('../helpers/blockchain') + +const { deployWithdrawalQueue } = require('./withdrawal-queue-deploy.test') + +contract('WithdrawalQueue', ([owner, daoAgent, finalizer, user, oracle]) => { + const evmSnapshot = new EvmSnapshot(ethers.provider) + const snapshot = () => evmSnapshot.make() + const rollback = () => evmSnapshot.rollback() + + const TOTAL_SHARES = toBN(e18(10)) + + let queue, steth + + let rebaseCounter = 0 + const setShareRate = async (rate) => { + await queue.onOracleReport(false, rebaseCounter, ++rebaseCounter, { from: oracle }) + await steth.setTotalPooledEther(TOTAL_SHARES.mul(toBN(rate))) + } + + before('deploy', async () => { + const deployed = await deployWithdrawalQueue({ + stethOwner: owner, + queueAdmin: daoAgent, + queueFinalizer: finalizer, + queueOracle: oracle, + }) + + steth = deployed.steth + queue = deployed.withdrawalQueue + + const userShares = toBN(TOTAL_SHARES).sub(toBN(await steth.getTotalShares())) + assert.bnAbove(userShares, 0) + + await steth.mintShares(user, userShares) + await setShareRate(1) + + await steth.approve(queue.address, MAX_UINT256, { from: user }) + + await snapshot() + }) + + context(`2 requests with diff share rate, maxShareRate == shareRate(1)`, async () => { + /// + /// invariant 1: all requests in the same batch should be finalized using the same share rate + /// + /// invariant 2: a withdrawal request cannot be finalized using a lower share rate than the + /// minimum share rate that was reported by the oracle since the last oracle report before + /// the request was added to the queue + /// + after(rollback) + + const requestIds = [0, 0] + + it(`share rate 1.0: a user requests a withdrawal of 1 stETH (10**18 shares)`, async () => { + const tx = await queue.requestWithdrawals([e18(1)], user, { from: user }) + requestIds[0] = +getFirstEventArgs(tx, 'WithdrawalRequested').requestId + assert.equals(await queue.unfinalizedStETH(), e18(1)) + }) + + it(`protocol receives rewards, changing share rate to 2.0`, async () => { + await setShareRate(2) + }) + + it(`share rate 2.0: a user requests a withdrawal of 2 stETH (10**18 shares)`, async () => { + const tx = await queue.requestWithdrawals([e18(2)], user, { from: user }) + requestIds[1] = +getFirstEventArgs(tx, 'WithdrawalRequested').requestId + assert.equals(await queue.unfinalizedStETH(), e18(3)) + }) + + it(`protocol receives slashing, changing share rate to 1.0`, async () => { + await setShareRate(1) + }) + + let batches + + it(`both requests can be finalized with 2 ETH`, async () => { + const result = await queue.calculateFinalizationBatches(e27(1), MAX_UINT256, 1000, [ + e18(2), + false, + Array(36).fill(0), + 0, + ]) + assert.isTrue(result.finished) + assert.equals(result.batchesLength, 2) + batches = result.batches.slice(0, result.batchesLength) + assert.equals(batches, [1, 2]) + + const batch = await queue.prefinalize.call(batches, e27(1)) + assert.equals(batch.ethToLock, e18(2)) + assert.equals(batch.sharesToBurn, e18(2)) + }) + + let claimableEther + + it(`requests get finalized`, async () => { + await queue.finalize(batches, e27(1), { from: finalizer, value: e18(2) }) + assert.equals(await queue.getLastFinalizedRequestId(), requestIds[1]) + + const hints = await queue.findCheckpointHints(requestIds, 1, await queue.getLastCheckpointIndex()) + claimableEther = await queue.getClaimableEther(requestIds, hints) + }) + + it(`first request is fullfilled with 1 ETH`, async () => { + assert.isClose(claimableEther[0], e18(1), 10) + }) + + it(`second request is fullfilled with 1 ETH`, async () => { + assert.isClose(claimableEther[1], e18(1), 10) + }) + }) + + context(`2 requests 2 batches, shareRate(2) > maxShareRate > shareRate(1)`, async () => { + /// + /// invariant 1: all requests in the same batch should be finalized using the same share rate + /// + /// invariant 2: a withdrawal request cannot be finalized using a lower share rate than the + /// minimum share rate that was reported by the oracle since the last oracle report before + /// the request was added to the queue + /// + after(rollback) + + const requestIds = [0, 0] + + it(`share rate 1.0: a user requests a withdrawal of 1 stETH (10**18 shares)`, async () => { + const tx = await queue.requestWithdrawals([e18(1)], user, { from: user }) + requestIds[0] = +getFirstEventArgs(tx, 'WithdrawalRequested').requestId + assert.equals(await queue.unfinalizedStETH(), e18(1)) + }) + + it(`protocol receives rewards, changing share rate to 2.0`, async () => { + await setShareRate(2) + }) + + it(`share rate 2.0: a user requests a withdrawal of 2 stETH (10**18 shares)`, async () => { + const tx = await queue.requestWithdrawals([e18(2)], user, { from: user }) + requestIds[1] = +getFirstEventArgs(tx, 'WithdrawalRequested').requestId + assert.equals(await queue.unfinalizedStETH(), e18(3)) + }) + + it(`protocol receives slashing, changing share rate to 1.0`, async () => { + await setShareRate(1) + }) + + let batches + const maxShareRate = e27(1.5) + + it(`both requests can be finalized with 2 ETH`, async () => { + const result = await queue.calculateFinalizationBatches(maxShareRate, MAX_UINT256, 1000, [ + e18(2.5), + false, + Array(36).fill(0), + 0, + ]) + assert.isTrue(result.finished) + assert.equals(result.batchesLength, 2) + batches = result.batches.slice(0, result.batchesLength) + assert.equals(batches, [1, 2]) + + const batch = await queue.prefinalize.call(batches, maxShareRate) + assert.equals(batch.ethToLock, e18(2.5)) + assert.equals(batch.sharesToBurn, e18(2)) + }) + + let claimableEther + + it(`requests get finalized`, async () => { + await queue.finalize(batches, maxShareRate, { from: finalizer, value: e18(2.5) }) + assert.equals(await queue.getLastFinalizedRequestId(), requestIds[1]) + + const hints = await queue.findCheckpointHints(requestIds, 1, await queue.getLastCheckpointIndex()) + claimableEther = await queue.getClaimableEther(requestIds, hints) + }) + + it(`first request is fullfilled with 1 ETH`, async () => { + assert.isClose(claimableEther[0], e18(1), 10) + }) + + it(`second request is fullfilled with 1 ETH`, async () => { + assert.isClose(claimableEther[1], e18(1.5), 10) + }) + }) +}) diff --git a/test/0.8.9/withdrawal-queue.test.js b/test/0.8.9/withdrawal-queue.test.js index d0e3bd04d..62fd8dd9b 100644 --- a/test/0.8.9/withdrawal-queue.test.js +++ b/test/0.8.9/withdrawal-queue.test.js @@ -1,32 +1,38 @@ -const { artifacts, contract, ethers, web3 } = require('hardhat') +const { contract, ethers, web3 } = require('hardhat') const { bn, getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test') const { ETH, StETH, shareRate, shares } = require('../helpers/utils') const { assert } = require('../helpers/assert') -const withdrawals = require('../helpers/withdrawals') +const { MAX_UINT256, ACCOUNTS_AND_KEYS } = require('../helpers/constants') const { signPermit, makeDomainSeparator } = require('../0.6.12/helpers/permit_helpers') -const { MAX_UINT256, ACCOUNTS_AND_KEYS } = require('../0.6.12/helpers/constants') -const { impersonate, EvmSnapshot, setBalance } = require('../helpers/blockchain') +const { impersonate, EvmSnapshot, getCurrentBlockTimestamp, setBalance } = require('../helpers/blockchain') -const StETHMock = artifacts.require('StETHMock.sol') -const WstETH = artifacts.require('WstETHMock.sol') +const { deployWithdrawalQueue } = require('./withdrawal-queue-deploy.test') -contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { +contract('WithdrawalQueue', ([owner, stranger, daoAgent, user, pauser, resumer, oracle]) => { let withdrawalQueue, steth, wsteth const snapshot = new EvmSnapshot(ethers.provider) - before('Deploy', async () => { - steth = await StETHMock.new({ value: ETH(1) }) - wsteth = await WstETH.new(steth.address) + const currentRate = async () => + bn(await steth.getTotalPooledEther()) + .mul(bn(10).pow(bn(27))) + .div(await steth.getTotalShares()) - withdrawalQueue = (await withdrawals.deploy(daoAgent, wsteth.address)).queue + before('Deploy', async () => { + const deployed = await deployWithdrawalQueue({ + stethOwner: owner, + queueAdmin: daoAgent, + queuePauser: daoAgent, + queueResumer: daoAgent, + }) - await withdrawalQueue.initialize(daoAgent, daoAgent, daoAgent, steth.address, steth.address) - await withdrawalQueue.resume({ from: daoAgent }) + steth = deployed.steth + wsteth = deployed.wsteth + withdrawalQueue = deployed.withdrawalQueue 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 }) @@ -49,6 +55,137 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(0)) }) + context('Pause/Resume', async () => { + 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 }) + await withdrawalQueue.grantRole(RESUME_ROLE, resumer, { from: daoAgent }) + await withdrawalQueue.pauseFor(100000000, { from: pauser }) + assert(await withdrawalQueue.isPaused()) + await withdrawalQueue.resume({ from: resumer }) + assert(!(await withdrawalQueue.isPaused())) + await assert.revertsOZAccessControl(withdrawalQueue.pauseFor(100000000, { from: resumer }), resumer, 'PAUSE_ROLE') + await assert.revertsOZAccessControl( + withdrawalQueue.pauseFor(100000000, { from: stranger }), + stranger, + 'PAUSE_ROLE' + ) + await withdrawalQueue.pauseFor(100000000, { from: pauser }) + await assert.revertsOZAccessControl(withdrawalQueue.resume({ from: pauser }), pauser, 'RESUME_ROLE') + await assert.revertsOZAccessControl(withdrawalQueue.resume({ from: stranger }), stranger, 'RESUME_ROLE') + }) + + it('withdraw/finalize only allowed when at resumed state', async () => { + await withdrawalQueue.pauseFor(100000000, { from: daoAgent }) + assert(await withdrawalQueue.isPaused()) + await assert.reverts(withdrawalQueue.requestWithdrawals([ETH(1)], owner, { from: user }), 'ResumedExpected()') + + await assert.reverts( + withdrawalQueue.requestWithdrawalsWstETH([ETH(1)], owner, { from: user }), + 'ResumedExpected()' + ) + + const [alice] = ACCOUNTS_AND_KEYS + const amount = ETH(1) + const deadline = MAX_UINT256 + await setBalance(alice, ETH(10)) + await impersonate(ethers.provider, alice.address) + const stETHDomainSeparator = await steth.DOMAIN_SEPARATOR() + const wstETHDomainSeparator = await wsteth.DOMAIN_SEPARATOR() + + let { v, r, s } = signPermit( + alice.address, + withdrawalQueue.address, + amount, // amount + 0, // nonce + deadline, + wstETHDomainSeparator, + alice.key + ) + + const wstETHPermission = { + value: amount, + deadline, // deadline + v, + r, + s, + } + + await assert.reverts( + withdrawalQueue.requestWithdrawalsWstETHWithPermit([ETH(1)], owner, wstETHPermission, { from: alice.address }), + 'ResumedExpected()' + ) + ;({ v, r, s } = signPermit( + alice.address, + withdrawalQueue.address, + amount, // amount + 0, // nonce + deadline, + stETHDomainSeparator, + alice.key + )) + + const stETHPermission = { + value: amount, + deadline, // deadline + v, + r, + s, + } + + await assert.reverts( + withdrawalQueue.requestWithdrawalsWithPermit([ETH(1)], owner, stETHPermission, { from: alice.address }), + 'ResumedExpected()' + ) + await assert.reverts(withdrawalQueue.finalize([1], 0, { from: owner }), 'ResumedExpected()') + }) + + it('cant resume if not paused', async () => { + await assert.reverts(withdrawalQueue.resume(), 'PausedExpected()') + }) + }) + + context('BunkerMode', async () => { + it('init config', async () => { + assert(!(await withdrawalQueue.isBunkerModeActive())) + assert.equals(ethers.constants.MaxUint256, await withdrawalQueue.bunkerModeSinceTimestamp()) + }) + + it('access control', async () => { + assert(!(await withdrawalQueue.isBunkerModeActive())) + const ORACLE_ROLE = await withdrawalQueue.ORACLE_ROLE() + await withdrawalQueue.grantRole(ORACLE_ROLE, oracle, { from: daoAgent }) + await assert.revertsOZAccessControl( + withdrawalQueue.onOracleReport(true, 0, 0, { from: stranger }), + stranger, + 'ORACLE_ROLE' + ) + await withdrawalQueue.onOracleReport(true, 0, 0, { from: oracle }) + }) + + it('state and events', async () => { + assert(!(await withdrawalQueue.isBunkerModeActive())) + assert.equals(ethers.constants.MaxUint256, await withdrawalQueue.bunkerModeSinceTimestamp()) + let timestamp = await getCurrentBlockTimestamp() + await assert.reverts( + withdrawalQueue.onOracleReport(true, +timestamp + 1000000, +timestamp + 1100000, { from: steth.address }), + 'InvalidReportTimestamp()' + ) + // enable + timestamp = await getCurrentBlockTimestamp() + const tx1 = await withdrawalQueue.onOracleReport(true, timestamp, timestamp, { from: steth.address }) + assert.emits(tx1, 'BunkerModeEnabled', { _sinceTimestamp: timestamp }) + assert(await withdrawalQueue.isBunkerModeActive()) + assert.equals(timestamp, await withdrawalQueue.bunkerModeSinceTimestamp()) + // disable + timestamp = await getCurrentBlockTimestamp() + const tx2 = await withdrawalQueue.onOracleReport(false, timestamp, timestamp, { from: steth.address }) + assert.emits(tx2, 'BunkerModeDisabled') + assert(!(await withdrawalQueue.isBunkerModeActive())) + assert.equals(ethers.constants.MaxUint256, await withdrawalQueue.bunkerModeSinceTimestamp()) + }) + }) + context('Request', async () => { it('One can request a withdrawal', async () => { const receipt = await withdrawalQueue.requestWithdrawals([StETH(300)], owner, { from: user }) @@ -162,7 +299,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { it('One cant request more than they have', async () => { await assert.reverts( withdrawalQueue.requestWithdrawals([StETH(400)], owner, { from: user }), - 'TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE' + 'ALLOWANCE_EXCEEDED' ) }) @@ -171,8 +308,48 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await assert.reverts( withdrawalQueue.requestWithdrawals([StETH(300)], owner, { from: user }), - 'TRANSFER_AMOUNT_EXCEEDS_ALLOWANCE' + 'ALLOWANCE_EXCEEDED' + ) + }) + + it('One cant request while is paused', async () => { + const PAUSE_INFINITELY = await withdrawalQueue.PAUSE_INFINITELY() + await withdrawalQueue.pauseFor(PAUSE_INFINITELY, { from: daoAgent }) + await assert.reverts(withdrawalQueue.requestWithdrawals([StETH(300)], owner, { from: user }), 'ResumedExpected()') + await assert.reverts( + withdrawalQueue.requestWithdrawalsWstETH([ETH(300)], owner, { from: user }), + 'ResumedExpected()' + ) + }) + + it('data is being accumulated properly', async () => { + const queueItemStep0 = await withdrawalQueue.getQueueItem(await withdrawalQueue.getLastRequestId()) + + const amountStep1 = StETH(50) + const sharesStep1 = await steth.getSharesByPooledEth(amountStep1) + await withdrawalQueue.requestWithdrawals([amountStep1], owner, { from: user }) + const queueItemStep1 = await withdrawalQueue.getQueueItem(await withdrawalQueue.getLastRequestId()) + + assert.equals(+queueItemStep1.cumulativeStETH, +amountStep1 + +queueItemStep0.cumulativeStETH) + assert.equals(+queueItemStep1.cumulativeShares, +sharesStep1 + +queueItemStep0.cumulativeShares) + assert.equals(queueItemStep1.owner, owner) + assert.equals(queueItemStep1.claimed, false) + + const amountStep2 = StETH(100) + const sharesStep2 = await steth.getSharesByPooledEth(amountStep2) + await withdrawalQueue.requestWithdrawals([amountStep2], owner, { from: user }) + const queueItemStep2 = await withdrawalQueue.getQueueItem(await withdrawalQueue.getLastRequestId()) + + assert.equals( + +queueItemStep2.cumulativeStETH, + +amountStep2 + +queueItemStep1.cumulativeStETH + +queueItemStep0.cumulativeStETH + ) + assert.equals( + +queueItemStep2.cumulativeShares, + +sharesStep2 + +queueItemStep1.cumulativeShares + +queueItemStep0.cumulativeShares ) + assert.equals(queueItemStep2.owner, owner) + assert.equals(queueItemStep2.claimed, false) }) }) @@ -183,19 +360,13 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) }) - it('Calculate one request batch', async () => { - const batch = await withdrawalQueue.finalizationBatch(1, shareRate(300)) - - assert.equals(batch.ethToLock, ETH(300)) - assert.equals(batch.sharesToBurn, shares(1)) - }) - it('Finalizer can finalize a request', async () => { - await assert.reverts( - withdrawalQueue.finalize(1, { from: stranger }), - `AccessControl: account ${stranger.toLowerCase()} is missing role ${await withdrawalQueue.FINALIZE_ROLE()}` + await assert.revertsOZAccessControl( + withdrawalQueue.finalize([1], 0, { from: stranger }), + stranger, + 'FINALIZE_ROLE' ) - await withdrawalQueue.finalize(1, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], 1, { from: steth.address, value: amount }) assert.equals(await withdrawalQueue.getLockedEtherAmount(), amount) assert.equals( @@ -205,7 +376,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) it('One can finalize requests with discount', async () => { - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(150) }) + await withdrawalQueue.finalize([1], shareRate(150), { from: steth.address, value: ETH(150) }) assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(150)) assert.equals( @@ -214,28 +385,14 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { ) }) - it('Same discounts is squashed into one', async () => { - await steth.setTotalPooledEther(ETH(900)) - await steth.mintShares(user, shares(1)) - await steth.approve(withdrawalQueue.address, StETH(300), { from: user }) - - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(10) }) - assert.equals(await withdrawalQueue.getLastCheckpointIndex(), 1) - - await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - await withdrawalQueue.finalize(2, { from: steth.address, value: ETH(10) }) - - assert.equals(await withdrawalQueue.getLastCheckpointIndex(), 1) - }) - it('One can finalize a batch of requests at once', async () => { await steth.setTotalPooledEther(ETH(900)) await steth.mintShares(user, shares(1)) await steth.approve(withdrawalQueue.address, StETH(300), { from: user }) await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - const batch = await withdrawalQueue.finalizationBatch(2, shareRate(300)) - await withdrawalQueue.finalize(2, { from: steth.address, value: batch.ethToLock }) + const batch = await withdrawalQueue.prefinalize.call([2], shareRate(300)) + await withdrawalQueue.finalize([2], shareRate(300), { from: steth.address, value: batch.ethToLock }) assert.equals(batch.sharesToBurn, shares(2)) assert.equals(await withdrawalQueue.getLastRequestId(), 2) @@ -254,7 +411,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - await withdrawalQueue.finalize(1, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) assert.equals(await withdrawalQueue.getLastRequestId(), 2) assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), 1) @@ -264,7 +421,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await ethers.provider.getBalance(withdrawalQueue.address) ) - await withdrawalQueue.finalize(2, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([2], shareRate(300), { from: steth.address, value: amount }) assert.equals(await withdrawalQueue.getLastRequestId(), 2) assert.equals(await withdrawalQueue.getLastFinalizedRequestId(), 2) @@ -274,6 +431,43 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await ethers.provider.getBalance(withdrawalQueue.address) ) }) + + it('batch reverts if share rate is zero', async () => { + await assert.reverts(withdrawalQueue.prefinalize([1], shareRate(0)), 'ZeroShareRate()') + }) + + 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 }), + `InvalidRequestId(${idAhead})` + ) + + await assert.reverts(withdrawalQueue.prefinalize([idAhead], shareRate(300)), `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 assert.reverts( + withdrawalQueue.finalize([id], shareRate(300), { from: steth.address, value: amount }), + `InvalidRequestId(${id})` + ) + + await assert.reverts(withdrawalQueue.prefinalize([id], shareRate(300)), `InvalidRequestId(${id})`) + }) + + it('reverts if given amount to finalize exceeds requested', async () => { + const id = +(await withdrawalQueue.getLastRequestId()) + const amountExceeded = bn(ETH(400)) + + await assert.reverts( + withdrawalQueue.finalize([id], shareRate(300), { from: steth.address, value: amountExceeded }), + `TooMuchEtherToFinalize(${+amountExceeded}, ${+amount})` + ) + }) }) context('getClaimableEth()', () => { @@ -282,10 +476,18 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) it('works', async () => { - await withdrawalQueue.requestWithdrawals([ETH(1)], owner, { from: user }) - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(1) }) + await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: ETH(1) }) + + assert.almostEqual(await withdrawalQueue.getClaimableEther([1], [1]), ETH(1), 100) + }) + + it('reverts if last hint checkpoint is ahead of requestId', async () => { + await withdrawalQueue.finalize([1], shareRate(0.5), { from: steth.address, value: ETH(0.5) }) - assert.equals(await withdrawalQueue.getClaimableEther([1], [1]), ETH(1)) + await withdrawalQueue.requestWithdrawals([ETH(2)], owner, { from: user }) + await withdrawalQueue.finalize([2], shareRate(0.5), { from: steth.address, value: ETH(0.5) }) + + await assert.reverts(withdrawalQueue.getClaimableEther([1], [2]), 'InvalidHint(2)') }) it('return 0 for non-finalized request', async () => { @@ -294,7 +496,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) it('return 0 for claimed request', async () => { - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(1) }) + await withdrawalQueue.finalize([1], shareRate(1), { from: steth.address, value: ETH(1) }) await withdrawalQueue.claimWithdrawals([1], [1], { from: owner }) assert.equals(await withdrawalQueue.getClaimableEther([1], [1]), ETH(0)) @@ -305,12 +507,18 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await assert.reverts(withdrawalQueue.getClaimableEther([0], [1]), 'InvalidRequestId(0)') await assert.reverts(withdrawalQueue.getClaimableEther([2], [1]), 'InvalidRequestId(2)') - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(1) }) + await withdrawalQueue.finalize([1], shareRate(1), { from: steth.address, value: ETH(1) }) await assert.reverts(withdrawalQueue.getClaimableEther([1], [2]), 'InvalidHint(2)') await assert.reverts(withdrawalQueue.getClaimableEther([1], [0]), 'InvalidHint(0)') await withdrawalQueue.requestWithdrawals([ETH(1)], owner, { from: user }) await assert.reverts(withdrawalQueue.getClaimableEther([1], [2]), 'InvalidHint(2)') + + await withdrawalQueue.requestWithdrawals([ETH(1), ETH(1)], owner, { from: user }) + await withdrawalQueue.finalize([2], shareRate(0.99), { from: steth.address, value: ETH(0.99) }) + await withdrawalQueue.finalize([3], shareRate(0.98), { from: steth.address, value: ETH(0.98) }) + + await assert.reverts(withdrawalQueue.getClaimableEther([3], [1]), 'InvalidHint(1)') }) }) @@ -321,7 +529,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) it('Owner can claim a finalized request to recipient address', async () => { - await withdrawalQueue.finalize(1, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) const balanceBefore = bn(await ethers.provider.getBalance(user)) @@ -330,8 +538,35 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { assert.equals(await ethers.provider.getBalance(user), balanceBefore.add(bn(amount))) }) + context('claimWithdrawalsTo', () => { + it('reverts for zero recipient', async () => { + await assert.reverts( + withdrawalQueue.claimWithdrawalsTo([1], [1], ZERO_ADDRESS, { from: owner }), + 'ZeroRecipient()' + ) + }) + + it('reverts with zero _requestId', async () => { + await assert.reverts(withdrawalQueue.claimWithdrawalsTo([0], [1], user, { from: owner }), 'InvalidRequestId(0)') + }) + + it('reverts if sender is not owner', async () => { + await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await assert.reverts( + withdrawalQueue.claimWithdrawalsTo([1], [1], owner, { from: stranger }), + `NotOwner("${stranger}", "${owner}")` + ) + }) + + it('reverts if there is not enough balance', async () => { + await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) + await setBalance(withdrawalQueue.address, ETH(200)) + await assert.reverts(withdrawalQueue.claimWithdrawalsTo([1], [1], owner, { from: owner }), 'NotEnoughEther()') + }) + }) + it('Owner can claim a finalized request without hint', async () => { - await withdrawalQueue.finalize(1, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], shareRate(300), { from: steth.address, value: amount }) const balanceBefore = bn(await ethers.provider.getBalance(owner)) @@ -359,20 +594,20 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - await withdrawalQueue.finalize(2, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([2], shareRate(300), { 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, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([1], shareRate(300), { 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, { from: steth.address, value: ETH(150) }) + await withdrawalQueue.finalize([1], shareRate(150), { from: steth.address, value: ETH(150) }) const balanceBefore = bn(await ethers.provider.getBalance(owner)) assert.equals(await withdrawalQueue.getLockedEtherAmount(), ETH(150)) @@ -388,14 +623,17 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await steth.setTotalPooledEther(ETH(22)) await steth.mintShares(user, shares(21)) await steth.approve(withdrawalQueue.address, StETH(21), { from: user }) - assert.equals(await withdrawalQueue.getLastCheckpointIndex(), 0) - await withdrawalQueue.finalize(1, { from: steth.address, value: amount }) - + const batch = await withdrawalQueue.prefinalize([1], shareRate(1)) + await withdrawalQueue.finalize([1], shareRate(1), { from: steth.address, value: batch.ethToLock }) for (let i = 1; i <= 20; i++) { assert.equals(await withdrawalQueue.getLastCheckpointIndex(), i) await withdrawalQueue.requestWithdrawals([StETH(1)], ZERO_ADDRESS, { from: user }) - await withdrawalQueue.finalize(i + 1, { from: steth.address, value: bn(ETH(1)).sub(bn(i * 1000)) }) + const batch = await withdrawalQueue.prefinalize([i + 1], shareRate(i + 1)) + await withdrawalQueue.finalize([i + 1], shareRate(i + 1), { + from: steth.address, + value: batch.ethToLock, + }) } assert.equals(await withdrawalQueue.getLastCheckpointIndex(), 21) @@ -410,181 +648,165 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) }) - context('findLastFinalizableRequestIdByTimestamp()', async () => { - const numOfRequests = 10 + context('claim scenarios', async () => { + 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 () => { - for (let i = 1; i <= numOfRequests; i++) { - await withdrawalQueue.requestWithdrawals([ETH(20)], owner, { from: user }) - } + await snapshot.rollback() + await withdrawalQueue.requestWithdrawals(requestsAmounts, user, { from: user }) + requestIds = await withdrawalQueue.getWithdrawalRequests(user, { from: user }) }) - it('works', async () => { - for (let i = 1; i <= numOfRequests; i++) { - const timestamp = (await withdrawalQueue.getWithdrawalStatus([i]))[0].timestamp - assert.equals(await withdrawalQueue.findLastFinalizableRequestIdByTimestamp(timestamp, 1, 10), i) + it('direct', async () => { + 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) }) } + const balanceAfter = bn(await ethers.provider.getBalance(user)) + assert.equals(balanceAfter, balanceBefore.add(bn(total))) }) - it('returns zero on empty range', async () => { - assert.equals(await withdrawalQueue.findLastFinalizableRequestIdByTimestamp(1, 2, 1), 0) - }) - - it('return zero if no unfinalized request found', async () => { - const timestamp = (await withdrawalQueue.getWithdrawalStatus([1]))[0].timestamp - - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH[10] }) - assert.equals(await withdrawalQueue.findLastFinalizableRequestIdByTimestamp(timestamp, 2, 10), 0) - }) - - it('checks params', async () => { - await assert.reverts(withdrawalQueue.findLastFinalizableRequestIdByTimestamp(0, 0, 10), 'ZeroTimestamp()') - - const timestamp = (await withdrawalQueue.getWithdrawalStatus([2]))[0].timestamp - - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByTimestamp(timestamp, 0, 10), - 'InvalidRequestIdRange(0, 10)' - ) - - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByTimestamp(timestamp, 0, 11), - 'InvalidRequestIdRange(0, 11)' - ) - - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(20) }) - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByTimestamp(timestamp, 1, 10), - 'InvalidRequestIdRange(1, 10)' - ) - }) - }) - - context('findLastFinalizableRequestIdByBudget()', async () => { - const numOfRequests = 10 - - beforeEach(async () => { - for (let i = 1; i <= numOfRequests; i++) { - await withdrawalQueue.requestWithdrawals([ETH(20)], owner, { from: user }) + it('reverse', async () => { + 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) }) } + const balanceAfter = bn(await ethers.provider.getBalance(user)) + assert.equals(balanceAfter, balanceBefore.add(bn(total))) }) - it('works', async () => { - // 1e18 shares is 300e18 ether, let's discount to 150 - const rate = shareRate(150) - - for (let i = 1; i <= numOfRequests; i++) { - const budget = ETH(i * 10 + 5) - assert.equals(await withdrawalQueue.findLastFinalizableRequestIdByBudget(budget, rate, 1, 10), i) + it('random', async () => { + 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) }) } + const balanceAfter = bn(await ethers.provider.getBalance(user)) + assert.equals(balanceAfter, balanceBefore.add(bn(total))) }) - it('return zero if no unfinalized request found', async () => { - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH[10] }) - assert.equals(await withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(1), shareRate(300), 2, 10), 0) - }) - - it('returns zero on empty range', async () => { - assert.equals(await withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(1), shareRate(300), 2, 1), 0) + it('different rates', async () => { + const balanceBefore = bn(await ethers.provider.getBalance(user)) + const totalDistributedEth = bn(0) + for (let index = 0; index < requestIds.length; index++) { + const requestId = requestIds[index] + const batch = await withdrawalQueue.prefinalize([requestId], shareRate(300 / (index + 1))) + await withdrawalQueue.finalize([requestId], shareRate(300 / (index + 1)), { + from: steth.address, + value: batch.ethToLock, + }) + 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 }) + } + const balanceAfter = bn(await ethers.provider.getBalance(user)) + assert.equals(balanceAfter, balanceBefore.add(totalDistributedEth)) }) + }) - it('checks params', async () => { - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(0), shareRate(300), 0, 10), - 'ZeroAmountOfETH()' - ) - - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(1), shareRate(0), 0, 10), - 'ZeroShareRate()' - ) + 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 }) - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(1), shareRate(300), 0, 10), - 'InvalidRequestIdRange(0, 10)' - ) + const id = await withdrawalQueue.getLastRequestId() + await withdrawalQueue.finalize([id], shareRate(1), { from: steth.address, value: finalizedWEI }) - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(1), shareRate(300), 0, 11), - 'InvalidRequestIdRange(0, 11)' + const hints = await withdrawalQueue.findCheckpointHints( + requestIds, + 1, + await withdrawalQueue.getLastCheckpointIndex() ) - await withdrawalQueue.finalize(1, { from: steth.address, value: ETH(20) }) - await assert.reverts( - withdrawalQueue.findLastFinalizableRequestIdByBudget(ETH(1), shareRate(300), 1, 10), - 'InvalidRequestIdRange(1, 10)' - ) - }) - }) + // 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))) + ) + }) - context('findLastFinalizableRequestId()', async () => { - const numOfRequests = 10 + 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`) - beforeEach(async () => { - for (let i = 1; i <= numOfRequests + 1; i++) { - await withdrawalQueue.requestWithdrawals([ETH(20)], owner, { from: user }) - } - }) + const balanceBefore = bn(await ethers.provider.getBalance(user)) + await withdrawalQueue.claimWithdrawals(requestIds, hints, { from: user }) + const balanceAfter = bn(await ethers.provider.getBalance(user)) + assert.equals(balanceBefore.addn(finalizedWEI), balanceAfter, `Total Claimed doesn't add up to finalized amount`) + } - it('works', async () => { - for (let i = 1; i <= numOfRequests; i++) { - const budget = ETH(i * 10 + 5) - const timestamp = (await withdrawalQueue.getWithdrawalStatus([i]))[0].timestamp - assert.equals(await withdrawalQueue.findLastFinalizableRequestId(budget, shareRate(150), timestamp), i) - } + it('distribute&claim 10wei per 100*100WEI requests ', async () => { + await fuzzClaim(100, 100, 1000) }) - it('returns zero if no unfinalized requests', async () => { - await withdrawalQueue.finalize(10, { from: steth.address, value: ETH[10] }) - - const timestamp = (await withdrawalQueue.getWithdrawalStatus([10]))[0].timestamp - assert.equals(await withdrawalQueue.findLastFinalizableRequestId(ETH(100), shareRate(100), timestamp), 0) + it('distribute&claim 1wei per 100*100WEI requests', async () => { + await fuzzClaim(100, 100, 100) }) - it('checks params', async () => { - await assert.reverts(withdrawalQueue.findLastFinalizableRequestId(ETH(0), shareRate(300), 1), 'ZeroAmountOfETH()') - - await assert.reverts(withdrawalQueue.findLastFinalizableRequestId(ETH(1), shareRate(0), 1), 'ZeroShareRate()') - - await assert.reverts(withdrawalQueue.findLastFinalizableRequestId(ETH(1), shareRate(1), 0), 'ZeroTimestamp()') + 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~ { + context('findCheckpointHints()', 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)) beforeEach(async () => { await withdrawalQueue.requestWithdrawals(requests, owner, { from: user }) for (let i = 1; i <= numOfRequests; i++) { - await withdrawalQueue.finalize(i, { from: steth.address, value: discountedPrices[i] }) + await withdrawalQueue.finalize([i], discountShareRates[i - 1], { + from: steth.address, + value: discountedPrices[i - 1], + }) } assert.equals(await withdrawalQueue.getLastCheckpointIndex(), numOfRequests) - assert.equals( - await withdrawalQueue.findCheckpointHintsUnbounded([await withdrawalQueue.getLastFinalizedRequestId()]), - await withdrawalQueue.getLastCheckpointIndex() - ) - }) - - it('works unbounded', async () => { - assert.equals( - await withdrawalQueue.findCheckpointHintsUnbounded([10]), - await withdrawalQueue.getLastCheckpointIndex() - ) }) it('reverts if request is not finalized', async () => { await withdrawalQueue.requestWithdrawals([ETH(1)], owner, { from: user }) await assert.reverts(withdrawalQueue.findCheckpointHints([11], 1, 10), 'RequestNotFoundOrNotFinalized(11)') - await assert.reverts(withdrawalQueue.findCheckpointHintsUnbounded([11]), 'RequestNotFoundOrNotFinalized(11)') }) it('reverts if there is no such a request', async () => { await assert.reverts(withdrawalQueue.findCheckpointHints([12], 1, 10), 'RequestNotFoundOrNotFinalized(12)') - await assert.reverts(withdrawalQueue.findCheckpointHintsUnbounded([12]), 'RequestNotFoundOrNotFinalized(12)') }) it('range search (found)', async () => { @@ -626,7 +848,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { } }) - context('findCheckpointHints()', () => { + context('findCheckpointHints() 2', () => { let requestId const amount = ETH(20) @@ -635,15 +857,49 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { requestId = await withdrawalQueue.getLastRequestId() }) + it('reverts if requestId is zero', async () => { + const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() + await assert.reverts(withdrawalQueue.findCheckpointHints([0], 1, lastCheckpointIndex), 'InvalidRequestId(0)') + }) + + it('reverts if first index is zero', async () => { + const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() + await assert.reverts( + withdrawalQueue.findCheckpointHints([1], 0, lastCheckpointIndex), + `InvalidRequestIdRange(0, ${+lastCheckpointIndex})` + ) + }) + + it('reverts if last index is larger than in store', async () => { + const lastCheckpointWrong = (await withdrawalQueue.getLastCheckpointIndex()) + 1 + await assert.reverts( + withdrawalQueue.findCheckpointHints([1], 1, lastCheckpointWrong), + `InvalidRequestIdRange(1, ${+lastCheckpointWrong})` + ) + }) + it('returns empty list when passed empty request ids list', async () => { const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() const hints = await withdrawalQueue.findCheckpointHints([], 1, lastCheckpointIndex) assert.equal(hints.length, 0) }) + 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 lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() + const hints = await withdrawalQueue.findCheckpointHints( + [requestId], + +lastCheckpointIndex + 1, + lastCheckpointIndex + ) + assert.equal(hints.length, 1) + assert.equals(hints[0], 0) + }) + it('returns hints array with one item for list from single request id', async () => { - const batch = await withdrawalQueue.finalizationBatch(requestId, shareRate(300)) - await withdrawalQueue.finalize(requestId, { from: steth.address, value: batch.ethToLock }) + const batch = await withdrawalQueue.prefinalize.call([requestId], shareRate(300)) + await withdrawalQueue.finalize([requestId], shareRate(300), { from: steth.address, value: batch.ethToLock }) const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() const hints = await withdrawalQueue.findCheckpointHints([requestId], 1, lastCheckpointIndex) assert.equal(hints.length, 1) @@ -651,7 +907,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) it('returns correct hints array for given request ids', async () => { - await withdrawalQueue.finalize(requestId, { from: steth.address, value: ETH(20) }) + await withdrawalQueue.finalize([requestId], shareRate(20), { from: steth.address, value: ETH(20) }) await steth.mintShares(owner, shares(1)) await steth.approve(withdrawalQueue.address, StETH(300), { from: owner }) @@ -664,7 +920,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await withdrawalQueue.requestWithdrawals([thirdRequestAmount], user, { from: user }) const thirdRequestId = await withdrawalQueue.getLastRequestId() - await withdrawalQueue.finalize(thirdRequestId, { from: steth.address, value: ETH(40) }) + await withdrawalQueue.finalize([thirdRequestId], shareRate(20), { from: steth.address, value: ETH(40) }) const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() const hints = await withdrawalQueue.findCheckpointHints( @@ -674,12 +930,12 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { ) assert.equal(hints.length, 3) assert.equals(hints[0], 1) - assert.equals(hints[1], 1) - assert.equals(hints[2], 1) + assert.equals(hints[1], 2) + assert.equals(hints[2], 2) }) it('reverts with RequestIdsNotSorted error when request ids not in ascending order', async () => { - await withdrawalQueue.finalize(requestId, { from: steth.address, value: ETH(20) }) + await withdrawalQueue.finalize([requestId], shareRate(20), { from: steth.address, value: ETH(20) }) await steth.mintShares(owner, shares(1)) await steth.approve(withdrawalQueue.address, StETH(300), { from: owner }) @@ -692,7 +948,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { await withdrawalQueue.requestWithdrawals([thirdRequestAmount], user, { from: user }) const thirdRequestId = await withdrawalQueue.getLastRequestId() - await withdrawalQueue.finalize(thirdRequestId, { from: steth.address, value: ETH(40) }) + await withdrawalQueue.finalize([thirdRequestId], shareRate(20), { from: steth.address, value: ETH(40) }) const lastCheckpointIndex = await withdrawalQueue.getLastCheckpointIndex() await assert.reverts( @@ -702,39 +958,6 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) }) - context('findCheckpointHintsUnbounded()', () => { - let requestId - const amount = ETH(20) - - beforeEach('Enqueue a request', async () => { - await withdrawalQueue.requestWithdrawals([amount], owner, { from: user }) - requestId = await withdrawalQueue.getLastRequestId() - }) - - it('returns correct hints array for given request ids', async () => { - await withdrawalQueue.finalize(requestId, { from: steth.address, value: ETH(20) }) - - await steth.mintShares(owner, shares(1)) - 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() - - const thirdRequestAmount = ETH(30) - await withdrawalQueue.requestWithdrawals([thirdRequestAmount], user, { from: user }) - const thirdRequestId = await withdrawalQueue.getLastRequestId() - - await withdrawalQueue.finalize(thirdRequestId, { from: steth.address, value: ETH(40) }) - - const hints = await withdrawalQueue.findCheckpointHintsUnbounded([requestId, secondRequestId, thirdRequestId]) - assert.equal(hints.length, 3) - assert.equals(hints[0], 1) - assert.equals(hints[1], 1) - assert.equals(hints[2], 1) - }) - }) - context('claimWithdrawals()', () => { const amount = ETH(20) @@ -749,10 +972,10 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { const secondRequestAmount = ETH(10) await withdrawalQueue.requestWithdrawals([secondRequestAmount], owner, { from: owner }) const secondRequestId = await withdrawalQueue.getLastRequestId() - await withdrawalQueue.finalize(secondRequestId, { from: steth.address, value: ETH(30) }) + 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 }) + 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) }) @@ -790,6 +1013,23 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { const wstETHBalanceAfter = await wsteth.balanceOf(user) assert.equals(wstETHBalanceAfter, wstETHBalanceBefore.sub(bn(requests[0])).sub(bn(requests[1]))) }) + + it('uses sender address as owner if zero passed', async () => { + await wsteth.mint(user, ETH(1)) + await steth.mintShares(wsteth.address, shares(1)) + await steth.mintShares(user, shares(1)) + await wsteth.approve(withdrawalQueue.address, ETH(1), { from: user }) + + const tx = await withdrawalQueue.requestWithdrawalsWstETH([ETH(1)], ZERO_ADDRESS, { from: user }) + + assert.emits(tx, 'WithdrawalRequested', { + requestId: 1, + requestor: user.toLowerCase(), + owner: user.toLowerCase(), + amountOfStETH: await steth.getPooledEthByShares(ETH(1)), + amountOfShares: shares(1), + }) + }) }) context('requestWithdrawalsWstETHWithPermit()', () => { @@ -840,6 +1080,44 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) }) + context('requestWithdrawalsWithPermit()', () => { + const [alice] = ACCOUNTS_AND_KEYS + it('works correctly with non empty payload', async () => { + await web3.eth.sendTransaction({ to: alice.address, from: user, value: ETH(1) }) + await steth.mintShares(alice.address, shares(100)) + const withdrawalRequestsCount = 5 + const requests = Array(withdrawalRequestsCount).fill(ETH(10)) + + const amount = bn(ETH(10)).mul(bn(withdrawalRequestsCount)) + const deadline = MAX_UINT256 + await impersonate(ethers.provider, alice.address) + const domainSeparator = await steth.DOMAIN_SEPARATOR() + const { v, r, s } = signPermit( + alice.address, + withdrawalQueue.address, + amount, // amount + 0, // nonce + deadline, + domainSeparator, + alice.key + ) + const permission = [ + amount, + deadline, // deadline + v, + r, + s, + ] + + const aliceBalancesBefore = await steth.balanceOf(alice.address) + const lastRequestIdBefore = await withdrawalQueue.getLastRequestId() + await withdrawalQueue.requestWithdrawalsWithPermit(requests, owner, permission, { from: alice.address }) + assert.equals(await withdrawalQueue.getLastRequestId(), lastRequestIdBefore.add(bn(requests.length))) + const aliceBalancesAfter = await steth.balanceOf(alice.address) + assert.equals(aliceBalancesAfter, aliceBalancesBefore.sub(bn(ETH(10)).mul(bn(withdrawalRequestsCount)))) + }) + }) + context('Transfer request', async () => { const amount = ETH(300) let requestId @@ -884,7 +1162,7 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { }) it("One can't change claimed request", async () => { - await withdrawalQueue.finalize(requestId, { from: steth.address, value: amount }) + await withdrawalQueue.finalize([requestId], shareRate(300), { from: steth.address, value: amount }) await withdrawalQueue.claimWithdrawal(requestId, { from: user }) await assert.reverts( @@ -924,4 +1202,15 @@ contract('WithdrawalQueue', ([owner, stranger, daoAgent, user]) => { assert.isTrue(firstGasUsed >= secondGasUsed) }) }) + + context('getWithdrawalStatus', () => { + it('reverts if requestId is zero', async () => { + await assert.reverts(withdrawalQueue.getWithdrawalStatus([0]), `InvalidRequestId(0)`) + }) + + it('reverts if requestId is ahead of currently stored', async () => { + const idAhead = +(await withdrawalQueue.getLastRequestId()) + 1 + await assert.reverts(withdrawalQueue.getWithdrawalStatus([idAhead]), `InvalidRequestId(${idAhead})`) + }) + }) }) diff --git a/test/0.8.9/withdrawal-request-nft.test.js b/test/0.8.9/withdrawal-request-nft.test.js deleted file mode 100644 index 44538619f..000000000 --- a/test/0.8.9/withdrawal-request-nft.test.js +++ /dev/null @@ -1,327 +0,0 @@ -const { contract, artifacts, ethers } = require('hardhat') -const { assert } = require('../helpers/assert') - -const { EvmSnapshot, setBalance } = require('../helpers/blockchain') -const { shares, ETH, shareRate } = require('../helpers/utils') -const withdrawals = require('../helpers/withdrawals') - -const StETH = artifacts.require('StETHMock') -const WstETH = artifacts.require('WstETHMock') -const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock') - -contract('WithdrawalNFT', (addresses) => { - const [deployer, stEthHolder, wstEthHolder, nftHolderStETH, nftHolderWstETH, recipient, stranger] = addresses - let withdrawalQueueERC721, stETH, wstETH, erc721ReceiverMock - let nftHolderStETHTokenIds, nftHolderWstETHTokenIds, nonExistedTokenId - const snapshot = new EvmSnapshot(ethers.provider) - - before(async () => { - stETH = await StETH.new({ value: ETH(1), from: deployer }) - await setBalance(stETH.address, ETH(100)) - - wstETH = await WstETH.new(stETH.address, { from: deployer }) - erc721ReceiverMock = await ERC721ReceiverMock.new({ from: deployer }) - withdrawalQueueERC721 = (await withdrawals.deploy(deployer, wstETH.address, 'Lido TEST Request', 'unstEsT')).queue - await withdrawalQueueERC721.initialize( - deployer, // owner - deployer, // pauser - deployer, // resumer - deployer, // finalizer - deployer - ) - await withdrawalQueueERC721.resume({ from: deployer }) - - await stETH.setTotalPooledEther(ETH(101)) - await stETH.mintShares(stEthHolder, shares(50)) - await stETH.mintShares(wstETH.address, shares(50)) - await wstETH.mint(wstEthHolder, ETH(25)) - - await stETH.approve(withdrawalQueueERC721.address, ETH(50), { from: stEthHolder }) - await wstETH.approve(withdrawalQueueERC721.address, ETH(25), { from: wstEthHolder }) - await withdrawalQueueERC721.requestWithdrawals([ETH(25), ETH(25)], nftHolderStETH, { from: stEthHolder }) - nftHolderStETHTokenIds = [1, 2] - await withdrawalQueueERC721.requestWithdrawalsWstETH([ETH(25)], nftHolderWstETH, { from: wstEthHolder }) - nftHolderWstETHTokenIds = [3] - nonExistedTokenId = 4 - await snapshot.make() - }) - - afterEach(async () => { - await snapshot.rollback() - }) - - describe('ERC721Metadata', () => { - it('Initial properties', async () => { - assert.equals(await withdrawalQueueERC721.symbol(), 'unstEsT') - assert.equals(await withdrawalQueueERC721.name(), 'Lido TEST Request') - }) - }) - - describe('supportsInterface()', () => { - it('returns true for IERC165 interfaceiId (0x01ffc9a7)', async () => { - assert.isTrue(await withdrawalQueueERC721.supportsInterface('0x01ffc9a7')) - }) - it('returns true for IERC721 interface id (0x80ac58cd)', async () => { - assert.isTrue(await withdrawalQueueERC721.supportsInterface('0x80ac58cd')) - }) - it('returns true for AccessControlEnumerable interface id (0x5a05180f)', async () => { - assert.isTrue(await withdrawalQueueERC721.supportsInterface('0x5a05180f')) - }) - it('returns false for unsupported e interface id (0xffffffff)', async () => { - assert.isFalse(await withdrawalQueueERC721.supportsInterface('0xffffffff')) - }) - it('returns false for unsupported e interface id (0xdeadbeaf)', async () => { - assert.isFalse(await withdrawalQueueERC721.supportsInterface('0xdeadbeaf')) - }) - }) - - describe('balanceOf()', () => { - it('return 0 when user has not withdrawal requests', async () => { - assert.equals(await withdrawalQueueERC721.balanceOf(recipient), 0) - }) - - it('return correct withdrawal requests count', async () => { - assert.equals(await withdrawalQueueERC721.balanceOf(nftHolderStETH), 2) - assert.equals(await withdrawalQueueERC721.balanceOf(nftHolderWstETH), 1) - }) - }) - - describe('ownerOf()', () => { - it('reverts with error InvalidRequestId() when token id is 0', async () => { - await assert.reverts(withdrawalQueueERC721.ownerOf(0), `InvalidRequestId(0)`) - }) - - it('reverts with error InvalidRequestId() when called with non existed token id', async () => { - await assert.reverts(withdrawalQueueERC721.ownerOf(nonExistedTokenId), `InvalidRequestId(${nonExistedTokenId})`) - }) - - it('reverts correct owner', async () => { - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), nftHolderStETH) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[1]), nftHolderStETH) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderWstETHTokenIds[0]), nftHolderWstETH) - }) - }) - - describe('approve()', async () => { - it('reverts with message "ApprovalToOwner()" when approval for owner address', async () => { - await assert.reverts( - withdrawalQueueERC721.approve(nftHolderStETH, nftHolderStETHTokenIds[0], { from: nftHolderStETH }), - 'ApprovalToOwner()' - ) - }) - - it('reverts with message "NotOwnerOrApprovedForAll()" when called noy by owner', async () => { - await assert.reverts( - withdrawalQueueERC721.approve(recipient, nftHolderStETHTokenIds[0], { from: stranger }), - `NotOwnerOrApprovedForAll("${stranger}")` - ) - }) - - it('sets approval for address', async () => { - await withdrawalQueueERC721.approve(recipient, nftHolderStETHTokenIds[0], { from: nftHolderStETH }) - assert.equal(await withdrawalQueueERC721.getApproved(nftHolderStETHTokenIds[0]), recipient) - }) - }) - - describe('getApproved()', async () => { - it('reverts with message "InvalidRequestId()" when called with non existed token id', async () => { - await assert.reverts( - withdrawalQueueERC721.getApproved(nonExistedTokenId), - `InvalidRequestId(${nonExistedTokenId})` - ) - }) - }) - - describe('setApprovalForAll()', async () => { - it('reverts with message "ApproveToCaller()" when owner equal to operator', async () => { - await assert.reverts( - withdrawalQueueERC721.setApprovalForAll(nftHolderStETH, true, { from: nftHolderStETH }), - 'ApproveToCaller()' - ) - }) - }) - - describe('safeTransferFrom(address,address,uint256)', async () => { - it('reverts with message "NotOwnerOrApproved()" when approvalNotSet and not owner', async () => { - await assert.reverts( - withdrawalQueueERC721.safeTransferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: stranger, - }), - `NotOwnerOrApproved("${stranger}")` - ) - }) - - it('transfers if called by owner', async () => { - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - await withdrawalQueueERC721.safeTransferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: nftHolderStETH, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - }) - - it('transfers if token approval set', async () => { - await withdrawalQueueERC721.approve(recipient, nftHolderStETHTokenIds[0], { from: nftHolderStETH }) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - await withdrawalQueueERC721.safeTransferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: recipient, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - }) - - it('transfers if operator approval set', async () => { - await withdrawalQueueERC721.setApprovalForAll(recipient, true, { from: nftHolderStETH }) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[1]), recipient) - await withdrawalQueueERC721.safeTransferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: recipient, - }) - await withdrawalQueueERC721.safeTransferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[1], { - from: recipient, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[1]), recipient) - }) - - it('reverts with message "TransferToNonIERC721Receiver()" when transfer to contract that not implements IERC721Receiver interface', async () => { - await assert.reverts( - withdrawalQueueERC721.safeTransferFrom(nftHolderWstETH, stETH.address, nftHolderWstETHTokenIds[0], { - from: nftHolderWstETH, - }), - `TransferToNonIERC721Receiver("${stETH.address}")` - ) - }) - - it('reverts with propagated error message when recipient contract implements ERC721Receiver and reverts on onERC721Received call', async () => { - await erc721ReceiverMock.setDoesAcceptTokens(false, { from: deployer }) - await assert.reverts( - withdrawalQueueERC721.safeTransferFrom(nftHolderStETH, erc721ReceiverMock.address, nftHolderStETHTokenIds[0], { - from: nftHolderStETH, - }), - 'ERC721_NOT_ACCEPT_TOKENS' - ) - }) - - it("doesn't revert when recipient contract implements ERC721Receiver interface and accepts tokens", async () => { - await erc721ReceiverMock.setDoesAcceptTokens(true, { from: deployer }) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), erc721ReceiverMock.address) - await withdrawalQueueERC721.safeTransferFrom( - nftHolderStETH, - erc721ReceiverMock.address, - nftHolderStETHTokenIds[0], - { - from: nftHolderStETH, - } - ) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), erc721ReceiverMock.address) - }) - }) - - describe('transferFrom()', async () => { - it('reverts with message "NotOwnerOrApproved()" when approvalNotSet and not owner', async () => { - await assert.reverts( - withdrawalQueueERC721.transferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { from: stranger }), - `NotOwnerOrApproved("${stranger}")` - ) - }) - - it('reverts when transfer to the same address', async () => { - await assert.reverts( - withdrawalQueueERC721.transferFrom(nftHolderWstETH, nftHolderWstETH, nftHolderWstETHTokenIds[0], { - from: nftHolderWstETH, - }), - 'TransferToThemselves()' - ) - }) - - it('reverts with error "RequestAlreadyClaimed()" when called on claimed request', async () => { - const batch = await withdrawalQueueERC721.finalizationBatch(3, shareRate(1)) - await withdrawalQueueERC721.finalize(3, { from: deployer, value: batch.ethToLock }) - const ownerETHBefore = await ethers.provider.getBalance(nftHolderStETH) - const tx = await withdrawalQueueERC721.methods['claimWithdrawal(uint256)'](nftHolderStETHTokenIds[0], { - from: nftHolderStETH, - }) - const ownerETHAfter = await ethers.provider.getBalance(nftHolderStETH) - // adhoc fix for solidity-coverage that ignores gasPrice = 0 - assert.almostEqual(ownerETHAfter, ownerETHBefore.add(ETH(25)), tx.receipt.gasUsed) - - await assert.reverts( - withdrawalQueueERC721.transferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: nftHolderStETH, - }), - `RequestAlreadyClaimed(${nftHolderStETHTokenIds[0]})` - ) - }) - - it('transfers if called by owner', async () => { - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderWstETHTokenIds[0]), recipient) - await withdrawalQueueERC721.transferFrom(nftHolderWstETH, recipient, nftHolderWstETHTokenIds[0], { - from: nftHolderWstETH, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderWstETHTokenIds[0]), recipient) - }) - - it('transfers if token approval set', async () => { - await withdrawalQueueERC721.approve(recipient, nftHolderStETHTokenIds[0], { from: nftHolderStETH }) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - await withdrawalQueueERC721.transferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: recipient, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - }) - - it('transfers if operator approval set', async () => { - await withdrawalQueueERC721.setApprovalForAll(recipient, true, { from: nftHolderStETH }) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - assert.notEqual(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[1]), recipient) - await withdrawalQueueERC721.transferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: recipient, - }) - await withdrawalQueueERC721.transferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[1], { - from: recipient, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[1]), recipient) - }) - - it('can claim request after transfer', async () => { - await withdrawalQueueERC721.transferFrom(nftHolderStETH, recipient, nftHolderStETHTokenIds[0], { - from: nftHolderStETH, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderStETHTokenIds[0]), recipient) - - const batch = await withdrawalQueueERC721.finalizationBatch(3, shareRate(1)) - await withdrawalQueueERC721.finalize(3, { from: deployer, value: batch.ethToLock }) - - const recipientETHBefore = await ethers.provider.getBalance(recipient) - const tx = await withdrawalQueueERC721.methods['claimWithdrawal(uint256)'](nftHolderStETHTokenIds[0], { - from: recipient, - }) - const recipientETHAfter = await ethers.provider.getBalance(recipient) - // adhoc fix for solidity-coverage that ignores gasPrice = 0 - assert.almostEqual(recipientETHAfter, recipientETHBefore.add(ETH(25)), tx.receipt.gasUsed) - }) - - it("doesn't reverts when transfer to contract that not implements IERC721Receiver interface", async () => { - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderWstETHTokenIds[0]), nftHolderWstETH) - await withdrawalQueueERC721.transferFrom(nftHolderWstETH, stETH.address, nftHolderWstETHTokenIds[0], { - from: nftHolderWstETH, - }) - assert.equal(await withdrawalQueueERC721.ownerOf(nftHolderWstETHTokenIds[0]), stETH.address) - }) - }) - - describe('Burn', () => { - it('balanceOf decreases after claim', async () => { - const balanceBefore = await withdrawalQueueERC721.balanceOf(nftHolderStETH) - - const batch = await withdrawalQueueERC721.finalizationBatch(3, shareRate(1)) - await withdrawalQueueERC721.finalize(3, { from: deployer, value: batch.ethToLock }) - - await withdrawalQueueERC721.methods['claimWithdrawal(uint256)'](nftHolderStETHTokenIds[0], { - from: nftHolderStETH, - }) - - assert.equals(balanceBefore - (await withdrawalQueueERC721.balanceOf(nftHolderStETH)), 1) - }) - }) -}) diff --git a/test/common/lib/signature-utils.test.js b/test/common/lib/signature-utils.test.js new file mode 100644 index 000000000..ee06c0c1f --- /dev/null +++ b/test/common/lib/signature-utils.test.js @@ -0,0 +1,129 @@ +const { artifacts, ethers, contract } = require('hardhat') + +const { assert } = require('../../helpers/assert') +const { toBN, hex, hexConcat } = require('../../helpers/utils') +const { EvmSnapshot } = require('../../helpers/blockchain') +const { ecSign } = require('../../helpers/signatures') +const { ACCOUNTS_AND_KEYS } = require('../../helpers/constants') + +const ERC1271SignerMock = artifacts.require('ERC1271SignerMock') +const ERC1271SignerDumbMock = artifacts.require('ERC1271SignerDumbMock') +const ERC1271MutatingSignerMock = artifacts.require('ERC1271MutatingSignerMock') +const SignatureUtilsConsumer_0_4_24 = artifacts.require('SignatureUtilsConsumer_0_4_24') +const SignatureUtilsConsumer_0_8_9 = artifacts.require('SignatureUtilsConsumer_0_8_9') + +testWithConsumer(SignatureUtilsConsumer_0_4_24, 'Solidity 0.4.24') +testWithConsumer(SignatureUtilsConsumer_0_8_9, 'Solidity 0.8.9') + +function testWithConsumer(SignatureUtilsConsumer, desc) { + const ERC1271_MAGIC_VALUE = '0x1626ba7e' + + contract(`SignatureUtils.isValidSignature, ${desc}`, () => { + const msgHash = `0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef` + let sigUtils, snapshot + + before(async () => { + sigUtils = await SignatureUtilsConsumer.new() + snapshot = new EvmSnapshot(ethers.provider) + await snapshot.make() + }) + + afterEach(async () => { + await snapshot.rollback() + }) + + context(`signer is a EOA`, () => { + const [alice, bob] = ACCOUNTS_AND_KEYS + + it(`returns true given a valid ECDSA signature`, async () => { + const sig = ecSign(msgHash, alice.key) + assert.isTrue(await sigUtils.isValidSignature(alice.address, msgHash, sig.v, sig.r, sig.s)) + }) + + it(`returns false given a valid ECDSA signature from another account`, async () => { + const sig = ecSign(msgHash, bob.key) + assert.isFalse(await sigUtils.isValidSignature(alice.address, msgHash, sig.v, sig.r, sig.s)) + }) + + it(`reverts on an invalid ECDSA signature`, async () => { + const sig = ecSign(msgHash, alice.key) + + const MAX_S = '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0' + const INVALID_S = '0x' + hex(toBN(MAX_S).addn(1), 32) + + await assert.reverts( + sigUtils.isValidSignature(alice.address, msgHash, sig.v, sig.r, INVALID_S), + `ECDSA: invalid signature 's' value` + ) + + const INVALID_V = 1 + + await assert.reverts( + sigUtils.isValidSignature(alice.address, msgHash, INVALID_V, sig.r, sig.s), + `ECDSA: invalid signature 'v' value` + ) + }) + }) + + context(`signer is a contract (ERC1271)`, () => { + const v = '0xff' + const r = '0x8badf00d8badf00d8badf00d8badf00d8badf00d8badf00d8badf00d8badf00d' + const s = '0xc00010ffc00010ffc00010ffc00010ffc00010ffc00010ffc00010ffc00010ff' + + context(`checks the signer.isValidSignature call result`, () => { + let signerContract + + before(async () => { + signerContract = await ERC1271SignerDumbMock.new() + await snapshot.make() + }) + + it(`returns true when the call returns the magic value`, async () => { + await signerContract.configure({ reverts: false, retval: ERC1271_MAGIC_VALUE }) + assert.isTrue(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + }) + + it(`returns false when the call returns any other value`, async () => { + await signerContract.configure({ reverts: false, retval: '0x' + hex(toBN(ERC1271_MAGIC_VALUE).addn(1), 4) }) + assert.isFalse(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + + await signerContract.configure({ reverts: false, retval: '0x00000000' }) + assert.isFalse(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + + await signerContract.configure({ reverts: false, retval: '0x12345678' }) + assert.isFalse(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + }) + + it(`returns false when the call reverts`, async () => { + await signerContract.configure({ reverts: true, retval: ERC1271_MAGIC_VALUE }) + assert.isFalse(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + }) + }) + + context(`packs the signature when passing to signer.isValidSignature`, () => { + // + it(`the passed signature contains r, s, and then v`, async () => { + const signerContract = await ERC1271SignerMock.new() + + await signerContract.configure({ + validHash: msgHash, + validSig: hexConcat(r, s, v), + retvalOnValid: ERC1271_MAGIC_VALUE, + retvalOnInvalid: '0x00000000', + }) + + assert.isTrue(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + }) + }) + + context(`returns false when the signer contract misbehaves`, () => { + // + it(`signer contract attempts to modify the state`, async () => { + const signerContract = await ERC1271MutatingSignerMock.new() + assert.isFalse(await sigUtils.isValidSignature(signerContract.address, msgHash, v, r, s)) + assert.equals(await signerContract.callCount_isValidSignature(), 0) + }) + }) + }) + }) +} diff --git a/test/helpers/assert.js b/test/helpers/assert.js index 37d27791e..5c077074c 100644 --- a/test/helpers/assert.js +++ b/test/helpers/assert.js @@ -6,14 +6,23 @@ const { toChecksumAddress } = require('ethereumjs-util') const { isAddress } = require('ethers/lib/utils') const { toBN } = require('./utils') -chai.util.addMethod(chai.assert, 'emits', function (receipt, eventName, args = {}, options = {}) { - const event = getEvent(receipt, eventName, args, options.abi) - this.isTrue(event !== undefined, `Event ${eventName} with args ${JSON.stringify(args)} wasn't found`) +chai.util.addMethod(chai.assert, 'emits', function (receipt, eventName, args = undefined, options = {}) { + const events = getEvents(receipt, eventName, { decodeForAbi: options.abi }) + chai.assert(events.length !== 0, () => `Expected event '${eventName}' wasn't emitted`) + if (args !== undefined) { + chai.assert( + findEventWithArgs(args, events) !== undefined, + () => `No '${eventName}' event was emitted with expected args ${JSON.stringify(args)}` + ) + } }) chai.util.addMethod(chai.assert, 'emitsAt', function (receipt, eventName, index, args = {}, options = {}) { const event = getEventAt(receipt, eventName, index, args, options.abi) - this.isTrue(event !== undefined, `Event ${eventName} at ${index} with args ${JSON.stringify(args)} wasn't found`) + chai.assert( + event !== undefined, + () => `Event '${eventName}' at index ${index} with args ${JSON.stringify(args)} wasn't found` + ) }) chai.util.addMethod( @@ -63,17 +72,25 @@ chai.util.addMethod(chai.assert, 'equals', function (actual, expected, errorMsg) }) chai.util.addMethod(chai.assert, 'equalsDelta', function (actual, expected, delta, errorMsg) { + this.isClose(actual, expected, delta, errorMsg) +}) + +const msg = (errorMsg, str) => `${errorMsg ? `${errorMsg}: ` : ''}${str}` + +chai.util.addMethod(chai.assert, 'isClose', function (actual, expected, delta, errorMsg) { const diff = toBN(actual).sub(toBN(expected)).abs() chai.assert( diff.lte(toBN(delta)), - () => - `${ - errorMsg ? `${errorMsg}: ` : '' - }Expected ${actual} to be close to ${expected} with max diff ${delta}, actual diff ${diff}`, - () => - `${ - errorMsg ? `${errorMsg}: ` : '' - }Expected ${actual} not to be close to ${expected} with min diff ${delta}, actual diff ${diff}` + () => msg(errorMsg, `Expected ${actual} to be close to ${expected} with max diff ${delta}, actual diff ${diff}`), + () => msg(errorMsg, `Expected ${actual} not to be close to ${expected} with min diff ${delta}, actual diff ${diff}`) + ) +}) + +chai.util.addMethod(chai.assert, 'bnAbove', function (nAbove, nBelow, errorMsg) { + chai.assert( + toBN(nAbove).gt(toBN(nBelow)), + () => msg(errorMsg, `Expected ${nAbove} to be above ${nBelow}`), + () => msg(errorMsg, `Expected ${nAbove} not to be above ${nBelow}`) ) }) @@ -131,8 +148,13 @@ function getEventAt(receipt, eventName, index, args, abi) { } function getEvent(receipt, eventName, args, abi) { - return getEvents(receipt, eventName, { decodeForAbi: abi }).find((e) => - // find the first index where every event argument matches the expected one + const events = getEvents(receipt, eventName, { decodeForAbi: abi }) + return findEventWithArgs(args, events) +} + +function findEventWithArgs(args, events) { + // find the first index where every event argument matches the expected one + return events.find((e) => Object.entries(args).every( ([argName, argValue]) => e.args[argName] !== undefined && normalizeArg(e.args[argName]) === normalizeArg(argValue) ) diff --git a/test/helpers/constants.js b/test/helpers/constants.js index ae016dacd..a0d76ef80 100644 --- a/test/helpers/constants.js +++ b/test/helpers/constants.js @@ -4,6 +4,70 @@ const INITIAL_HOLDER = '0x000000000000000000000000000000000000dead' const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' const MAX_UINT256 = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +// derived from mnemonic: clarify final village pulse require old seek excite mushroom forest satoshi video +const ACCOUNTS_AND_KEYS = [ + { + address: '0x8e81C8f0CFf3d6eA2Fe72c1A5ee49Fc377401c2D', + key: '84132dd41f32804774a98647c308c0c94a54c0f3931128c0210b6f3689d2b7e7', + }, + { + address: '0x244A0A1d21f21167c17e04EBc5FA33c885990674', + key: '31a372c197c7c5d6856bfac311a66f179bdc3bda20e78030b0fef90e40cbc83f', + }, + { + address: '0x6966f881B3Ee9074b0783CC614e3864e380B8b27', + key: '58bdc54eb2aa3a92e5a36fae01d23b7626fa89116a41e01ca3c6cb18799aee3d', + }, + { + address: '0xaC5faE80468DcC49D404d0625609C031B9AF2cb7', + key: 'd14ca7a30bc2921ddb89ea2f6c52393f91e794cd9c3c994f547eb7edeb092fd1', + }, + { + address: '0x18C246058e9e4Ae1737387fA90cD69E39f78F73A', + key: '1c09baae4343b0a66567d56769b62537623389542480f30e9acdae0624612872', + }, + { + address: '0xc4dc5dAD36fF9b8cB694E79238ECbe76ab13BcEc', + key: '2048571627de761088f2f369306f9c231e6c7ab94be1f0a0c979776a2f424328', + }, + { + address: '0x75Bf6E76D3dB629f46da549D49C9ea821CE8e9A9', + key: '9a9afb6a8e2384e4cce14824d15162bba2eed7e31df846ff77d697fed8cba0c1', + }, + { + address: '0x55937b3ae7a34F551e5aFa5BA51Fba4eD9f8FeD7', + key: '9747a0294186b8fef20ec1e4341a10c87840b18a8930b84c4c5dcd97799ba0bc', + }, + { + address: '0x1eEEBc3900803a28ca6E68Eb98FDeCf98350D97B', + key: 'cdbaf218f6332bfa630ec2abc7a8bc450b2e8746133d84450dfc2234fdfb83ef', + }, + { + address: '0x8719beDE472E67642b88DbA9aD1b1b9dc05CC60e', + key: 'c08dbf2c9aaf5dc3cd5ce5349e754de086202a5f352a512851a4f174ac246198', + }, + { + address: '0xE03701ea2248C7ca2Fed1e655ff3C803C7267302', + key: '40566af8625c93f95c5e1ea9aa42642bcbdd64fe4fe7926283d04173cb842189', + }, + { + address: '0x10506aB975D36aa781B77C1Ce204F46e8f87dA57', + key: '55ef7a25bba3449344d8a1e525ba3ca4999bdc763be4efaf9e94a4de396f8370', + }, + { + address: '0xee11cdb1f9eEe2eB40D2CF99b4fa7D782aE582A6', + key: '985117805090656613ae19743879efe4cff76049de03516debf41926313e3629', + }, + { + address: '0xB9203C29E242d7a19AE6970cDf0d873048B99419', + key: '3a37d1ad3751e697c26dd05a71cd9263c023f3e8a1d1067215044032c62ee916', + }, + { + address: '0xcC03603436Bc0Edd1e0661379f3Aa289ae967597', + key: 'db81ac68891890ec33021264ce81f9b5f9296a9fd95cdc785655db3066db2c08', + }, +] + const SLOTS_PER_EPOCH = 32 const SECONDS_PER_SLOT = 12 const EPOCHS_PER_FRAME = 225 // one day @@ -17,6 +81,7 @@ module.exports = { ZERO_ADDRESS, ZERO_BYTES32, MAX_UINT256, + ACCOUNTS_AND_KEYS, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, EPOCHS_PER_FRAME, diff --git a/test/helpers/debug.js b/test/helpers/debug.js new file mode 100644 index 000000000..b351e1e2a --- /dev/null +++ b/test/helpers/debug.js @@ -0,0 +1,21 @@ +const { BN } = require('bn.js') + +// transforms all object entries +const transformEntries = (obj, tr) => + Object.fromEntries( + Object.entries(obj) + .map(tr) + .filter((x) => x !== undefined) + ) + +// converts all object BN keys to strings, drops numeric keys and the __length__ key +const processNamedTuple = (obj) => + transformEntries(obj, ([k, v]) => { + return /^(\d+|__length__)$/.test(k) ? undefined : [k, BN.isBN(v) ? v.toString() : v] + }) + +const printEvents = (tx) => { + console.log(tx.receipt.logs.map(({ event, args }) => ({ event, args: processNamedTuple(args) }))) +} + +module.exports = { transformEntries, processNamedTuple, printEvents } diff --git a/test/helpers/factories.js b/test/helpers/factories.js index c93c7d2ca..c080ddb22 100644 --- a/test/helpers/factories.js +++ b/test/helpers/factories.js @@ -116,12 +116,13 @@ async function hashConsensusFactory({ voting, oracle, signers, legacyOracle, dep SECONDS_PER_SLOT, deployParams.genesisTime, EPOCHS_PER_FRAME, - initialEpoch, deployParams.hashConsensus.fastLaneLengthSlots, voting.address, oracle.address ) + await consensus.updateInitialEpoch(initialEpoch, { from: voting.address }) + await consensus.grantRole(await consensus.MANAGE_MEMBERS_AND_QUORUM_ROLE(), voting.address, { from: voting.address }) await consensus.grantRole(await consensus.DISABLE_CONSENSUS_ROLE(), voting.address, { from: voting.address }) await consensus.grantRole(await consensus.MANAGE_FRAME_CONFIG_ROLE(), voting.address, { from: voting.address }) @@ -132,12 +133,6 @@ async function hashConsensusFactory({ voting, oracle, signers, legacyOracle, dep await consensus.addMember(signers[3].address, 2, { from: voting.address }) await consensus.addMember(signers[4].address, 2, { from: voting.address }) - await oracle.initialize(voting.address, consensus.address, CONSENSUS_VERSION) - - 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 }) - return consensus } @@ -161,6 +156,8 @@ async function hashConsensusTimeTravellableFactory({ oracle.address ) + await consensus.updateInitialEpoch(initialEpoch, { from: voting.address }) + await consensus.grantRole(await consensus.MANAGE_MEMBERS_AND_QUORUM_ROLE(), voting.address, { from: voting.address }) await consensus.grantRole(await consensus.DISABLE_CONSENSUS_ROLE(), voting.address, { from: voting.address }) await consensus.grantRole(await consensus.MANAGE_FRAME_CONFIG_ROLE(), voting.address, { from: voting.address }) @@ -169,6 +166,7 @@ async function hashConsensusTimeTravellableFactory({ await consensus.addMember(signers[2].address, 1, { from: voting.address }) await consensus.addMember(signers[3].address, 2, { from: voting.address }) await consensus.addMember(signers[4].address, 2, { from: voting.address }) + await consensus.setTime(deployParams.genesisTime + initialEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) return consensus @@ -257,19 +255,22 @@ async function elRewardsVaultFactory({ pool, treasury }) { return await LidoExecutionLayerRewardsVault.new(pool.address, treasury.address) } -async function withdrawalQueueFactory({ appManager, oracle, wsteth }) { +async function withdrawalQueueFactory({ appManager, pool, oracle, wsteth }) { const withdrawalQueue = (await withdrawals.deploy(appManager.address, wsteth.address)).queue - await withdrawalQueue.initialize( - appManager.address, - appManager.address, - appManager.address, - appManager.address, - appManager.address - ) + await withdrawalQueue.initialize(appManager.address) + + const ORACLE_ROLE = await withdrawalQueue.ORACLE_ROLE() + await withdrawalQueue.grantRole(ORACLE_ROLE, oracle.address, { from: appManager.address }) + const FINALIZE_ROLE = await withdrawalQueue.FINALIZE_ROLE() + await withdrawalQueue.grantRole(FINALIZE_ROLE, pool.address, { from: appManager.address }) - const BUNKER_MODE_REPORT_ROLE = await withdrawalQueue.BUNKER_MODE_REPORT_ROLE() - await withdrawalQueue.grantRole(BUNKER_MODE_REPORT_ROLE, oracle.address, { from: appManager.address }) + await grantRoles({ + by: appManager.address, + on: withdrawalQueue, + to: appManager.address, + roles: ['PAUSE_ROLE', 'RESUME_ROLE', 'FINALIZE_ROLE', 'ORACLE_ROLE'], + }) return withdrawalQueue } @@ -296,13 +297,15 @@ async function guardiansFactory({ deployParams }) { async function burnerFactory({ appManager, treasury, pool, voting }) { const burner = await Burner.new(appManager.address, treasury.address, pool.address, 0, 0) - const [REQUEST_BURN_MY_STETH_ROLE, RECOVER_ASSETS_ROLE] = await Promise.all([ + const [REQUEST_BURN_MY_STETH_ROLE, REQUEST_BURN_SHARES_ROLE, RECOVER_ASSETS_ROLE] = await Promise.all([ burner.REQUEST_BURN_MY_STETH_ROLE(), + burner.REQUEST_BURN_SHARES_ROLE(), burner.RECOVER_ASSETS_ROLE(), ]) await burner.grantRole(REQUEST_BURN_MY_STETH_ROLE, voting.address, { from: appManager.address }) await burner.grantRole(RECOVER_ASSETS_ROLE, voting.address, { from: appManager.address }) + await burner.grantRole(REQUEST_BURN_SHARES_ROLE, voting.address, { from: appManager.address }) return burner } @@ -319,7 +322,23 @@ async function oracleReportSanityCheckerFactory({ lidoLocator, voting, appManage deployParams.oracleReportSanityChecker.managersRoster ) - await checker.grantRole(await checker.ALL_LIMITS_MANAGER_ROLE(), voting.address, { from: appManager.address }) + await grantRoles({ + by: appManager.address, + on: checker, + to: voting.address, + roles: [ + 'ALL_LIMITS_MANAGER_ROLE', + 'CHURN_VALIDATORS_PER_DAY_LIMIT_MANGER_ROLE', + 'ONE_OFF_CL_BALANCE_DECREASE_LIMIT_MANAGER_ROLE', + 'ANNUAL_BALANCE_INCREASE_LIMIT_MANAGER_ROLE', + 'SHARE_RATE_DEVIATION_LIMIT_MANAGER_ROLE', + 'MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT_ROLE', + 'MAX_ACCOUNTING_EXTRA_DATA_LIST_ITEMS_COUNT_ROLE', + 'MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM_COUNT_ROLE', + 'REQUEST_TIMESTAMP_MARGIN_MANAGER_ROLE', + 'MAX_POSITIVE_TOKEN_REBASE_MANAGER_ROLE', + ], + }) return checker } @@ -347,19 +366,32 @@ async function postSetup({ appManager, voting, deployParams, + oracle, legacyOracle, consensusContract, }) { await pool.initialize(lidoLocator.address, eip712StETH.address, { value: ETH(1) }) + await oracle.initialize(voting.address, consensusContract.address, CONSENSUS_VERSION) + 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 }) + await legacyOracle.initialize(lidoLocator.address, consensusContract.address) await depositContract.reset() await depositContract.set_deposit_root(deployParams.depositRoot) - await withdrawalQueue.updateBunkerMode(false, 0, { from: appManager.address }) await pool.resumeProtocolAndStaking({ from: voting.address }) } +async function grantRoles({ by, on, to, roles }) { + await Promise.all( + roles.map(async (role) => { + await on.grantRole(await on[role](), to, { from: by }) + }) + ) +} + module.exports = { appManagerFactory, treasuryFactory, @@ -389,4 +421,5 @@ module.exports = { oracleReportSanityCheckerFactory, validatorExitBusFactory, oracleReportSanityCheckerStubFactory, + grantRoles, } diff --git a/test/helpers/oracle.js b/test/helpers/oracle.js index 8e291fc97..56bfb988d 100644 --- a/test/helpers/oracle.js +++ b/test/helpers/oracle.js @@ -2,37 +2,71 @@ const { web3 } = require('hardhat') const { CONSENSUS_VERSION, ZERO_BYTES32 } = require('./constants') const { assert } = require('./assert') +const { toBN } = require('./utils') function getReportDataItems(r) { return [ - r.consensusVersion, - +r.refSlot, - r.numValidators, - r.clBalanceGwei, - r.stakingModuleIdsWithNewlyExitedValidators, - r.numExitedValidatorsByStakingModule, - r.withdrawalVaultBalance, - r.elRewardsVaultBalance, - r.lastWithdrawalRequestIdToFinalize, - r.finalizationShareRate, + String(r.consensusVersion), + String(r.refSlot), + String(r.numValidators), + String(r.clBalanceGwei), + r.stakingModuleIdsWithNewlyExitedValidators.map(String), + r.numExitedValidatorsByStakingModule.map(String), + String(r.withdrawalVaultBalance), + String(r.elRewardsVaultBalance), + String(r.sharesRequestedToBurn), + r.withdrawalFinalizationBatches.map(String), + String(r.simulatedShareRate), r.isBunkerMode, - r.extraDataFormat, + String(r.extraDataFormat), r.extraDataHash, - r.extraDataItemsCount, + String(r.extraDataItemsCount), ] } function calcReportDataHash(reportItems) { const data = web3.eth.abi.encodeParameters( [ - '(uint256,uint256,uint256,uint256,uint256[],uint256[],uint256,uint256,uint256,uint256,bool,uint256,bytes32,uint256)', + '(uint256,uint256,uint256,uint256,uint256[],uint256[],uint256,uint256,uint256,uint256[],uint256,bool,uint256,bytes32,uint256)', ], [reportItems] ) - return web3.utils.keccak256(data) } +const DEFAULT_REPORT_FIELDS = { + consensusVersion: 1, + refSlot: 0, + numValidators: 0, + clBalanceGwei: 0, + stakingModuleIdsWithNewlyExitedValidators: [], + numExitedValidatorsByStakingModule: [], + withdrawalVaultBalance: 0, + elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, + withdrawalFinalizationBatches: [], + simulatedShareRate: 0, + isBunkerMode: false, + extraDataFormat: 0, + extraDataHash: ZERO_BYTES32, + extraDataItemsCount: 0, +} + +const E9 = toBN(10).pow(toBN(9)) + +async function prepareOracleReport({ clBalance, ...restFields }) { + const fields = { + ...DEFAULT_REPORT_FIELDS, + ...restFields, + clBalanceGwei: toBN(clBalance).div(E9), + } + + const items = getReportDataItems(fields) + const hash = calcReportDataHash(items) + + return { fields, items, hash } +} + async function triggerConsensusOnHash(hash, consensus) { const members = await consensus.getMembers() const { refSlot } = await consensus.getCurrentFrame() @@ -41,37 +75,48 @@ async function triggerConsensusOnHash(hash, consensus) { assert.equal((await consensus.getConsensusState()).consensusReport, hash) } -async function pushOracleReport(consensus, oracle, numValidators, clBalance, elRewards) { +async function reportOracle(consensus, oracle, reportFields) { const { refSlot } = await consensus.getCurrentFrame() - const reportFields = { - consensusVersion: 1, - refSlot, - numValidators, - clBalanceGwei: clBalance / 1e9, - stakingModuleIdsWithNewlyExitedValidators: [], - numExitedValidatorsByStakingModule: [], - withdrawalVaultBalance: 0, - elRewardsVaultBalance: elRewards || 0, - lastWithdrawalRequestIdToFinalize: 0, - finalizationShareRate: 0, - isBunkerMode: false, - extraDataFormat: 0, - extraDataHash: ZERO_BYTES32, - extraDataItemsCount: 0, - } - const reportItems = getReportDataItems(reportFields) - const reportHash = calcReportDataHash(reportItems) + const report = await prepareOracleReport({ ...reportFields, refSlot }) - const members = await consensus.getMembers() + // non-empty extra data is not supported here yet + assert.equals(report.fields.extraDataFormat, 0) + assert.equals(report.fields.extraDataHash, ZERO_BYTES32) + assert.equals(report.fields.extraDataItemsCount, 0) - await triggerConsensusOnHash(reportHash, consensus) + const members = await consensus.getMembers() + await triggerConsensusOnHash(report.hash, consensus) const oracleVersion = await oracle.getContractVersion() - - const submitDataTx = await oracle.submitReportData(reportItems, oracleVersion, { from: members.addresses[0] }) + const submitDataTx = await oracle.submitReportData(report.items, oracleVersion, { from: members.addresses[0] }) const submitExtraDataTx = await oracle.submitReportExtraDataEmpty({ from: members.addresses[0] }) - return { submitDataTx, submitExtraDataTx } + return { report, submitDataTx, submitExtraDataTx } +} + +// FIXME: kept for compat, remove after refactoring tests +function pushOracleReport(consensus, oracle, numValidators, clBalance, elRewardsVaultBalance) { + return reportOracle(consensus, oracle, { numValidators, clBalance, elRewardsVaultBalance }) } -module.exports = { getReportDataItems, calcReportDataHash, pushOracleReport } +async function getSecondsPerFrame(consensus) { + const [chainConfig, frameConfig] = await Promise.all([consensus.getChainConfig(), consensus.getFrameConfig()]) + return +chainConfig.secondsPerSlot * +chainConfig.slotsPerEpoch * +frameConfig.epochsPerFrame +} + +async function getSlotTimestamp(slot, consensus) { + const chainConfig = await consensus.getChainConfig() + return +chainConfig.genesisTime + +chainConfig.secondsPerSlot * slot +} + +module.exports = { + DEFAULT_REPORT_FIELDS, + getReportDataItems, + calcReportDataHash, + prepareOracleReport, + triggerConsensusOnHash, + reportOracle, + pushOracleReport, + getSecondsPerFrame, + getSlotTimestamp, +} diff --git a/test/helpers/reportData.js b/test/helpers/reportData.js new file mode 100644 index 000000000..eb3537fa7 --- /dev/null +++ b/test/helpers/reportData.js @@ -0,0 +1,46 @@ +const { web3 } = require('hardhat') + +function calcValidatorsExitBusReportDataHash(reportItems) { + const data = web3.eth.abi.encodeParameters(['(uint256,uint256,uint256,uint256,bytes)'], [reportItems]) + return web3.utils.keccak256(data) +} + +function getValidatorsExitBusReportDataItems(r) { + return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data] +} + +function calcAccountingReportDataHash(reportItems) { + const data = web3.eth.abi.encodeParameters( + [ + '(uint256,uint256,uint256,uint256,uint256[],uint256[],uint256,uint256,uint256,uint256[],uint256,bool,uint256,bytes32,uint256)', + ], + [reportItems] + ) + return web3.utils.keccak256(data) +} +function getAccountingReportDataItems(r) { + return [ + String(r.consensusVersion), + String(r.refSlot), + String(r.numValidators), + String(r.clBalanceGwei), + r.stakingModuleIdsWithNewlyExitedValidators.map(String), + r.numExitedValidatorsByStakingModule.map(String), + String(r.withdrawalVaultBalance), + String(r.elRewardsVaultBalance), + String(r.sharesRequestedToBurn), + r.withdrawalFinalizationBatches.map(String), + String(r.simulatedShareRate), + r.isBunkerMode, + String(r.extraDataFormat), + String(r.extraDataHash), + String(r.extraDataItemsCount), + ] +} + +module.exports = { + calcAccountingReportDataHash, + getAccountingReportDataItems, + getValidatorsExitBusReportDataItems, + calcValidatorsExitBusReportDataHash, +} diff --git a/test/helpers/signatures.js b/test/helpers/signatures.js index 200964dab..f51ca7be9 100644 --- a/test/helpers/signatures.js +++ b/test/helpers/signatures.js @@ -1,6 +1,13 @@ const BN = require('bn.js') +const { ecsign: ecSignBuf } = require('ethereumjs-util') const { keccak256 } = require('js-sha3') -const { ecSign, strip0x, bufferFromHexString, hexStringFromBuffer } = require('../0.6.12/helpers') + +const { strip0x, bufferFromHexString, hexStringFromBuffer } = require('./utils') + +function ecSign(digest, privateKey) { + const { v, r, s } = ecSignBuf(bufferFromHexString(digest), bufferFromHexString(privateKey)) + return { v, r: hexStringFromBuffer(r), s: hexStringFromBuffer(s) } +} // Converts a ECDSA signature to the format provided in https://eips.ethereum.org/EIPS/eip-2098. function toEip2098({ v, r, s }) { @@ -127,6 +134,9 @@ function hexToBytes(hex) { } module.exports = { + ecSign, + toEip2098, + keccak256, signDepositData, signPauseData, DSMPauseMessage, diff --git a/test/helpers/signing-keys.js b/test/helpers/signing-keys.js index db0e72266..1b7e16dc8 100644 --- a/test/helpers/signing-keys.js +++ b/test/helpers/signing-keys.js @@ -3,8 +3,7 @@ const SIGNATURE_LENGTH = 96 const EMPTY_PUBLIC_KEY = '0x' + '0'.repeat(2 * PUBKEY_LENGTH) const EMPTY_SIGNATURE = '0x' + '0'.repeat(2 * SIGNATURE_LENGTH) -const { pad, hexConcat, hexSplit } = require('./utils') -const { strip0x } = require('../0.6.12/helpers') +const { strip0x, pad, hexConcat, hexSplit } = require('./utils') class ValidatorKeys { constructor(publicKeys, signatures) { diff --git a/test/helpers/staking-modules.js b/test/helpers/staking-modules.js index bc946515d..edc4ecce9 100644 --- a/test/helpers/staking-modules.js +++ b/test/helpers/staking-modules.js @@ -80,6 +80,20 @@ async function setupNodeOperatorsRegistry({ dao, acl, lidoLocator, stakingRouter { from: appManager.address } ) + await acl.grantPermission( + stakingRouter.address, + nodeOperatorsRegistry.address, + NODE_OPERATOR_REGISTRY_MANAGE_NODE_OPERATOR_ROLE, + { from: appManager.address } + ) + + await acl.grantPermission( + stakingRouter.address, + nodeOperatorsRegistry.address, + NODE_OPERATOR_REGISTRY_MANAGE_NODE_OPERATOR_ROLE, + { from: appManager.address } + ) + return nodeOperatorsRegistry } diff --git a/test/helpers/stubs/generic.stub.js b/test/helpers/stubs/generic.stub.js index 474a89673..df48b8d40 100644 --- a/test/helpers/stubs/generic.stub.js +++ b/test/helpers/stubs/generic.stub.js @@ -38,6 +38,9 @@ class GenericStub { * @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 @@ -47,7 +50,6 @@ class GenericStub { * @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 {number} [config.nextState] one based state index to set after stub call * @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 @@ -67,8 +69,13 @@ class GenericStub { const [methodAbi] = methodAbis const configParser = new GenericStubConfigParser() - const parsedConfig = configParser.parse(methodAbi.signature, config) - await stubInstance.GenericStub__addStub(Object.values(parsedConfig)) + 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)) + } } } @@ -84,6 +91,7 @@ class GenericStubConfigParser { logs: this._parseLogs(config), forwardETH: this._parseForwardETH(config), isRevert: this._parseIsRevert(config), + currentState: this._parseCurrentState(config), nextState: this._parseNextState(config), } } @@ -98,7 +106,7 @@ class GenericStubConfigParser { } if (config.revert) { return config.revert.error - ? this._encodeError(config.revert.error) + ? this._encodeError(config.revert) : this._encodeError({ error: 'Error', args: { type: ['string'], value: [config.revert.reason || ''] } }) } return this._encode({ type: [], value: [] }) @@ -152,7 +160,13 @@ class GenericStubConfigParser { } _parseNextState(config) { - return config.nextState || 0 + 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 }) { diff --git a/test/helpers/stubs/staking-module.stub.js b/test/helpers/stubs/staking-module.stub.js index e333f50b5..994a33d10 100644 --- a/test/helpers/stubs/staking-module.stub.js +++ b/test/helpers/stubs/staking-module.stub.js @@ -8,13 +8,15 @@ class StakingModuleStub extends GenericStub { static async stubGetStakingModuleSummary( stakingModuleStub, - { totalExitedValidators, totalDepositedValidators, availableValidatorsCount } + { totalExitedValidators, totalDepositedValidators, depositableValidatorsCount }, + configOverrides = {} ) { await GenericStub.stub(stakingModuleStub, 'getStakingModuleSummary', { return: { type: ['uint256', 'uint256', 'uint256'], - value: [totalExitedValidators, totalDepositedValidators, availableValidatorsCount], + value: [totalExitedValidators, totalDepositedValidators, depositableValidatorsCount], }, + ...configOverrides, }) } diff --git a/test/helpers/utils.js b/test/helpers/utils.js index aaaede6c9..ee296ec8b 100644 --- a/test/helpers/utils.js +++ b/test/helpers/utils.js @@ -1,7 +1,10 @@ const { web3 } = require('hardhat') const assert = require('node:assert') +const chai = require('chai') const { BN } = require('bn.js') +const { getEvents } = require('@aragon/contract-helpers-test') +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000' const pad = (hex, bytesLength, fill = '0') => { @@ -60,14 +63,23 @@ const toBN = (obj) => { return str.startsWith('0x') ? new BN(str.substring(2), 16) : new BN(str, 10) } -function hex(n, byteLen) { - return n.toString(16).padStart(byteLen * 2, '0') +function hex(n, byteLen = undefined) { + const s = n.toString(16) + return byteLen === undefined ? s : s.padStart(byteLen * 2, '0') } function strip0x(s) { return s.substr(0, 2) === '0x' ? s.substr(2) : s } +function bufferFromHexString(hex) { + return Buffer.from(strip0x(hex), 'hex') +} + +function hexStringFromBuffer(buf) { + return '0x' + buf.toString('hex') +} + // Divides a BN by 1e15 const div15 = (bn) => bn.div(new BN(1000000)).div(new BN(1000000)).div(new BN(1000)) @@ -117,7 +129,49 @@ 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)) { + return { elBalanceUpdate: 0, sharesToBurn: 0 } + } + + let remainLimit = limitE9.sub(accumulatedRebase) + const remainEther = remainLimit.mul(toBN(preTotalPooledEther)).div(bnE9) + 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 remainShares = remainLimit.mul(toBN(preTotalShares)).div(bnE9.add(remainLimit)) + + if (remainShares.lte(toBN(sharesToBurn))) { + return { elBalanceUpdate, sharesToBurn: remainShares } + } + + return { elBalanceUpdate, sharesToBurn } +} + +const calcShareRateDeltaE27 = (preTotalPooledEther, postTotalPooledEther, preTotalShares, postTotalShares) => { + const oldShareRateE27 = toBN(e27(1)).mul(toBN(preTotalPooledEther)).div(toBN(preTotalShares)) + const newShareRatesE27 = toBN(e27(1)).mul(toBN(postTotalPooledEther)).div(toBN(postTotalShares)) + return newShareRatesE27.sub(oldShareRateE27) +} + +function getFirstEventArgs(receipt, eventName, abi = undefined) { + const events = getEvents(receipt, eventName, { decodeForAbi: abi }) + chai.assert(events.length !== 0, () => `Expected event ${eventName} wasn't emitted`) + return events[0].args +} + module.exports = { + ZERO_ADDRESS, ZERO_HASH, pad, hexConcat, @@ -125,6 +179,8 @@ module.exports = { toBN, hex, strip0x, + bufferFromHexString, + hexStringFromBuffer, div15, e9, e18, @@ -143,4 +199,7 @@ module.exports = { toStr, prepIdsCountsPayload, calcSharesMintedAsFees, + getFirstEventArgs, + calcShareRateDeltaE27, + limitRebase, } diff --git a/test/helpers/withdrawals.js b/test/helpers/withdrawals.js index 1a40270bb..0413bd2f5 100644 --- a/test/helpers/withdrawals.js +++ b/test/helpers/withdrawals.js @@ -1,7 +1,7 @@ const { artifacts } = require('hardhat') const OssifiableProxy = artifacts.require('OssifiableProxy.sol') -const WithdrawalQueueERC721 = artifacts.require('WithdrawalQueueERC721.sol') +const WithdrawalQueueERC721 = artifacts.require('WithdrawalQueueERC721Mock.sol') async function deploy(ownerAddress, wstethAddress, name = 'Lido: Withdrawal Request NFT', symbol = 'unstETH') { const impl = await WithdrawalQueueERC721.new(wstethAddress, name, symbol) diff --git a/test/scenario/changing_oracles_during_epoch.test.js b/test/scenario/changing_oracles_during_epoch.test.js index 2c0de7380..1ad956326 100644 --- a/test/scenario/changing_oracles_during_epoch.test.js +++ b/test/scenario/changing_oracles_during_epoch.test.js @@ -5,13 +5,13 @@ const { e9 } = require('../helpers/utils') const { deployAccountingOracleSetup, - initAccountingOracle, + configureAccountingOracleSetup, + getInitialFrameStartTime, deployMockLegacyOracle, CONSENSUS_VERSION, - HASH_1, ZERO_HASH, - getReportDataItems, - calcReportDataHash, + getAccountingReportDataItems, + calcAccountingReportDataHash, } = require('../0.8.9/oracle/accounting-oracle-deploy.test') const SLOTS_PER_EPOCH = 32 @@ -19,9 +19,6 @@ const SECONDS_PER_SLOT = 12 const GENESIS_TIME = 1606824000 const EPOCHS_PER_FRAME = 225 -const SLOTS_PER_FRAME = EPOCHS_PER_FRAME * SLOTS_PER_EPOCH -const SECONDS_PER_FRAME = SLOTS_PER_FRAME * SECONDS_PER_SLOT - contract('AccountingOracle', ([voting, malicious1, malicious2, member1, member2, member3]) => { let lido, consensus, oracle @@ -33,8 +30,9 @@ contract('AccountingOracle', ([voting, malicious1, malicious2, member1, member2, numExitedValidatorsByStakingModule: [], withdrawalVaultBalance: 0, elRewardsVaultBalance: 0, - lastWithdrawalRequestIdToFinalize: 0, - finalizationShareRate: 0, + sharesRequestedToBurn: 0, + withdrawalFinalizationBatches: [], + simulatedShareRate: 0, isBunkerMode: false, extraDataFormat: 0, extraDataHash: ZERO_HASH, @@ -64,48 +62,44 @@ contract('AccountingOracle', ([voting, malicious1, malicious2, member1, member2, consensus = deployed.consensus oracle = deployed.oracle - await initAccountingOracle({ ...deployed, admin: voting }) - - assert.equals(await oracle.getTime(), GENESIS_TIME + SECONDS_PER_FRAME) + await configureAccountingOracleSetup({ ...deployed, admin: voting }) + await consensus.setTime(await getInitialFrameStartTime(consensus)) await consensus.addMember(member1, 4, { from: voting }) await consensus.addMember(member2, 4, { from: voting }) }) - it('reverts with zero ref. slot', async () => { - assert.equals((await consensus.getCurrentFrame()).refSlot, 1 * SLOTS_PER_FRAME - 1) - await assert.reverts(consensus.submitReport(0, HASH_1, CONSENSUS_VERSION, { from: member1 }), 'InvalidSlot()') - }) + it('oracle contract handles changing the oracles during epoch', async () => { + const { refSlot } = await consensus.getCurrentFrame() - it('oracle conract handles changing the oracles during epoch', async () => { await consensus.addMember(malicious1, 4, { from: voting }) await consensus.addMember(malicious2, 4, { from: voting }) - const goodDataItems = getReportDataItems({ ...GOOD_DATA, refSlot: SLOTS_PER_FRAME - 1 }) - const badDataItems = getReportDataItems({ ...BAD_DATA, refSlot: SLOTS_PER_FRAME - 1 }) - const goodDataHash = calcReportDataHash(goodDataItems) - const badDataHash = calcReportDataHash(badDataItems) + const goodDataItems = getAccountingReportDataItems({ ...GOOD_DATA, refSlot }) + const badDataItems = getAccountingReportDataItems({ ...BAD_DATA, refSlot }) + const goodDataHash = calcAccountingReportDataHash(goodDataItems) + const badDataHash = calcAccountingReportDataHash(badDataItems) - await consensus.submitReport(SLOTS_PER_FRAME - 1, badDataHash, CONSENSUS_VERSION, { from: malicious1 }) - await consensus.submitReport(SLOTS_PER_FRAME - 1, badDataHash, CONSENSUS_VERSION, { from: malicious2 }) - await consensus.submitReport(SLOTS_PER_FRAME - 1, goodDataHash, CONSENSUS_VERSION, { from: member1 }) - await consensus.submitReport(SLOTS_PER_FRAME - 1, goodDataHash, CONSENSUS_VERSION, { from: member2 }) + await consensus.submitReport(refSlot, badDataHash, CONSENSUS_VERSION, { from: malicious1 }) + await consensus.submitReport(refSlot, badDataHash, CONSENSUS_VERSION, { from: malicious2 }) + await consensus.submitReport(refSlot, goodDataHash, CONSENSUS_VERSION, { from: member1 }) + await consensus.submitReport(refSlot, goodDataHash, CONSENSUS_VERSION, { from: member2 }) await consensus.removeMember(malicious1, 3, { from: voting }) await consensus.removeMember(malicious2, 3, { from: voting }) await consensus.addMember(member3, 3, { from: voting }) - let tx = await consensus.submitReport(SLOTS_PER_FRAME - 1, goodDataHash, CONSENSUS_VERSION, { from: member3 }) + let tx = await consensus.submitReport(refSlot, goodDataHash, CONSENSUS_VERSION, { from: member3 }) assert.emits(tx, 'ConsensusReached', { - refSlot: SLOTS_PER_FRAME - 1, + refSlot: +refSlot, report: goodDataHash, support: 3, }) tx = await oracle.submitReportData(goodDataItems, await oracle.getContractVersion(), { from: member3 }) - assert.emits(tx, 'ProcessingStarted', { refSlot: SLOTS_PER_FRAME - 1 }) + assert.emits(tx, 'ProcessingStarted', { refSlot: +refSlot }) const lastHandleOracleReportCall = await lido.getLastCall_handleOracleReport() assert.equals(lastHandleOracleReportCall.clBalance, e9(GOOD_DATA.clBalanceGwei)) diff --git a/test/scenario/deposit.test.js b/test/scenario/deposit.test.js index eb4e6e702..56f4ae7f3 100644 --- a/test/scenario/deposit.test.js +++ b/test/scenario/deposit.test.js @@ -52,7 +52,6 @@ contract('Lido with official deposit contract', ([user1, user2, user3, nobody, d depositContractFactory, postSetup: async ({ pool, lidoLocator, eip712StETH, withdrawalQueue, appManager, voting }) => { await pool.initialize(lidoLocator.address, eip712StETH.address, { value: ETH(1) }) - await withdrawalQueue.updateBunkerMode(false, 0, { from: appManager.address }) await pool.resumeProtocolAndStaking({ from: voting.address }) }, }) 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 951372ef2..fcef6e3d8 100644 --- a/test/scenario/execution_layer_rewards_after_the_merge.test.js +++ b/test/scenario/execution_layer_rewards_after_the_merge.test.js @@ -3,18 +3,31 @@ const { assert } = require('../helpers/assert') const { BN } = require('bn.js') const { getEventArgument } = require('@aragon/contract-helpers-test') -const { gwei, ZERO_HASH, ethToGwei, pad, toBN, ETH, tokens } = require('../helpers/utils') +const { gwei, ZERO_HASH, ethToGwei, pad, toBN, ETH, tokens, limitRebase } = require('../helpers/utils') const { DSMAttestMessage, DSMPauseMessage } = require('../helpers/signatures') -const { waitBlocks, setBalance } = require('../helpers/blockchain') +const { waitBlocks, setBalance, advanceChainTime } = require('../helpers/blockchain') const { deployProtocol } = require('../helpers/protocol') const { setupNodeOperatorsRegistry } = require('../helpers/staking-modules') -const { SLOTS_PER_FRAME } = require('../helpers/constants') +const { calcAccountingReportDataHash, getAccountingReportDataItems } = require('../helpers/reportData') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') const TOTAL_BASIS_POINTS = 10 ** 4 -const MAX_POSITIVE_REBASE_PRECISION_POINTS = 10 ** 9 const CURATED_MODULE_ID = 1 +const LIDO_INIT_BALANCE_ETH = 1 +const ONE_DAY = 1 * 24 * 60 * 60 + +const ORACLE_REPORT_LIMITS_BOILERPLATE = { + churnValidatorsPerDayLimit: 255, + oneOffCLBalanceDecreaseBPLimit: 100, + annualBalanceIncreaseBPLimit: 10000, + simulatedShareRateDeviationBPLimit: 10000, + maxValidatorExitRequestsPerReport: 10000, + maxAccountingExtraDataListItemsCount: 10000, + maxNodeOperatorsPerExtraDataItemCount: 10000, + requestTimestampMargin: 0, + maxPositiveTokenRebase: 1000000000, +} const makeAccountingReport = ({ refSlot, numValidators, clBalanceGwei, elRewardsVaultBalance }) => ({ refSlot, @@ -25,15 +38,16 @@ const makeAccountingReport = ({ refSlot, numValidators, clBalanceGwei, elRewards numExitedValidatorsByStakingModule: [], withdrawalVaultBalance: 0, elRewardsVaultBalance, - lastWithdrawalRequestIdToFinalize: 0, - finalizationShareRate: 0, + sharesRequestedToBurn: 0, + withdrawalFinalizationBatches: [], + simulatedShareRate: 0, isBunkerMode: false, extraDataFormat: 0, extraDataHash: ZERO_HASH, extraDataItemsCount: 0, }) -contract.skip('Lido: merge acceptance', (addresses) => { +contract('Lido: merge acceptance', (addresses) => { const [ // node operators operator_1, @@ -46,11 +60,12 @@ contract.skip('Lido: merge acceptance', (addresses) => { nobody, ] = addresses - let pool, nodeOperatorsRegistry, token + let pool, nodeOperatorsRegistry, token, oracleReportSanityChecker let oracleMock, depositContractMock let treasuryAddr, guardians, stakingRouter let depositSecurityModule, depositRoot - let elRewardsVault, voting + let elRewardsVault, voting, signers + let consensus // Total fee is 1% const totalFeePoints = 0.01 * TOTAL_BASIS_POINTS @@ -105,6 +120,9 @@ contract.skip('Lido: merge acceptance', (addresses) => { // contracts/Lido.sol pool = deployed.pool + // contracts/OracleReportSanityChecker.sol + oracleReportSanityChecker = deployed.oracleReportSanityChecker + // contracts/nos/NodeOperatorsRegistry.sol nodeOperatorsRegistry = deployed.stakingModules[0] @@ -115,12 +133,16 @@ contract.skip('Lido: merge acceptance', (addresses) => { oracleMock = deployed.oracle depositContractMock = deployed.depositContract + // consensus members + signers = deployed.signers + // addresses treasuryAddr = deployed.treasury.address depositSecurityModule = deployed.depositSecurityModule guardians = deployed.guardians elRewardsVault = deployed.elRewardsVault voting = deployed.voting.address + consensus = deployed.consensusContract depositRoot = await depositContractMock.get_deposit_root() @@ -161,12 +183,10 @@ contract.skip('Lido: merge acceptance', (addresses) => { await nodeOperatorsRegistry.setNodeOperatorStakingLimit(0, validatorsLimit, { from: voting }) // The key was added - let totalKeys = await nodeOperatorsRegistry.getTotalSigningKeyCount(nodeOperator1.id, { from: nobody }) assert.equals(totalKeys, 1, 'total signing keys') // The key was not used yet - let unusedKeys = await nodeOperatorsRegistry.getUnusedSigningKeyCount(nodeOperator1.id, { from: nobody }) assert.equals(unusedKeys, 1, 'unused signing keys') @@ -193,7 +213,6 @@ contract.skip('Lido: merge acceptance', (addresses) => { await nodeOperatorsRegistry.setNodeOperatorStakingLimit(1, validatorsLimit, { from: voting }) // The key was added - totalKeys = await nodeOperatorsRegistry.getTotalSigningKeyCount(nodeOperator2.id, { from: nobody }) assert.equals(totalKeys, 1, 'total signing keys') @@ -232,7 +251,6 @@ contract.skip('Lido: merge acceptance', (addresses) => { ) // No Ether was deposited yet to the validator contract - assert.equals(await depositContractMock.totalCalls(), 0) const ether2Stat = await pool.getBeaconStat() @@ -241,14 +259,16 @@ contract.skip('Lido: merge acceptance', (addresses) => { // All Ether was buffered within the pool contract atm - assert.equals(await pool.getBufferedEther(), ETH(3), 'buffered ether') - assert.equals(await pool.getTotalPooledEther(), ETH(3), 'total pooled ether') + // The contract's balance must be non-zero. When the contract is deployed, + // it receives LIDO_INIT_BALANCE_ETH ETH in deployProtocol() function. - // The amount of tokens corresponding to the deposited ETH value was minted to the user + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 3), 'buffered ether') + assert.equals(await pool.getTotalPooledEther(), ETH(LIDO_INIT_BALANCE_ETH + 3), 'total pooled ether') + // The amount of tokens corresponding to the deposited ETH value was minted to the user assert.equals(await token.balanceOf(user1), tokens(3), 'user1 tokens') - assert.equals(await token.totalSupply(), tokens(3), 'token total supply') + assert.equals(await token.totalSupply(), tokens(LIDO_INIT_BALANCE_ETH + 3), 'token total supply') }) it('the second user deposits 30 ETH to the pool', async () => { @@ -297,15 +317,18 @@ contract.skip('Lido: merge acceptance', (addresses) => { // Some Ether remained buffered within the pool contract - assert.equals(await pool.getBufferedEther(), ETH(1), 'buffered ether') - assert.equals(await pool.getTotalPooledEther(), ETH(1 + 32), 'total pooled ether') + // The contract's balance must be non-zero. When the contract is deployed, + // it receives LIDO_INIT_BALANCE_ETH ETH in deployProtocol() function. + + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 1), 'buffered ether') + assert.equals(await pool.getTotalPooledEther(), ETH(LIDO_INIT_BALANCE_ETH + 1 + 32), 'total pooled ether') // The amount of tokens corresponding to the deposited ETH value was minted to the users assert.equals(await token.balanceOf(user1), tokens(3), 'user1 tokens') assert.equals(await token.balanceOf(user2), tokens(30), 'user2 tokens') - assert.equals(await token.totalSupply(), tokens(3 + 30), 'token total supply') + assert.equals(await token.totalSupply(), tokens(LIDO_INIT_BALANCE_ETH + 3 + 30), 'token total supply') }) it('the third user deposits 64 ETH to the pool', async () => { @@ -358,8 +381,11 @@ contract.skip('Lido: merge acceptance', (addresses) => { // The pool ran out of validator keys, so the remaining 32 ETH were added to the // pool buffer - assert.equals(await pool.getBufferedEther(), ETH(1 + 32), 'buffered ether') - assert.equals(await pool.getTotalPooledEther(), ETH(33 + 64), 'total pooled ether') + // The contract's balance must be non-zero. When the contract is deployed, + // it receives LIDO_INIT_BALANCE_ETH ETH in deployProtocol() function. + + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 1 + 32), 'buffered ether') + assert.equals(await pool.getTotalPooledEther(), ETH(LIDO_INIT_BALANCE_ETH + 3 + 30 + 64), 'total pooled ether') // The amount of tokens corresponding to the deposited ETH value was minted to the users @@ -367,7 +393,7 @@ contract.skip('Lido: merge acceptance', (addresses) => { assert.equals(await token.balanceOf(user2), tokens(30), 'user2 tokens') assert.equals(await token.balanceOf(user3), tokens(64), 'user3 tokens') - assert.equals(await token.totalSupply(), tokens(3 + 30 + 64), 'token total supply') + assert.equals(await token.totalSupply(), tokens(LIDO_INIT_BALANCE_ETH + 3 + 30 + 64), 'token total supply') }) it('collect 9 ETH execution layer rewards to the vault', async () => { @@ -375,30 +401,34 @@ contract.skip('Lido: merge acceptance', (addresses) => { assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(9), 'Execution layer rewards vault balance') }) - it('the oracle reports balance increase on Ethereum2 side (+32 ETH) and claims collected execution layer rewards (+9 ETH)', async () => { - const frame = 1 - + it('the oracle reports balance increase on Ethereum2 side (+0.35 ETH) and claims collected execution layer rewards (+9 ETH)', async () => { // Total shares are equal to deposited eth before ratio change and fee mint const oldTotalShares = await token.getTotalShares() - assert.equals(oldTotalShares, ETH(97), 'total shares') + assert.equals(oldTotalShares, ETH(LIDO_INIT_BALANCE_ETH + 97), 'total shares') // Old total pooled Ether const oldTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(oldTotalPooledEther, ETH(33 + 64), 'total pooled ether') + assert.equals(oldTotalPooledEther, ETH(LIDO_INIT_BALANCE_ETH + 3 + 30 + 64), 'total pooled ether') + + // Reporting balance increase (64 => 64.35) - // Reporting 1.5-fold balance increase (64 => 96) + const { refSlot } = await consensus.getCurrentFrame() - await oracleMock.submitReportData( + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot: +refSlot, numValidators: 2, - clBalanceGwei: gwei(96), + clBalanceGwei: gwei(64.35), elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + }) ) + const reportHash = calcAccountingReportDataHash(reportItems) + + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) // Execution layer rewards just claimed assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') @@ -407,51 +437,61 @@ contract.skip('Lido: merge acceptance', (addresses) => { // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) // // totalFee = 1000 (10%) - // reward = 41000000000000000000 - // oldTotalShares = 97000000000000000000 - // newTotalPooledEther = 138000000000000000000 - // shares2mint = int(41000000000000000000 * 1000 * 97000000000000000000 / (138000000000000000000 * 10000 - 1000 * 41000000000000000000 )) - // shares2mint ~= 2970126960418222592 + // reward = 9350000000000000000 + // oldTotalShares = 98000000000000000000 + // newTotalPooledEther = 107350000000000000000 + // shares2mint = int(9350000000000000000 * 1000 * 98000000000000000000 / (107350000000000000000 * 10000 - 1000 * 9350000000000000000 )) + // shares2mint ~= 861062820091152800 const newTotalShares = await token.getTotalShares() - assert.equals(newTotalShares, new BN('99970126960418222554'), 'total shares') + assert.equals(newTotalShares, new BN('98861062820091152563'), 'total shares') const elRewards = 9 // Total pooled Ether increased - const newTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(newTotalPooledEther, ETH(33 + 96 + elRewards), 'total pooled ether') + assert.equals(newTotalPooledEther, ETH(LIDO_INIT_BALANCE_ETH + 3 + 30 + 64.35 + elRewards), 'total pooled ether') // Ether2 stat reported by the pool changed correspondingly const ether2Stat = await pool.getBeaconStat() assert.equals(ether2Stat.depositedValidators, 2, 'deposited ether2') - assert.equals(ether2Stat.beaconBalance, ETH(96), 'remote ether2') + assert.equals(ether2Stat.beaconBalance, ETH(64.35), 'remote ether2') // Buffered Ether amount changed on execution layer rewards - assert.equals(await pool.getBufferedEther(), ETH(33 + elRewards), 'buffered ether') + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 33 + elRewards), 'buffered ether') // New tokens was minted to distribute fee - assert.equals(await token.totalSupply(), tokens(129 + elRewards), 'token total supply') + assert.equals(await token.totalSupply(), tokens(98.35 + elRewards), 'token total supply') - const reward = toBN(ETH(96 - 64 + elRewards)) + const reward = toBN(ETH(64.35 - 64 + elRewards)) const mintedAmount = new BN(totalFeePoints).mul(reward).divn(TOTAL_BASIS_POINTS) + // rewards = 9350000000000000000 + // user1 shares = 3 + // user2 shares = 30 + // user3 shares = 64 + + // user1 balance = ETH(3) + 9350000000000000000 * 0.9 * 3/98 = ~3257602040816326530 + // user2 balance = ETH(30) + 9350000000000000000 * 0.9 * 30/98 = ~32576020408163265306 + // user3 balance = ETH(64) + 9350000000000000000 * 0.9 * 64/98 = ~69495510204081632653 + // Token user balances increased - assert.equals(await token.balanceOf(user1), new BN('4141237113402061855'), 'user1 tokens') - assert.equals(await token.balanceOf(user2), new BN('41412371134020618556'), 'user2 tokens') - assert.equals(await token.balanceOf(user3), new BN('88346391752577319587'), 'user3 tokens') + assert.equals(await token.balanceOf(user1), new BN('3257602040816326530'), 'user1 tokens') + assert.equals(await token.balanceOf(user2), new BN('32576020408163265306'), 'user2 tokens') + assert.equals(await token.balanceOf(user3), new BN('69495510204081632653'), 'user3 tokens') // Fee, in the form of minted tokens, was distributed between treasury, insurance fund // and node operators // treasuryTokenBalance ~= mintedAmount * treasuryFeePoints / 10000 // insuranceTokenBalance ~= mintedAmount * insuranceFeePoints / 10000 - assert.equals(await token.balanceOf(treasuryAddr), new BN('2049999999999999999'), 'treasury tokens') + + // treasuryFeePoints = 500, insuranceFeePoints = 0 + assert.equals(await token.balanceOf(treasuryAddr), new BN('467500000000000000'), 'treasury tokens') // Module fee, rewards distribution between modules should be make by module - assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('2049999999999999999'), 'module1 tokens') + assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('467499999999999999'), 'module1 tokens') // Real minted amount should be a bit less than calculated caused by round errors on mint and transfer operations assert( @@ -464,46 +504,47 @@ contract.skip('Lido: merge acceptance', (addresses) => { }) it('collect another 7 ETH execution layer rewards to the vault', async () => { - const balanceBefore = await web3.eth.getBalance(elRewardsVault.address) - await setBalance(elRewardsVault.address, ETH(2)) - assert.equals( - await web3.eth.getBalance(elRewardsVault.address), - ETH(2) + balanceBefore, - 'Execution layer rewards vault balance' - ) + const balanceBefore = +(await web3.eth.getBalance(elRewardsVault.address)) + await setBalance(elRewardsVault.address, ETH(7)) - await setBalance(elRewardsVault.address, ETH(5)) assert.equals( await web3.eth.getBalance(elRewardsVault.address), - ETH(7) + balanceBefore, + +ETH(7) + balanceBefore, 'Execution layer rewards vault balance' ) }) it('the oracle reports same balance on Ethereum2 side (+0 ETH) and claims collected execution layer rewards (+7 ETH)', async () => { - const frame = 2 - // Total shares are equal to deposited eth before ratio change and fee mint const oldTotalShares = await token.getTotalShares() - assert.equals(oldTotalShares, new BN('99970126960418222554'), 'total shares') + assert.equals(oldTotalShares, new BN('98861062820091152563'), 'total shares') // Old total pooled Ether const oldTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(oldTotalPooledEther, ETH(138), 'total pooled ether') + 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) + + const { refSlot } = await consensus.getCurrentFrame() - // Reporting the same balance as it was before (96ETH => 96ETH) - await oracleMock.submitReportData( + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot: +refSlot, numValidators: 2, - clBalanceGwei: gwei(96), + clBalanceGwei: gwei(64.35), elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + }) ) + const reportHash = calcAccountingReportDataHash(reportItems) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) // Execution layer rewards just claimed + assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') // Total shares preserved because fee shares NOT minted @@ -515,28 +556,49 @@ contract.skip('Lido: merge acceptance', (addresses) => { // Total pooled Ether increased const newTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(newTotalPooledEther, ETH(138 + 7), 'total pooled ether') + assert.equals(newTotalPooledEther, ETH(107.35 + 7), 'total pooled ether') // Ether2 stat reported by the pool changed correspondingly const ether2Stat = await pool.getBeaconStat() assert.equals(ether2Stat.depositedValidators, 2, 'deposited ether2') - assert.equals(ether2Stat.beaconBalance, ETH(96), 'remote ether2') + assert.equals(ether2Stat.beaconBalance, ETH(64.35), 'remote ether2') // Buffered Ether amount changed on execution layer rewards - assert.equals(await pool.getBufferedEther(), ETH(42 + 7), 'buffered ether') + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 42 + 7), 'buffered ether') - assert.equals(await token.totalSupply(), tokens(145), 'token total supply') + assert.equals(await token.totalSupply(), tokens(114.35), 'token total supply') // All of the balances should be increased with proportion of newTotalPooledEther/oldTotalPooledEther (which is >1) // cause shares per user and overall shares number are preserved - assert.equals(await token.balanceOf(user1), new BN('4351299865531151949'), 'user1 tokens') - assert.equals(await token.balanceOf(user2), new BN('43512998655311519498'), 'user2 tokens') - assert.equals(await token.balanceOf(user3), new BN('92827730464664574929'), 'user3 tokens') + // oldTotalPooledEther = 107.35 + // newTotalPooledEther = 107.35 + 7 = 114.35 + // newTotalPooledEther/oldTotalPooledEther = 1.065207266 + // sharePrice = 1156673787819739000 + + // user1 balance = 3257602040816326530 * 1.065207266 = ~3470021363459216942 + // user1 balance = sharePrice * shares = 1156673787819739000 * 3 = ~3470021363459216942 - assert.equals(await token.balanceOf(treasuryAddr), new BN('2153985507246376811'), 'treasury tokens') - assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('2153985507246376811'), 'module1 tokens') + // user2 balance = 32576020408163265306 * 1.065207266 = ~34700213634592169424 + // user2 balance = sharePrice * shares = 1156673787819739000 * 30 = ~34700213634592169424 + + // user3 balance = 69495510204081632653 * 1.065207266 = ~74027122420463294773 + // user3 balance = sharePrice * shares = 1156673787819739000 * 64 = ~74027122420463294773 + + assert.equals(await token.balanceOf(user1), new BN('3470021363459216942'), 'user1 tokens') + assert.equals(await token.balanceOf(user2), new BN('34700213634592169424'), 'user2 tokens') + assert.equals(await token.balanceOf(user3), new BN('74027122420463294773'), 'user3 tokens') + + // treasuryTokenBalance = (oldTreasuryShares + mintedRewardShares * treasuryFeePoints / 10000) * sharePrice + + // oldTreasuryShares = 430531410045576260 + // mintedRewardShares = 0 + // sharePrice = 1156673787819739000 + // treasuryFeePoints = 500 + // treasuryTokenBalance = (43.0531410045576260 + (0 * 500) / 10000) * 1156673787819739000 = ~497984396832789939 + assert.equals(await token.balanceOf(treasuryAddr), new BN('497984396832789939'), 'treasury tokens') + assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('497984396832789938'), 'module1 tokens') // operators do not claim rewards from module assert.equals(await token.balanceOf(nodeOperator1.address), 0, 'operator_1 tokens') @@ -549,27 +611,33 @@ contract.skip('Lido: merge acceptance', (addresses) => { }) it('the oracle reports loss on Ethereum2 side (-2 ETH) and claims collected execution layer rewards (+5 ETH)', async () => { - const frame = 3 - // Total shares are equal to deposited eth before ratio change and fee mint const oldTotalShares = await token.getTotalShares() - assert.equals(oldTotalShares, new BN('99970126960418222554'), 'total shares') + assert.equals(oldTotalShares, new BN('98861062820091152563'), 'total shares') // Old total pooled Ether const oldTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(oldTotalPooledEther, ETH(145), 'total pooled ether') + assert.equals(oldTotalPooledEther, ETH(114.35), 'total pooled ether') - // Reporting balance decrease (96ETH => 94ETH) - await oracleMock.submitReportData( + // Reporting balance decrease (64.35ETH => 62.35ETH) + await advanceChainTime(ONE_DAY) + + const { refSlot } = await consensus.getCurrentFrame() + + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot: +refSlot, numValidators: 2, - clBalanceGwei: gwei(94), + clBalanceGwei: gwei(62.35), elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + }) ) + const reportHash = calcAccountingReportDataHash(reportItems) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) // Execution layer rewards just claimed assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') @@ -581,27 +649,49 @@ contract.skip('Lido: merge acceptance', (addresses) => { // Total pooled Ether increased by 5ETH - 2ETH const newTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(newTotalPooledEther, ETH(145 + 3), 'total pooled ether') + assert.equals(newTotalPooledEther, ETH(114.35 + 3), 'total pooled ether') // Ether2 stat reported by the pool changed correspondingly const ether2Stat = await pool.getBeaconStat() assert.equals(ether2Stat.depositedValidators, 2, 'deposited ether2') - assert.equals(ether2Stat.beaconBalance, ETH(94), 'remote ether2') + assert.equals(ether2Stat.beaconBalance, ETH(62.35), 'remote ether2') // Buffered Ether amount changed on execution layer rewards - assert.equals(await pool.getBufferedEther(), ETH(49 + 5), 'buffered ether') + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 49 + 5), 'buffered ether') - assert.equals(await token.totalSupply(), tokens(145 + 3), 'token total supply') + assert.equals(await token.totalSupply(), tokens(114.35 + 3), 'token total supply') // All of the balances should be increased with proportion of newTotalPooledEther/oldTotalPooledEther (which is >1) // cause shares per user and overall shares number are preserved - assert.equals(await token.balanceOf(user1), new BN('4441326759300761990'), 'user1 tokens') - assert.equals(await token.balanceOf(user2), new BN('44413267593007619901'), 'user2 tokens') - assert.equals(await token.balanceOf(user3), new BN('94748304198416255789'), 'user3 tokens') + // oldTotalPooledEther = 114.35 + // newTotalPooledEther = 114.35 + 3 = 117.35 + // newTotalPooledEther/oldTotalPooledEther = 1,0262352427 + // sharePrice = 1187019405340151800 + + // user1 balance = 3470021363459216942 * 1,0262352427 = ~3561058216020455690 + // user1 balance = sharePrice * shares = 1187019405340151800 * 3 = ~3561058216020455690 + + // user2 balance = 34700213634592169424 * 1,0262352427 = ~35610582160204556904 + // user2 balance = sharePrice * shares = 1187019405340151800 * 30 = ~35610582160204556904 + + // user3 balance = 74027122420463294773 * 1,0262352427 = ~75969241941769721395 + // user3 balance = sharePrice * shares = 1187019405340151800 * 64 = ~75969241941769721395 - assert.equals(await token.balanceOf(treasuryAddr), new BN('2198550724637681159'), 'treasury tokens') - assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('2198550724637681159'), 'module1 tokens') + assert.equals(await token.balanceOf(user1), new BN('3561058216020455690'), 'user1 tokens') + assert.equals(await token.balanceOf(user2), new BN('35610582160204556904'), 'user2 tokens') + assert.equals(await token.balanceOf(user3), new BN('75969241941769721395'), 'user3 tokens') + + // treasuryTokenBalance = (oldTreasuryShares + mintedRewardShares * treasuryFeePoints / 10000) * sharePrice + + // oldTreasuryShares = 430531410045576260 + // mintedRewardShares = 0 + // sharePrice = 1187019405340151800 + // treasuryFeePoints = 500 + // treasuryTokenBalance = (43.0531410045576260 + (0 * 500) / 10000) * 1187019405340151800 = ~511049138332557056 + + assert.equals(await token.balanceOf(treasuryAddr), new BN('511049138332557056'), 'treasury tokens') + assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('511049138332557055'), 'module1 tokens') }) it('collect another 3 ETH execution layer rewards to the vault', async () => { @@ -610,27 +700,32 @@ contract.skip('Lido: merge acceptance', (addresses) => { }) it('the oracle reports loss on Ethereum2 side (-3 ETH) and claims collected execution layer rewards (+3 ETH)', async () => { - const frame = 4 - // Total shares are equal to deposited eth before ratio change and fee mint const oldTotalShares = await token.getTotalShares() - assert.equals(oldTotalShares, new BN('99970126960418222554'), 'total shares') + assert.equals(oldTotalShares, new BN('98861062820091152563'), 'total shares') // Old total pooled Ether - const oldTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(oldTotalPooledEther, ETH(148), 'total pooled ether') + assert.equals(oldTotalPooledEther, ETH(117.35), 'total pooled ether') + + // Reporting balance decrease (62.35ETH => 59.35ETH) + await advanceChainTime(ONE_DAY) + + const { refSlot } = await consensus.getCurrentFrame() - // Reporting balance decrease (94ETH => 91ETH) - await oracleMock.submitReportData( + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot: +refSlot, numValidators: 2, - clBalanceGwei: gwei(91), + clBalanceGwei: gwei(59.35), elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + }) ) + const reportHash = calcAccountingReportDataHash(reportItems) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) // Execution layer rewards just claimed assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') @@ -640,27 +735,31 @@ contract.skip('Lido: merge acceptance', (addresses) => { const newTotalShares = await token.getTotalShares() assert.equals(newTotalShares, oldTotalShares, 'total shares') - // Total pooled Ether increased by 5ETH - 2ETH const newTotalPooledEther = await pool.getTotalPooledEther() assert.equals(newTotalPooledEther, oldTotalPooledEther, 'total pooled ether') // Ether2 stat reported by the pool changed correspondingly const ether2Stat = await pool.getBeaconStat() assert.equals(ether2Stat.depositedValidators, 2, 'deposited ether2') - assert.equals(ether2Stat.beaconBalance, ETH(91), 'remote ether2') + assert.equals(ether2Stat.beaconBalance, ETH(59.35), 'remote ether2') // Buffered Ether amount changed on execution layer rewards - assert.equals(await pool.getBufferedEther(), ETH(54 + 3), 'buffered ether') + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 54 + 3), 'buffered ether') + + assert.equals(await token.totalSupply(), tokens(117.35), 'token total supply') - assert.equals(await token.totalSupply(), tokens(148), 'token total supply') + // oldTotalPooledEther = 117.35 + // newTotalPooledEther = 117.35 + // newTotalPooledEther/oldTotalPooledEther = 1 + // sharePrice = 1187019405340151800 // All of the balances should be the same as before cause overall changes sums to zero - assert.equals(await token.balanceOf(user1), new BN('4441326759300761990'), 'user1 tokens') - assert.equals(await token.balanceOf(user2), new BN('44413267593007619901'), 'user2 tokens') - assert.equals(await token.balanceOf(user3), new BN('94748304198416255789'), 'user3 tokens') + assert.equals(await token.balanceOf(user1), new BN('3561058216020455690'), 'user1 tokens') + assert.equals(await token.balanceOf(user2), new BN('35610582160204556904'), 'user2 tokens') + assert.equals(await token.balanceOf(user3), new BN('75969241941769721395'), 'user3 tokens') - assert.equals(await token.balanceOf(treasuryAddr), new BN('2198550724637681159'), 'treasury tokens') - assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('2198550724637681159'), 'module1 tokens') + assert.equals(await token.balanceOf(treasuryAddr), new BN('511049138332557056'), 'treasury tokens') + assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('511049138332557055'), 'module1 tokens') }) it('collect another 2 ETH execution layer rewards to the vault', async () => { @@ -669,27 +768,34 @@ contract.skip('Lido: merge acceptance', (addresses) => { }) it('the oracle reports loss on Ethereum2 side (-8 ETH) and claims collected execution layer rewards (+2 ETH)', async () => { - const frame = 5 - // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() - assert.equals(oldTotalShares, new BN('99970126960418222554'), 'total shares') + assert.equals(oldTotalShares, new BN('98861062820091152563'), 'total shares') // Old total pooled Ether const oldTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(oldTotalPooledEther, ETH(148), 'total pooled ether') + assert.equals(oldTotalPooledEther, ETH(117.35), 'total pooled ether') + + // Reporting balance decrease (59.35ETH => 51.35ETH) + await advanceChainTime(ONE_DAY) - // Reporting balance decrease (91ETH => 83ETH) - await oracleMock.submitReportData( + const { refSlot } = await consensus.getCurrentFrame() + + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot: +refSlot, numValidators: 2, - clBalanceGwei: gwei(83), + clBalanceGwei: gwei(51.35), elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + }) ) + const reportHash = calcAccountingReportDataHash(reportItems) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) // Execution layer rewards just claimed assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') @@ -701,26 +807,48 @@ contract.skip('Lido: merge acceptance', (addresses) => { // Total pooled Ether decreased by 8ETH-2ETH const newTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(newTotalPooledEther, ETH(142), 'total pooled ether') + assert.equals(newTotalPooledEther, ETH(111.35), 'total pooled ether') // Ether2 stat reported by the pool changed correspondingly const ether2Stat = await pool.getBeaconStat() assert.equals(ether2Stat.depositedValidators, 2, 'deposited ether2') - assert.equals(ether2Stat.beaconBalance, ETH(83), 'remote ether2') + assert.equals(ether2Stat.beaconBalance, ETH(51.35), 'remote ether2') // Buffered Ether amount changed on execution layer rewards - assert.equals(await pool.getBufferedEther(), ETH(57 + 2), 'buffered ether') + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 57 + 2), 'buffered ether') + + assert.equals(await token.totalSupply(), tokens(111.35), 'token total supply') - assert.equals(await token.totalSupply(), tokens(142), 'token total supply') + // oldTotalPooledEther = 117.35 + // newTotalPooledEther = 117.35 - 6 = 111.35 + // newTotalPooledEther/oldTotalPooledEther = 0,948870899 + // sharePrice = 1126328170299326100 + + // user1 balance = 3561058216020455690 * 0,948870899 = ~3378984510897978194 + // user1 balance = sharePrice * shares = 1126328170299326100 * 3 = ~3378984510897978194 + + // user2 balance = 35610582160204556904 * 0,948870899 = ~33789845108979781945 + // user2 balance = sharePrice * shares = 1126328170299326100 * 30 = ~33789845108979781945 + + // user3 balance = 75969241941769721395 * 0,948870899 = ~72085002899156868150 + // user3 balance = sharePrice * shares = 1126328170299326100 * 64 = ~72085002899156868150 // All of the balances should be decreased with proportion of newTotalPooledEther/oldTotalPooledEther (which is <1) // cause shares per user and overall shares number are preserved - assert.equals(await token.balanceOf(user1), new BN('4261272971761541909'), 'user1 tokens') - assert.equals(await token.balanceOf(user2), new BN('42612729717615419094'), 'user2 tokens') - assert.equals(await token.balanceOf(user3), new BN('90907156730912894068'), 'user3 tokens') + assert.equals(await token.balanceOf(user1), new BN('3378984510897978194'), 'user1 tokens') + assert.equals(await token.balanceOf(user2), new BN('33789845108979781945'), 'user2 tokens') + assert.equals(await token.balanceOf(user3), new BN('72085002899156868150'), 'user3 tokens') + + // treasuryTokenBalance = (oldTreasuryShares + mintedRewardShares * treasuryFeePoints / 10000) * sharePrice + + // oldTreasuryShares = 430531410045576260 + // mintedRewardShares = 0 + // sharePrice = 1126328170299326100 + // treasuryFeePoints = 500 + // treasuryTokenBalance = (43.0531410045576260 + (0 * 500) / 10000) * 1126328170299326100 = ~484919655333022823 - assert.equals(await token.balanceOf(treasuryAddr), new BN('2109420289855072463'), 'treasury tokens') - assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('2109420289855072463'), 'module1 tokens') + assert.equals(await token.balanceOf(treasuryAddr), new BN('484919655333022823'), 'treasury tokens') + assert.equals(await token.balanceOf(nodeOperatorsRegistry.address), new BN('484919655333022821'), 'module1 tokens') assert.equals(await token.balanceOf(nodeOperator1.address), 0, 'operator_1 tokens') assert.equals(await token.balanceOf(nodeOperator2.address), 0, 'operator_2 tokens') }) @@ -730,154 +858,181 @@ contract.skip('Lido: merge acceptance', (addresses) => { assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(3), 'Execution layer vault balance') }) - it('the oracle reports balance increase on Ethereum2 side (+2 ETH) and claims collected execution layer rewards (+3 ETH)', async () => { - const frame = 6 - + it('the oracle reports balance increase on Ethereum2 side (+0.14 ETH) and claims collected execution layer rewards (+3 ETH)', async () => { // Total shares are equal to deposited eth before ratio change and fee mint + const oldTotalShares = await token.getTotalShares() - assert.equals(oldTotalShares, new BN('99970126960418222554'), 'total shares') + assert.equals(oldTotalShares, new BN('98861062820091152563'), 'total shares') // Old total pooled Ether const oldTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(oldTotalPooledEther, ETH(142), 'total pooled ether') + assert.equals(oldTotalPooledEther, ETH(111.35), 'total pooled ether') + + // Reporting balance increase (51.35ETH => 51.49ETH) + await advanceChainTime(ONE_DAY) - // Reporting balance increase (83ETH => 85ETH) - await oracleMock.submitReportData( + const { refSlot } = await consensus.getCurrentFrame() + + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot: +refSlot, numValidators: 2, - clBalanceGwei: gwei(85), + clBalanceGwei: gwei(51.49), elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + }) ) + const reportHash = calcAccountingReportDataHash(reportItems) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) // Execution layer rewards just claimed + assert.equals(await web3.eth.getBalance(elRewardsVault.address), ETH(0), 'Execution layer rewards vault balance') // Total shares increased because fee minted (fee shares added) // shares ~= oldTotalShares + reward * oldTotalShares / (newTotalPooledEther - reward) + // + // totalFee = 1000 (10%) + // reward = 3140000000000000000 + // oldTotalShares = 98861062820091152563 + // newTotalPooledEther = 114490000000000000000 + // shares2mint = int(3140000000000000000 * 1000 * 98861062820091152563 / (114490000000000000000 * 10000 - 1000 * 3140000000000000000 )) + // shares2mint ~= 271881776603740030 + // newTotalShares = oldTotalShares + shares2mint = 98861062820091152563 + 271881776603740030 ~= 99132944596694892595 const newTotalShares = await token.getTotalShares() - assert.equals(newTotalShares, new BN('100311321932979376897'), 'total shares') + assert.equals(newTotalShares, new BN('99132944596694892595'), 'total shares') - // Total pooled Ether increased by 2ETH+3ETH + // Total pooled Ether increased by 0.14ETH+3ETH const newTotalPooledEther = await pool.getTotalPooledEther() - assert.equals(newTotalPooledEther, ETH(142 + 5), 'total pooled ether') + assert.equals(newTotalPooledEther, ETH(111.49 + 3), 'total pooled ether') // Ether2 stat reported by the pool changed correspondingly const ether2Stat = await pool.getBeaconStat() assert.equals(ether2Stat.depositedValidators, 2, 'deposited ether2') - assert.equals(ether2Stat.beaconBalance, ETH(85), 'remote ether2') + assert.equals(ether2Stat.beaconBalance, ETH(51.49), 'remote ether2') // Buffered Ether amount changed on execution layer rewards - assert.equals(await pool.getBufferedEther(), ETH(59 + 3), 'buffered ether') + assert.equals(await pool.getBufferedEther(), ETH(LIDO_INIT_BALANCE_ETH + 59 + 3), 'buffered ether') - assert.equals(await token.totalSupply(), tokens(142 + 5), 'token total supply') + assert.equals(await token.totalSupply(), tokens(111.49 + 3), 'token total supply') + + // newTotalPooledEther/oldTotalPooledEther = 1.0281993714 + // sharePrice = 1154913742003555000 + + // user1 balance = sharePrice * shares = 1154913742003555000 * 3 = ~3464741226010665095 + // user2 balance = sharePrice * shares = 1154913742003555000 * 30 = ~34647412260106650951 + // user3 balance = sharePrice * shares = 1154913742003555000 * 64 = ~73914479488227522028 // Token user balances increased - assert.equals(await token.balanceOf(user1), new BN('4396313312415956969'), 'user1 tokens') - assert.equals(await token.balanceOf(user2), new BN('43963133124159569699'), 'user2 tokens') - assert.equals(await token.balanceOf(user3), new BN('93788017331540415359'), 'user3 tokens') + assert.equals(await token.balanceOf(user1), new BN('3464741226010665095'), 'user1 tokens') + assert.equals(await token.balanceOf(user2), new BN('34647412260106650951'), 'user2 tokens') + assert.equals(await token.balanceOf(user3), new BN('73914479488227522028'), 'user3 tokens') // Fee, in the form of minted tokens, was distributed between treasury, insurance fund // and node operators // treasuryTokenBalance = (oldTreasuryShares + mintedRewardShares * treasuryFeePoints / 10000) * sharePrice - assert.equals((await token.balanceOf(treasuryAddr)).divn(10), new BN('242626811594202898'), 'treasury tokens') + + // oldTreasuryShares = 566472298347446300 + // mintedRewardShares = 0 + // sharePrice = 1154913742003555000 + // treasuryFeePoints = 500 + // treasuryTokenBalance = (56.6472298347446300 + (0 * 500) / 10000) * 1154913742003555000 = ~65422664182580344 + + assert.equals((await token.balanceOf(treasuryAddr)).divn(10), new BN('65422664182580344'), 'treasury tokens') assert.equals( (await token.balanceOf(nodeOperatorsRegistry.address)).divn(10), - new BN('242626811594202898'), + new BN('65422664182580344'), 'module1 tokens' ) }) - // TODO: Revive - it.skip('collect 0.1 ETH execution layer rewards to elRewardsVault and withdraw it entirely by means of multiple oracle reports (+1 ETH)', async () => { - // Specify different withdrawal limits for a few epochs to test different values - const getMaxPositiveRebaseForFrame = (_frame) => { - let ret = 0 - - if (_frame === 7) { - ret = toBN(2) - } else if (_frame === 8) { - ret = toBN(1) - } else { - ret = toBN(3) - } + it('collect execution layer rewards to elRewardsVault and withdraw it entirely by means of multiple oracle reports', async () => { + const tokenRebaseLimit = toBN(10000000) - return ret.mul(toBN(MAX_POSITIVE_REBASE_PRECISION_POINTS / TOTAL_BASIS_POINTS)) - } + await oracleReportSanityChecker.setOracleReportLimits( + { + ...ORACLE_REPORT_LIMITS_BOILERPLATE, + maxPositiveTokenRebase: tokenRebaseLimit.toString(), // 1% + }, + { from: voting, gasPrice: 1 } + ) - const elRewards = ETH(0.1) + const elRewards = ETH(5) await setBalance(elRewardsVault.address, elRewards) assert.equals(await web3.eth.getBalance(elRewardsVault.address), elRewards, 'Execution layer rewards vault balance') let frame = 7 - let lastBeaconBalance = toBN(ETH(85)) - await pool.setMaxPositiveTokenRebase(getMaxPositiveRebaseForFrame(frame), { from: voting }) + let lastBeaconBalance = toBN(ETH(51.49)) - let maxPositiveRebase = await pool.getMaxPositiveTokenRebase() - let elRewardsVaultBalance = toBN(await web3.eth.getBalance(elRewardsVault.address)) + let elRewardsVaultBalance = toBN(elRewards) let totalPooledEther = await pool.getTotalPooledEther() + let totalShares = await pool.getTotalShares() let bufferedEther = await pool.getBufferedEther() - let totalSupply = await pool.totalSupply() - const beaconBalanceInc = toBN(ETH(0.001)) let elRewardsWithdrawn = toBN(0) + const beaconBalanceInc = toBN(ETH(0.001)) // Do multiple oracle reports to withdraw all ETH from execution layer rewards vault while (elRewardsVaultBalance > 0) { - const maxPositiveRebaseCalculated = getMaxPositiveRebaseForFrame(frame) - await pool.setMaxPositiveTokenRebase(maxPositiveRebaseCalculated, { from: voting }) - maxPositiveRebase = await pool.getMaxPositiveTokenRebase() - const clIncurredRebase = beaconBalanceInc.mul(toBN(MAX_POSITIVE_REBASE_PRECISION_POINTS)).div(totalPooledEther) + await advanceChainTime(ONE_DAY) - const maxELRewardsAmountPerWithdrawal = totalPooledEther - .mul(maxPositiveRebase.sub(clIncurredRebase)) - .div(toBN(MAX_POSITIVE_REBASE_PRECISION_POINTS)) + const currentELBalance = await web3.eth.getBalance(elRewardsVault.address) - const elRewardsToWithdraw = BN.min(maxELRewardsAmountPerWithdrawal, elRewardsVaultBalance) - - // Reporting balance increase - await oracleMock.submitReportData( + const { refSlot } = await consensus.getCurrentFrame() + const reportItems = getAccountingReportDataItems( makeAccountingReport({ - refSlot: frame * SLOTS_PER_FRAME - 1, + refSlot, numValidators: 2, clBalanceGwei: ethToGwei(lastBeaconBalance.add(beaconBalanceInc)), - elRewardsVaultBalance: await web3.eth.getBalance(elRewardsVault.address), - }), - 1 + elRewardsVaultBalance: currentELBalance, + }) + ) + const reportHash = calcAccountingReportDataHash(reportItems) + + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[2].address }) + await consensus.submitReport(refSlot, reportHash, 1, { from: signers[3].address }) + + await oracleMock.submitReportData(reportItems, 1, { from: signers[4].address }) + + const { elBalanceUpdate } = limitRebase( + toBN(tokenRebaseLimit), + totalPooledEther, + totalShares, + beaconBalanceInc, + toBN(currentELBalance), + toBN(0) ) assert.equals( await web3.eth.getBalance(elRewardsVault.address), - elRewardsVaultBalance.sub(elRewardsToWithdraw), + elRewardsVaultBalance.sub(toBN(elBalanceUpdate)), 'Execution layer rewards vault balance' ) assert.equals( await pool.getTotalPooledEther(), - totalPooledEther.add(beaconBalanceInc).add(elRewardsToWithdraw), + totalPooledEther.add(beaconBalanceInc).add(elBalanceUpdate), 'total pooled ether' ) - assert.equals( - await pool.totalSupply(), - totalSupply.add(beaconBalanceInc).add(elRewardsToWithdraw), - 'token total supply' - ) - assert.equals(await pool.getBufferedEther(), bufferedEther.add(elRewardsToWithdraw), 'buffered ether') + + assert.equals(await pool.getBufferedEther(), bufferedEther.add(elBalanceUpdate), 'buffered ether') elRewardsVaultBalance = toBN(await web3.eth.getBalance(elRewardsVault.address)) totalPooledEther = await pool.getTotalPooledEther() + totalShares = await pool.getTotalShares() bufferedEther = await pool.getBufferedEther() - totalSupply = await pool.totalSupply() lastBeaconBalance = lastBeaconBalance.add(beaconBalanceInc) - elRewardsWithdrawn = elRewardsWithdrawn.add(elRewardsToWithdraw) + elRewardsWithdrawn = elRewardsWithdrawn.add(elBalanceUpdate) frame += 1 } assert.equals(elRewardsWithdrawn, elRewards) + assert.equals(elRewardsVaultBalance, toBN(0)) + assert.isTrue(frame > 10) }) }) diff --git a/test/scenario/lido-wq-acct-oracle-integration-gas.test.js b/test/scenario/lido-wq-acct-oracle-integration-gas.test.js new file mode 100644 index 000000000..1f8331003 --- /dev/null +++ b/test/scenario/lido-wq-acct-oracle-integration-gas.test.js @@ -0,0 +1,222 @@ +const { contract, artifacts } = require('hardhat') +const { BN } = require('bn.js') +const { assert } = require('../helpers/assert') +const { ZERO_ADDRESS } = require('../helpers/constants') +const { toBN, e9, e18, e27 } = require('../helpers/utils') +const { deployProtocol } = require('../helpers/protocol') +const { reportOracle, getSecondsPerFrame, getSlotTimestamp } = require('../helpers/oracle') +const { advanceChainTime } = require('../helpers/blockchain') +// const { processNamedTuple } = require('../helpers/debug') + +const StakingModuleMock = artifacts.require('StakingModuleMock') + +function piecewiseModN({ values, pointsPerValue, x }) { + const iValue = Math.floor(x / pointsPerValue) + const leftValue = values[iValue % values.length] + const rightValue = values[(iValue + 1) % values.length] + return leftValue + ((rightValue - leftValue) * (x % pointsPerValue)) / pointsPerValue +} + +contract('Lido, AccountingOracle, WithdrawalQueue integration', ([depositor, user, user2]) => { + const test = (numRebases, withdrawalRequestsPerRebase, rebasesPerShareRateExtrema) => { + let lido, router, wQueue, oracle, consensus, voting, stakingModule, stakingModuleId + let secondsPerFrame + + before('deploy contracts', async () => { + const deployed = await deployProtocol({ + stakingModulesFactory: async () => { + stakingModule = await StakingModuleMock.new() + return [ + { + module: stakingModule, + name: 'module1', + targetShares: 10000, + moduleFee: 500, + treasuryFee: 500, + }, + ] + }, + depositSecurityModuleFactory: async () => { + return { address: depositor } + }, + }) + + lido = deployed.pool + router = deployed.stakingRouter + wQueue = deployed.withdrawalQueue + oracle = deployed.oracle + consensus = deployed.consensusContract + voting = deployed.voting.address + + secondsPerFrame = await getSecondsPerFrame(consensus) + stakingModuleId = +(await router.getStakingModuleIds())[0] + + const withdrawalCredentials = '0x'.padEnd(66, '1234') + await router.setWithdrawalCredentials(withdrawalCredentials, { from: voting }) + + await deployed.oracleReportSanityChecker.setAnnualBalanceIncreaseBPLimit(10000, { from: voting }) + + await wQueue.resume({ from: deployed.appManager.address }) + }) + + const advanceTimeToNextFrame = async () => { + await advanceChainTime(secondsPerFrame) + } + + const calcCLBalanceIncreaseForShareRateBP = async (shareRateBP) => { + const totalShares = await lido.getTotalShares() + const newTotalEth = toBN(shareRateBP).mul(toBN(totalShares)).divn(10000) + return newTotalEth.sub(toBN(await lido.getTotalPooledEther())) + } + + const rebaseToShareRateBP = async (shareRateBP) => { + const stat = await lido.getBeaconStat() + const ethDiff = await calcCLBalanceIncreaseForShareRateBP(shareRateBP) + const newCLBalance = toBN(stat.beaconBalance).add(ethDiff) + + await advanceTimeToNextFrame() + + const { submitDataTx } = await reportOracle(consensus, oracle, { + numValidators: stat.beaconValidators, + clBalance: newCLBalance, + }) + + return submitDataTx + } + + let userBalance + + it(`a user submits ETH to the protocol`, async () => { + const ethToSubmit = toBN(e18(320)).sub(await lido.getTotalPooledEther()) + await lido.submit(ZERO_ADDRESS, { from: user, value: ethToSubmit }) + + userBalance = await lido.balanceOf(user) + await lido.approve(wQueue.address, userBalance, { from: user }) + }) + + it(`ether gets deposited to the CL`, async () => { + await stakingModule.setAvailableKeysCount(10) + await lido.deposit(10, stakingModuleId, '0x0', { from: depositor }) + assert.equals(await lido.getBufferedEther(), 0) + + let stat = await lido.getBeaconStat() + assert.equals(stat.depositedValidators, 10) + + const clBalance = toBN(stat.depositedValidators).mul(toBN(e18(32))) + + await advanceTimeToNextFrame() + + await reportOracle(consensus, oracle, { + numValidators: stat.depositedValidators, + clBalance, + }) + + stat = await lido.getBeaconStat() + assert.equals(stat.beaconValidators, 10) + assert.equals(stat.beaconBalance, clBalance) + }) + + const totalRequests = numRebases * withdrawalRequestsPerRebase + const shareRatesBP = [10010, 10020] + let shareRateBP + + for (let i = 0; i < numRebases; ++i) { + shareRateBP = Math.floor( + piecewiseModN({ + values: shareRatesBP, + pointsPerValue: rebasesPerShareRateExtrema, + x: i, + }) + ) + + context(`rebase ${i}, share rate: ${shareRateBP / 10000}`, () => { + before(async () => { + await rebaseToShareRateBP(shareRateBP) + assert.equals(await lido.getPooledEthByShares(10000), shareRateBP) + }) + + it(`adding ${withdrawalRequestsPerRebase} requests`, async () => { + const requestSize = toBN(userBalance).divn(totalRequests) + const amounts = new Array(withdrawalRequestsPerRebase).fill(requestSize) + await wQueue.requestWithdrawals(amounts, user, { from: user }) + }) + + if (i === numRebases - 1) { + it(`users submit enough ETH to buffer to fullfill all withdrawals`, async () => { + // twice as much ETH will be enough in all scenarios + await lido.submit(ZERO_ADDRESS, { from: user2, value: toBN(userBalance).muln(2) }) + }) + } + }) + } + + const finalShareRateBP = Math.floor((shareRatesBP[0] + shareRatesBP[1]) / 2) + const finalShareRate27 = e27(finalShareRateBP / 10000) + + context(`share rate: ${finalShareRateBP / 10000}`, () => { + let oracleReportFields, ethAvailForWithdrawals + + it(`calculating available ETH`, async () => { + const { refSlot } = await consensus.getCurrentFrame() + + const stat = await lido.getBeaconStat() + const ethDiff = await calcCLBalanceIncreaseForShareRateBP(finalShareRateBP) + const newCLBalance = toBN(stat.beaconBalance).add(ethDiff) + + oracleReportFields = { + refSlot, + numValidators: stat.beaconValidators, + clBalance: newCLBalance, + withdrawalVaultBalance: 0, + elRewardsVaultBalance: 0, + sharesRequestedToBurn: 0, + } + + const timestamp = await getSlotTimestamp(+refSlot, consensus) + const secondsElapsed = secondsPerFrame + + const [totalEth, totalShares, withdrawals, elRewards] = await lido.handleOracleReport.call( + timestamp, + secondsElapsed, + oracleReportFields.numValidators, + oracleReportFields.clBalance, + oracleReportFields.withdrawalVaultBalance, + oracleReportFields.elRewardsVaultBalance, + oracleReportFields.sharesRequestedToBurn, + [], + 0, // simulatedShareRate + { from: oracle.address } + ) + + assert.equals(withdrawals, 0) + assert.equals(elRewards, 0) + + const shareRateE27 = toBN(e27(totalEth)).div(toBN(totalShares)) + const oneWeiE27 = e9(1) + + assert.isClose(shareRateE27, finalShareRate27, oneWeiE27) + + const unfinalizedStETH = await wQueue.unfinalizedStETH() + const bufferedEth = await lido.getBufferedEther() + + ethAvailForWithdrawals = BN.min(toBN(unfinalizedStETH), toBN(bufferedEth)) + .add(toBN(withdrawals)) + .add(toBN(elRewards)) + + console.log(`ethAvailForWithdrawals: ${ethAvailForWithdrawals.div(toBN(10).pow(toBN(18)))}`) + }) + + it.skip('TODO: oracle report') + }) + } + + context('handleOracleReport gas consumption', () => { + const testWithParams = (numRebases, withdrawalRequestsPerRebase, rebasesPerShareRateExtrema) => { + const desc = + `rebases: ${numRebases}, requests per rebase: ${withdrawalRequestsPerRebase}, ` + + `rebases per extrema: ${rebasesPerShareRateExtrema}` + context(desc, () => test(numRebases, withdrawalRequestsPerRebase, rebasesPerShareRateExtrema)) + } + testWithParams(2, 1, 1) + }) +}) diff --git a/test/scenario/lido_happy_path.test.js b/test/scenario/lido_happy_path.test.js index 54a764802..e1736a53e 100644 --- a/test/scenario/lido_happy_path.test.js +++ b/test/scenario/lido_happy_path.test.js @@ -89,13 +89,15 @@ contract('Lido: happy path', (addresses) => { // Total fee is 10% const totalFeePoints = 0.1 * 10000 - it('voting sets fee and its distribution', async () => { + it.skip('voting sets fee and its distribution', async () => { // Fee and distribution were set - // assert.equals(await pool.getFee({ from: nobody }), totalFeePoints, 'total fee') - // const distribution = await pool.getFeeDistribution({ from: nobody }) - // console.log('distribution', distribution) - // assert.equals(distribution.treasuryFeeBasisPoints, treasuryFeePoints, 'treasury fee') - // assert.equals(distribution.operatorsFeeBasisPoints, nodeOperatorsFeePoints, 'node operators fee') + assert.equals(await pool.getFee({ from: nobody }), totalFeePoints, 'total fee') + const distribution = await pool.getFeeDistribution({ from: nobody }) + console.log('distribution', distribution) + const treasuryFeePoints = 0 // TODO + const nodeOperatorsFeePoints = 0 // TODO + assert.equals(distribution.treasuryFeeBasisPoints, treasuryFeePoints, 'treasury fee') + assert.equals(distribution.operatorsFeeBasisPoints, nodeOperatorsFeePoints, 'node operators fee') }) it('voting sets withdrawal credentials', async () => { @@ -401,7 +403,7 @@ contract('Lido: happy path', (addresses) => { // Reporting 1.005-fold balance increase (64 => 64.32) to stay in limits - await pushOracleReport(consensus, oracle, 2, ETH(64.32)) + await pushOracleReport(consensus, oracle, 2, ETH(64.32), ETH(0)) // Total shares increased because fee minted (fee shares added) // shares = oldTotalShares + reward * totalFee * oldTotalShares / (newTotalPooledEther - reward * totalFee) diff --git a/test/scenario/lido_rewards_distribution_math.test.js b/test/scenario/lido_rewards_distribution_math.test.js index c4a73c3cb..73d0dee29 100644 --- a/test/scenario/lido_rewards_distribution_math.test.js +++ b/test/scenario/lido_rewards_distribution_math.test.js @@ -69,7 +69,7 @@ contract('Lido: rewards distribution math', (addresses) => { } async function reportBeacon(validatorsCount, balance) { - const receipts = await pushOracleReport(consensus, oracle, validatorsCount, balance) + const receipts = await pushOracleReport(consensus, oracle, validatorsCount, balance, 0) await ethers.provider.send('evm_increaseTime', [SECONDS_PER_FRAME + 1000]) await ethers.provider.send('evm_mine') @@ -729,10 +729,10 @@ contract('Lido: rewards distribution math', (addresses) => { const rewardsAmount = ETH(1) const newBeaconBalance = totalPooledEtherBefore.sub(bufferedBefore).add(toBN(rewardsAmount)) - const treasurySharesBefore = await await token.sharesOf(treasuryAddr) - const nodeOperator1SharesBefore = await await token.sharesOf(nodeOperator1.address) - const nodeOperator2SharesBefore = await await token.sharesOf(nodeOperator2.address) - const nodeOperator3SharesBefore = await await token.sharesOf(nodeOperator3.address) + const treasurySharesBefore = await token.sharesOf(treasuryAddr) + const nodeOperator1SharesBefore = await token.sharesOf(nodeOperator1.address) + const nodeOperator2SharesBefore = await token.sharesOf(nodeOperator2.address) + const nodeOperator3SharesBefore = await token.sharesOf(nodeOperator3.address) await reportBeacon(3, newBeaconBalance) @@ -786,10 +786,10 @@ contract('Lido: rewards distribution math', (addresses) => { await stakingRouter.setStakingModuleStatus(firstModule.id, StakingModuleStatus.Stopped, { from: voting }) - const treasurySharesBefore = await await token.sharesOf(treasuryAddr) - const nodeOperator1SharesBefore = await await token.sharesOf(nodeOperator1.address) - const nodeOperator2SharesBefore = await await token.sharesOf(nodeOperator2.address) - const nodeOperator3SharesBefore = await await token.sharesOf(nodeOperator3.address) + const treasurySharesBefore = await token.sharesOf(treasuryAddr) + const nodeOperator1SharesBefore = await token.sharesOf(nodeOperator1.address) + const nodeOperator2SharesBefore = await token.sharesOf(nodeOperator2.address) + const nodeOperator3SharesBefore = await token.sharesOf(nodeOperator3.address) await reportBeacon(3, newBeaconBalance) @@ -828,10 +828,10 @@ contract('Lido: rewards distribution math', (addresses) => { await stakingRouter.setStakingModuleStatus(secondModule.id, StakingModuleStatus.Stopped, { from: voting }) - const treasurySharesBefore = await await token.sharesOf(treasuryAddr) - const nodeOperator1SharesBefore = await await token.sharesOf(nodeOperator1.address) - const nodeOperator2SharesBefore = await await token.sharesOf(nodeOperator2.address) - const nodeOperator3SharesBefore = await await token.sharesOf(nodeOperator3.address) + const treasurySharesBefore = await token.sharesOf(treasuryAddr) + const nodeOperator1SharesBefore = await token.sharesOf(nodeOperator1.address) + const nodeOperator2SharesBefore = await token.sharesOf(nodeOperator2.address) + const nodeOperator3SharesBefore = await token.sharesOf(nodeOperator3.address) await reportBeacon(3, newBeaconBalance) diff --git a/yarn.lock b/yarn.lock index 3101a03f7..e2cbe9895 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3751,6 +3751,7 @@ __metadata: lerna: ^3.22.1 lint-staged: ">=10" minimatch: ^6.2.0 + mocha-param: ^2.0.1 node-gyp: ^8.4.1 openzeppelin-solidity: 2.0.0 prettier: ^2.8.4 @@ -19574,6 +19575,13 @@ fsevents@~2.3.2: languageName: node linkType: hard +"mocha-param@npm:^2.0.1": + version: 2.0.1 + resolution: "mocha-param@npm:2.0.1" + checksum: a4dee1c32fdee27c6fd83c13a41a01148e2d5f2fd7440a29fc8d817027afde11ded3017556a2492485f611d64d738065c5e5d2cb5a49891d97872c2a9433724f + languageName: node + linkType: hard + "mocha@npm:7.1.2": version: 7.1.2 resolution: "mocha@npm:7.1.2"