From 7f8411b0348249ba7f84d91317a54bca3536a9e8 Mon Sep 17 00:00:00 2001 From: maksim Date: Tue, 12 Nov 2024 20:55:22 +0100 Subject: [PATCH 001/405] refactor: update comments in AccountingOracle contract --- contracts/0.8.9/oracle/AccountingOracle.sol | 40 +++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 8225667b25..0ac8f3dcb2 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -132,6 +132,7 @@ contract AccountingOracle is BaseOracle { bytes32 internal constant EXTRA_DATA_PROCESSING_STATE_POSITION = keccak256("lido.AccountingOracle.extraDataProcessingState"); + /// @dev will be renamed to ZERO_BYTES32 bytes32 internal constant ZERO_HASH = bytes32(0); address public immutable LIDO; @@ -275,15 +276,7 @@ contract AccountingOracle is BaseOracle { /// data for a report is possible after its processing deadline passes or a new data report /// arrives. /// - /// Depending on the size of the extra data, the processing might need to be split into - /// multiple transactions. Each transaction contains a chunk of report data (an array of items) - /// and the hash of the next transaction. The last transaction will contain ZERO_HASH - /// as the next transaction hash. - /// - /// | 32 bytes | array of items - /// | nextHash | ... - /// - /// Each item being encoded as follows: + /// Extra data is an array of items, each item being encoded as follows: /// /// 3 bytes 2 bytes X bytes /// | itemIndex | itemType | itemPayload | @@ -356,7 +349,7 @@ contract AccountingOracle is BaseOracle { /// @dev Hash of the extra data. See the constant defining a specific extra data /// format for the info on how to calculate the hash. /// - /// Must be set to a zero hash if the oracle report contains no extra data. + /// Must be set to a `ZERO_BYTES32` if the oracle report contains no extra data. /// bytes32 extraDataHash; @@ -374,16 +367,25 @@ contract AccountingOracle is BaseOracle { /// uint256 public constant EXTRA_DATA_FORMAT_EMPTY = 0; - /// @notice The list format for the extra data array. Used when all extra data processing - /// fits into a single or multiple transactions. + /// @notice The list format for the extra data array. Used when the oracle reports contains extra data. + /// + /// Depending on the extra data size, it's passed within a single or multiple transactions. + /// Each transaction contains data consisting of 1) the keccak256 hash of the next + /// transaction's data or `ZERO_BYTES32` if there are no more data chunks, and 2) a chunk + /// of report data (an array of items). + /// + /// | 32 bytes | X bytes | + /// | Next transaction's data hash or `ZERO_BYTES32` | array of items | /// - /// Depend on the extra data size it passed within a single or multiple transactions. - /// Each transaction contains next transaction hash and a bytearray containing data items - /// packed tightly. + /// The `extraDataHash` field of the `ReportData` struct is calculated as a keccak256 hash + /// over the first transaction's data, i.e. over the first data chunk with the second + /// transaction's data hash (or `ZERO_BYTES32`) prepended. /// - /// Hash is a keccak256 hash calculated over the transaction data (next transaction hash and bytearray items). - /// The Solidity equivalent of the hash calculation code would be `keccak256(data)`, - /// where `data` has the `bytes` type. + /// ReportData.extraDataHash := hash0 + /// hash0 := keccak256(| hash1 | extraData[0], ... extraData[n] |) + /// hash1 := keccak256(| hash2 | extraData[n + 1], ... extraData[m] |) + /// ... + /// hashK := keccak256(| ZERO_BYTES32 | extraData[x + 1], ... extraData[extraDataItemsCount] |) /// uint256 public constant EXTRA_DATA_FORMAT_LIST = 1; @@ -420,7 +422,7 @@ contract AccountingOracle is BaseOracle { /// @notice Submits report extra data in the EXTRA_DATA_FORMAT_LIST format for processing. /// - /// @param data The extra data chunk with items list. See docs for the `EXTRA_DATA_FORMAT_LIST` + /// @param data The extra data chunk. See docs for the `EXTRA_DATA_FORMAT_LIST` /// constant for details. /// function submitReportExtraDataList(bytes calldata data) external { From 2d1c5fcb27d9859a6c66c7b95ebfae18908a3b90 Mon Sep 17 00:00:00 2001 From: maksim Date: Wed, 13 Nov 2024 12:47:44 +0100 Subject: [PATCH 002/405] refactor: add a short description for the DepositSecurityModule contract --- contracts/0.8.9/DepositSecurityModule.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/0.8.9/DepositSecurityModule.sol b/contracts/0.8.9/DepositSecurityModule.sol index b39ef28bb0..9fc77d9715 100644 --- a/contracts/0.8.9/DepositSecurityModule.sol +++ b/contracts/0.8.9/DepositSecurityModule.sol @@ -32,6 +32,11 @@ interface IStakingRouter { /** * @title DepositSecurityModule * @dev The contract represents a security module for handling deposits. + * + * The contract allows pausing deposits in response to potential security incidents and + * requires a quorum of guardians to authorize deposit operations. It also provides a mechanism + * to unvet signing keys (a vetted key is a validator key approved for receiving ether deposits) + * in case of any issues. */ contract DepositSecurityModule { /** From 0f565e004459c0aaaf63b56ca60831ec8f6de98d Mon Sep 17 00:00:00 2001 From: maksim Date: Wed, 13 Nov 2024 17:17:39 +0100 Subject: [PATCH 003/405] refactor: rename ZERO_HASH to ZERO_BYTES32 --- contracts/0.8.9/oracle/AccountingOracle.sol | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 0ac8f3dcb2..c9794876ee 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -132,8 +132,7 @@ contract AccountingOracle is BaseOracle { bytes32 internal constant EXTRA_DATA_PROCESSING_STATE_POSITION = keccak256("lido.AccountingOracle.extraDataProcessingState"); - /// @dev will be renamed to ZERO_BYTES32 - bytes32 internal constant ZERO_HASH = bytes32(0); + bytes32 internal constant ZERO_BYTES32 = bytes32(0); address public immutable LIDO; ILidoLocator public immutable LOCATOR; @@ -463,7 +462,7 @@ contract AccountingOracle is BaseOracle { ConsensusReport memory report = _storageConsensusReport().value; result.currentFrameRefSlot = _getCurrentRefSlot(); - if (report.hash == ZERO_HASH || result.currentFrameRefSlot != report.refSlot) { + if (report.hash == ZERO_BYTES32 || result.currentFrameRefSlot != report.refSlot) { return result; } @@ -587,8 +586,8 @@ contract AccountingOracle is BaseOracle { function _handleConsensusReportData(ReportData calldata data, uint256 prevRefSlot) internal { if (data.extraDataFormat == EXTRA_DATA_FORMAT_EMPTY) { - if (data.extraDataHash != ZERO_HASH) { - revert UnexpectedExtraDataHash(ZERO_HASH, data.extraDataHash); + if (data.extraDataHash != ZERO_BYTES32) { + revert UnexpectedExtraDataHash(ZERO_BYTES32, data.extraDataHash); } if (data.extraDataItemsCount != 0) { revert UnexpectedExtraDataItemsCount(0, data.extraDataItemsCount); @@ -600,7 +599,7 @@ contract AccountingOracle is BaseOracle { if (data.extraDataItemsCount == 0) { revert ExtraDataItemsCountCannotBeZeroForNonEmptyData(); } - if (data.extraDataHash == ZERO_HASH) { + if (data.extraDataHash == ZERO_BYTES32) { revert ExtraDataHashCannotBeZeroForNonEmptyData(); } } @@ -710,7 +709,7 @@ contract AccountingOracle is BaseOracle { ConsensusReport memory report = _storageConsensusReport().value; - if (report.hash == ZERO_HASH || procState.refSlot != report.refSlot) { + if (report.hash == ZERO_BYTES32 || procState.refSlot != report.refSlot) { revert CannotSubmitExtraDataBeforeMainData(); } @@ -759,7 +758,7 @@ contract AccountingOracle is BaseOracle { _processExtraDataItems(data, iter); uint256 itemsProcessed = iter.index + 1; - if (dataHash == ZERO_HASH) { + if (dataHash == ZERO_BYTES32) { if (itemsProcessed != procState.itemsCount) { revert UnexpectedExtraDataItemsCount(procState.itemsCount, itemsProcessed); } From 508cd74a2889c641b6fbe4982ea5ae7dd003aaca Mon Sep 17 00:00:00 2001 From: maksim Date: Mon, 18 Nov 2024 11:02:59 +0700 Subject: [PATCH 004/405] refactor: move unvet payload validation to dedicated function --- contracts/0.8.9/DepositSecurityModule.sol | 34 +++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/contracts/0.8.9/DepositSecurityModule.sol b/contracts/0.8.9/DepositSecurityModule.sol index 9fc77d9715..2464f172a6 100644 --- a/contracts/0.8.9/DepositSecurityModule.sol +++ b/contracts/0.8.9/DepositSecurityModule.sol @@ -584,20 +584,7 @@ contract DepositSecurityModule { bytes calldata vettedSigningKeysCounts, Signature calldata sig ) external { - /// @dev The most likely reason for the signature to go stale - uint256 onchainNonce = STAKING_ROUTER.getStakingModuleNonce(stakingModuleId); - if (nonce != onchainNonce) revert ModuleNonceChanged(); - - uint256 nodeOperatorsCount = nodeOperatorIds.length / 8; - - if ( - nodeOperatorIds.length % 8 != 0 || - vettedSigningKeysCounts.length % 16 != 0 || - vettedSigningKeysCounts.length / 16 != nodeOperatorsCount || - nodeOperatorsCount > maxOperatorsPerUnvetting - ) { - revert UnvetPayloadInvalid(); - } + _checkIfUnvetPayloadValid(nodeOperatorIds, vettedSigningKeysCounts); address guardianAddr = msg.sender; int256 guardianIndex = _getGuardianIndex(msg.sender); @@ -630,4 +617,23 @@ contract DepositSecurityModule { vettedSigningKeysCounts ); } + + function _checkIfUnvetPayloadValid( + uint256 stakingModuleId, + uint256 nonce, + bytes calldata nodeOperatorIds, + bytes calldata vettedSigningKeysCounts + ) internal view { + /// @dev The most likely reason for the signature to go stale + uint256 onchainNonce = STAKING_ROUTER.getStakingModuleNonce(stakingModuleId); + if (nonce != onchainNonce) revert ModuleNonceChanged(); + + uint256 nodeOperatorsCount = nodeOperatorIds.length / 8; + + if ( + nodeOperatorIds.length % 8 != 0 || + vettedSigningKeysCounts.length != nodeOperatorsCount * 16 || + nodeOperatorsCount > maxOperatorsPerUnvetting + ) revert UnvetPayloadInvalid(); + } } From f7c7632c715d73a6ca6ed242c1cb85b0ef68a05d Mon Sep 17 00:00:00 2001 From: maksim Date: Mon, 25 Nov 2024 11:07:13 +0100 Subject: [PATCH 005/405] refactor: rename forced to boosted use 'boosted' instead of 'forced' when we describe a mode when validators to exit are prioritized --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 4 ++-- contracts/0.8.9/interfaces/IStakingModule.sol | 2 +- test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 1e5d30f3a9..e7866e751e 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -102,7 +102,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint8 internal constant TOTAL_DEPOSITED_KEYS_COUNT_OFFSET = 3; // TargetValidatorsStats - /// @dev Target limit mode, allows limiting target active validators count for operator (0 = disabled, 1 = soft mode, 2 = forced mode) + /// @dev Target limit mode, allows limiting target active validators count for operator (0 = disabled, 1 = soft mode, 2 = boosted mode) uint8 internal constant TARGET_LIMIT_MODE_OFFSET = 0; /// @dev relative target active validators limit for operator, set by DAO /// @notice used to check how many keys should go to exit, 0 - means all deposited keys would be exited @@ -689,7 +689,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @notice Updates the limit of the validators that can be used for deposit by DAO /// @param _nodeOperatorId Id of the node operator - /// @param _targetLimitMode target limit mode (0 = disabled, 1 = soft mode, 2 = forced mode) + /// @param _targetLimitMode target limit mode (0 = disabled, 1 = soft mode, 2 = boosted mode) /// @param _targetLimit Target limit of the node operator function updateTargetValidatorsLimits(uint256 _nodeOperatorId, uint256 _targetLimitMode, uint256 _targetLimit) public { _onlyExistedNodeOperator(_nodeOperatorId); diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 82d55cf05b..bf7056ee3c 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -23,7 +23,7 @@ interface IStakingModule { /// @notice Returns all-validators summary belonging to the node operator with the given id /// @param _nodeOperatorId id of the operator to return report for - /// @return targetLimitMode shows whether the current target limit applied to the node operator (0 = disabled, 1 = soft mode, 2 = forced mode) + /// @return targetLimitMode shows whether the current target limit applied to the node operator (0 = disabled, 1 = soft mode, 2 = boosted mode) /// @return targetValidatorsCount relative target active validators limit for operator /// @return stuckValidatorsCount number of validators with an expired request to exit time /// @return refundedValidatorsCount number of validators that can't be withdrawn, but deposit diff --git a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts index b7bb098784..ead4a584b2 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts @@ -340,7 +340,7 @@ describe("StakingRouter:module-sync", () => { context("updateTargetValidatorsLimits", () => { const NODE_OPERATOR_ID = 0n; - const TARGET_LIMIT_MODE = 1; // 1 - soft, i.e. on WQ request; 2 - forced + const TARGET_LIMIT_MODE = 1; // 1 - soft, i.e. on WQ request; 2 - boosted const TARGET_LIMIT = 100n; it("Reverts if the caller does not have the role", async () => { From 9b454a831193fdd3662f2ea1dcb5da18e7da58d0 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 19 Dec 2024 19:45:01 +0100 Subject: [PATCH 006/405] feat: add withdrawal credentials lib --- contracts/0.8.9/WithdrawalVault.sol | 49 +++- .../IWithdrawalCredentialsRequests.sol | 11 + .../lib/WithdrawalCredentialsRequests.sol | 72 ++++++ .../WithdrawalCredentials_Harness.sol | 16 ++ .../WithdrawalsPredeployed_Mock.sol | 46 ++++ .../withdrawalCredentials.test.ts | 36 +++ .../withdrawalRequests.behaviour.ts | 217 ++++++++++++++++++ test/0.8.9/withdrawalVault.test.ts | 60 ++++- 8 files changed, 486 insertions(+), 21 deletions(-) create mode 100644 contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol create mode 100644 contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol create mode 100644 test/0.8.9/contracts/WithdrawalCredentials_Harness.sol create mode 100644 test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol create mode 100644 test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts create mode 100644 test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index c5485b7852..2ba6867ba1 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -9,6 +9,8 @@ import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; +import {IWithdrawalCredentialsRequests} from "./interfaces/IWithdrawalCredentialsRequests.sol"; +import {WithdrawalCredentialsRequests} from "./lib/WithdrawalCredentialsRequests.sol"; interface ILido { /** @@ -22,11 +24,13 @@ interface ILido { /** * @title A vault for temporary storage of withdrawals */ -contract WithdrawalVault is Versioned { +contract WithdrawalVault is Versioned, IWithdrawalCredentialsRequests { using SafeERC20 for IERC20; + using WithdrawalCredentialsRequests for *; ILido public immutable LIDO; address public immutable TREASURY; + address public immutable VALIDATORS_EXIT_BUS; // Events /** @@ -42,9 +46,9 @@ contract WithdrawalVault is Versioned { event ERC721Recovered(address indexed requestedBy, address indexed token, uint256 tokenId); // Errors - error LidoZeroAddress(); - error TreasuryZeroAddress(); + error ZeroAddress(); error NotLido(); + error NotValidatorExitBus(); error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); @@ -52,16 +56,14 @@ contract WithdrawalVault is Versioned { * @param _lido the Lido token (stETH) address * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) */ - constructor(ILido _lido, address _treasury) { - if (address(_lido) == address(0)) { - revert LidoZeroAddress(); - } - if (_treasury == address(0)) { - revert TreasuryZeroAddress(); - } + constructor(address _lido, address _treasury, address _validatorsExitBus) { + _assertNonZero(_lido); + _assertNonZero(_treasury); + _assertNonZero(_validatorsExitBus); - LIDO = _lido; + LIDO = ILido(_lido); TREASURY = _treasury; + VALIDATORS_EXIT_BUS = _validatorsExitBus; } /** @@ -70,6 +72,12 @@ contract WithdrawalVault is Versioned { */ function initialize() external { _initializeContractVersionTo(1); + _updateContractVersion(2); + } + + function finalizeUpgrade_v2() external { + _checkContractVersion(1); + _updateContractVersion(2); } /** @@ -122,4 +130,23 @@ contract WithdrawalVault is Versioned { _token.transferFrom(address(this), TREASURY, _tokenId); } + + function addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts + ) external payable { + if(msg.sender != address(VALIDATORS_EXIT_BUS)) { + revert NotValidatorExitBus(); + } + + WithdrawalCredentialsRequests.addWithdrawalRequests(pubkeys, amounts); + } + + function getWithdrawalRequestFee() external view returns (uint256) { + return WithdrawalCredentialsRequests.getWithdrawalRequestFee(); + } + + function _assertNonZero(address _address) internal pure { + if (_address == address(0)) revert ZeroAddress(); + } } diff --git a/contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol b/contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol new file mode 100644 index 0000000000..130af0e9cd --- /dev/null +++ b/contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol @@ -0,0 +1,11 @@ +interface IWithdrawalCredentialsRequests { + function addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts + ) external payable; + + // function addConsolidationRequests( + // bytes[] calldata sourcePubkeys, + // bytes[] calldata targetPubkeys + // ) external payable; +} diff --git a/contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol b/contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol new file mode 100644 index 0000000000..502ffa7664 --- /dev/null +++ b/contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 Lido + +pragma solidity 0.8.9; + +library WithdrawalCredentialsRequests { + address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; + + error InvalidArrayLengths(uint256 lengthA, uint256 lengthB); + error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); + error WithdrawalRequestFeeReadFailed(); + + error InvalidPubkeyLength(bytes pubkey); + error WithdrawalRequestAdditionFailed(bytes pubkey, uint256 amount); + + event WithdrawalRequestAdded(bytes pubkey, uint256 amount); + + function addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts + ) internal { + uint256 keysCount = pubkeys.length; + if (keysCount != amounts.length || keysCount == 0) { + revert InvalidArrayLengths(keysCount, amounts.length); + } + + uint256 minFeePerRequest = getWithdrawalRequestFee(); + if (minFeePerRequest * keysCount > msg.value) { + revert FeeNotEnough(minFeePerRequest, keysCount, msg.value); + } + + uint256 feePerRequest = msg.value / keysCount; + uint256 unallocatedFee = msg.value % keysCount; + uint256 prevBalance = address(this).balance - msg.value; + + + for (uint256 i = 0; i < keysCount; ++i) { + bytes memory pubkey = pubkeys[i]; + uint64 amount = amounts[i]; + + if(pubkey.length != 48) { + revert InvalidPubkeyLength(pubkey); + } + + uint256 feeToSend = feePerRequest; + + if (i == keysCount - 1) { + feeToSend += unallocatedFee; + } + + bytes memory callData = abi.encodePacked(pubkey, amount); + (bool success, ) = WITHDRAWAL_REQUEST.call{value: feeToSend}(callData); + + if (!success) { + revert WithdrawalRequestAdditionFailed(pubkey, amount); + } + + emit WithdrawalRequestAdded(pubkey, amount); + } + + assert(address(this).balance == prevBalance); + } + + function getWithdrawalRequestFee() internal view returns (uint256) { + (bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall(""); + + if (!success) { + revert WithdrawalRequestFeeReadFailed(); + } + + return abi.decode(feeData, (uint256)); + } +} diff --git a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol b/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol new file mode 100644 index 0000000000..8bd8450f4f --- /dev/null +++ b/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol @@ -0,0 +1,16 @@ +pragma solidity 0.8.9; + +import {WithdrawalCredentialsRequests} from "contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol"; + +contract WithdrawalCredentials_Harness { + function addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts + ) external payable { + WithdrawalCredentialsRequests.addWithdrawalRequests(pubkeys, amounts); + } + + function getWithdrawalRequestFee() external view returns (uint256) { + return WithdrawalCredentialsRequests.getWithdrawalRequestFee(); + } +} diff --git a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol new file mode 100644 index 0000000000..9db24d0346 --- /dev/null +++ b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.9; + +contract WithdrawalsPredeployed_Mock { + event WithdrawalRequestedMetadata( + uint256 dataLength + ); + event WithdrawalRequested( + bytes pubKey, + uint64 amount, + uint256 feePaid, + address sender + ); + + uint256 public fee; + bool public failOnAddRequest; + bool public failOnGetFee; + + function setFailOnAddRequest(bool _failOnAddRequest) external { + failOnAddRequest = _failOnAddRequest; + } + + function setFailOnGetFee(bool _failOnGetFee) external { + failOnGetFee = _failOnGetFee; + } + + function setFee(uint256 _fee) external { + require(_fee > 0, "fee must be greater than 0"); + fee = _fee; + } + + fallback(bytes calldata input) external payable returns (bytes memory output){ + if (input.length == 0) { + require(!failOnGetFee, "fail on get fee"); + + uint256 currentFee = fee; + output = new bytes(32); + assembly { mstore(add(output, 32), currentFee) } + return output; + } + + require(!failOnAddRequest, "fail on add request"); + + require(input.length == 56, "Invalid callData length"); + } +} diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts new file mode 100644 index 0000000000..753cee30f6 --- /dev/null +++ b/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts @@ -0,0 +1,36 @@ +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; + +import { Snapshot } from "test/suite"; + +import { deployWithdrawalsPredeployedMock, tesWithdrawalRequestsBehavior } from "./withdrawalRequests.behaviour"; + +describe("WithdrawalCredentials.sol", () => { + let actor: HardhatEthersSigner; + + let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; + let withdrawalCredentials: WithdrawalCredentials_Harness; + + let originalState: string; + + const getWithdrawalCredentialsContract = () => withdrawalCredentials.connect(actor); + const getWithdrawalsPredeployedContract = () => withdrawalsPredeployed.connect(actor); + + before(async () => { + [actor] = await ethers.getSigners(); + + withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(); + withdrawalCredentials = await ethers.deployContract("WithdrawalCredentials_Harness"); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("max", () => { + tesWithdrawalRequestsBehavior(getWithdrawalCredentialsContract, getWithdrawalsPredeployedContract); + }); +}); diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts new file mode 100644 index 0000000000..34ff988737 --- /dev/null +++ b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts @@ -0,0 +1,217 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; + +import { findEventsWithInterfaces } from "lib"; + +const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; +const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); + +const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; + +export async function deployWithdrawalsPredeployedMock(): Promise { + const withdrawalsPredeployed = await ethers.deployContract("WithdrawalsPredeployed_Mock"); + const withdrawalsPredeployedAddress = await withdrawalsPredeployed.getAddress(); + + await ethers.provider.send("hardhat_setCode", [ + withdrawalsPredeployedHardcodedAddress, + await ethers.provider.getCode(withdrawalsPredeployedAddress), + ]); + + const contract = await ethers.getContractAt("WithdrawalsPredeployed_Mock", withdrawalsPredeployedHardcodedAddress); + await contract.setFee(1n); + return contract; +} + +function toValidatorPubKey(num: number): string { + if (num < 0 || num > 0xffff) { + throw new Error("Number is out of the 2-byte range (0x0000 - 0xFFFF)."); + } + + return `0x${num.toString(16).padStart(4, "0").repeat(24)}`; +} + +const convertEthToGwei = (ethAmount: string | number): bigint => { + const ethString = ethAmount.toString(); + const wei = ethers.parseEther(ethString); + return wei / 1_000_000_000n; +}; + +function generateWithdrawalRequestPayload(numberOfRequests: number) { + const pubkeys: string[] = []; + const amounts: bigint[] = []; + for (let i = 1; i <= numberOfRequests; i++) { + pubkeys.push(toValidatorPubKey(i)); + amounts.push(convertEthToGwei(i)); + } + + return { pubkeys, amounts }; +} + +export function tesWithdrawalRequestsBehavior( + getContract: () => WithdrawalCredentials_Harness, + getWithdrawalsPredeployedContract: () => WithdrawalsPredeployed_Mock, +) { + async function getFee(requestsCount: number): Promise { + const fee = await getContract().getWithdrawalRequestFee(); + + return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); + } + + async function getWithdrawalCredentialsContractBalance(): Promise { + const contract = getContract(); + const contractAddress = await contract.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + + async function addWithdrawalRequests(requestCount: number, extraFee: bigint = 0n) { + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(); + + const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = (await getFee(pubkeys.length)) + extraFee; + const tx = await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + + const receipt = await tx.wait(); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + + const events = findEventsWithInterfaces(receipt!, "WithdrawalRequestAdded", [withdrawalRequestEventInterface]); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[0]).to.equal(pubkeys[i]); + expect(events[i].args[1]).to.equal(amounts[i]); + } + } + + context("addWithdrawalRequests", async () => { + it("Should revert if array lengths do not match or empty arrays are provided", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); + amounts.pop(); + + expect( + pubkeys.length !== amounts.length, + "Test setup error: pubkeys and amounts arrays should have different lengths.", + ); + + const contract = getContract(); + + const fee = await getFee(pubkeys.length); + await expect(contract.addWithdrawalRequests(pubkeys, amounts, { value: fee })) + .to.be.revertedWithCustomError(contract, "InvalidArrayLengths") + .withArgs(pubkeys.length, amounts.length); + + // Also test empty arrays + await expect(contract.addWithdrawalRequests([], [], { value: fee })) + .to.be.revertedWithCustomError(contract, "InvalidArrayLengths") + .withArgs(0, 0); + }); + + it("Should revert if not enough fee is sent", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); + const contract = getContract(); + + await getWithdrawalsPredeployedContract().setFee(3n); // Set fee to 3 gwei + + // Should revert if no fee is sent + await expect(contract.addWithdrawalRequests(pubkeys, amounts)).to.be.revertedWithCustomError( + contract, + "FeeNotEnough", + ); + + // Should revert if fee is less than required + const insufficientFee = 2n; + await expect( + contract.addWithdrawalRequests(pubkeys, amounts, { value: insufficientFee }), + ).to.be.revertedWithCustomError(contract, "FeeNotEnough"); + }); + + it("Should revert if any pubkey is not 48 bytes", async function () { + // Invalid pubkey (only 2 bytes) + const pubkeys = ["0x1234"]; + const amounts = [100n]; + + const fee = await getFee(pubkeys.length); + const contract = getContract(); + await expect(contract.addWithdrawalRequests(pubkeys, amounts, { value: fee })) + .to.be.revertedWithCustomError(contract, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + }); + + it("Should revert if addition fails at the withdrawal request contract", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); + const fee = await getFee(pubkeys.length); + + // Set mock to fail on add + await getWithdrawalsPredeployedContract().setFailOnAddRequest(true); + const contract = getContract(); + + await expect(contract.addWithdrawalRequests(pubkeys, amounts, { value: fee })).to.be.revertedWithCustomError( + contract, + "WithdrawalRequestAdditionFailed", + ); + }); + + it("Should accept full and partial withdrawals", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); + amounts[0] = 0n; // Full withdrawal + amounts[1] = 1n; // Partial withdrawal + + const fee = await getFee(pubkeys.length); + const contract = getContract(); + + await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); + }); + + it("Should accept exactly required fee without revert", async function () { + const requestCount = 1; + const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); + + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(); + + await getWithdrawalsPredeployedContract().setFee(3n); + expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); + const fee = 3n; + + await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + }); + + it("Should accept exceed fee without revert", async function () { + const requestCount = 1; + const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); + + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(); + + await getWithdrawalsPredeployedContract().setFee(3n); + expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); + const fee = 3n + 1n; // 1 request * 3 gwei (fee) + 1 gwei (extra fee)= 4 gwei + + await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + }); + + it("Should successfully add requests and emit events", async function () { + await addWithdrawalRequests(1); + await addWithdrawalRequests(3); + await addWithdrawalRequests(10); + await addWithdrawalRequests(100); + }); + + it("Should successfully add requests with extra fee and not change contract balance", async function () { + await addWithdrawalRequests(1, 100n); + await addWithdrawalRequests(3, 1n); + await addWithdrawalRequests(10, 1_000_000n); + await addWithdrawalRequests(7, 3n); + await addWithdrawalRequests(100, 0n); + }); + }); +} diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index c953f23d7e..9f1d80aa4f 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -5,35 +5,54 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { ERC20__Harness, ERC721__Harness, Lido__MockForWithdrawalVault, WithdrawalVault } from "typechain-types"; +import { + ERC20__Harness, + ERC721__Harness, + Lido__MockForWithdrawalVault, + WithdrawalsPredeployed_Mock, + WithdrawalVault, +} from "typechain-types"; import { MAX_UINT256, proxify } from "lib"; import { Snapshot } from "test/suite"; +import { + deployWithdrawalsPredeployedMock, + tesWithdrawalRequestsBehavior, +} from "./lib/withdrawalCredentials/withdrawalRequests.behaviour"; + const PETRIFIED_VERSION = MAX_UINT256; describe("WithdrawalVault.sol", () => { let owner: HardhatEthersSigner; let user: HardhatEthersSigner; let treasury: HardhatEthersSigner; + let validatorsExitBus: HardhatEthersSigner; let originalState: string; let lido: Lido__MockForWithdrawalVault; let lidoAddress: string; + let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; + let impl: WithdrawalVault; let vault: WithdrawalVault; let vaultAddress: string; + const getWithdrawalCredentialsContract = () => vault.connect(validatorsExitBus); + const getWithdrawalsPredeployedContract = () => withdrawalsPredeployed.connect(user); + before(async () => { - [owner, user, treasury] = await ethers.getSigners(); + [owner, user, treasury, validatorsExitBus] = await ethers.getSigners(); + + withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(); lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); lidoAddress = await lido.getAddress(); - impl = await ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address]); + impl = await ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, validatorsExitBus.address]); [vault] = await proxify({ impl, admin: owner }); @@ -47,20 +66,26 @@ describe("WithdrawalVault.sol", () => { context("Constructor", () => { it("Reverts if the Lido address is zero", async () => { await expect( - ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address]), - ).to.be.revertedWithCustomError(vault, "LidoZeroAddress"); + ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address, validatorsExitBus.address]), + ).to.be.revertedWithCustomError(vault, "ZeroAddress"); }); it("Reverts if the treasury address is zero", async () => { - await expect(ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress])).to.be.revertedWithCustomError( - vault, - "TreasuryZeroAddress", - ); + await expect( + ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress, validatorsExitBus.address]), + ).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Reverts if the validator exit buss address is zero", async () => { + await expect( + ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, ZeroAddress]), + ).to.be.revertedWithCustomError(vault, "ZeroAddress"); }); it("Sets initial properties", async () => { expect(await vault.LIDO()).to.equal(lidoAddress, "Lido address"); expect(await vault.TREASURY()).to.equal(treasury.address, "Treasury address"); + expect(await vault.VALIDATORS_EXIT_BUS()).to.equal(validatorsExitBus.address, "Validator exit bus address"); }); it("Petrifies the implementation", async () => { @@ -80,7 +105,11 @@ describe("WithdrawalVault.sol", () => { }); it("Initializes the contract", async () => { - await expect(vault.initialize()).to.emit(vault, "ContractVersionSet").withArgs(1); + await expect(vault.initialize()) + .to.emit(vault, "ContractVersionSet") + .withArgs(1) + .and.to.emit(vault, "ContractVersionSet") + .withArgs(2); }); }); @@ -168,4 +197,15 @@ describe("WithdrawalVault.sol", () => { expect(await token.ownerOf(1)).to.equal(treasury.address); }); }); + + context("addWithdrawalRequests", () => { + it("Reverts if the caller is not Validator Exit Bus", async () => { + await expect(vault.connect(user).addWithdrawalRequests(["0x1234"], [0n])).to.be.revertedWithCustomError( + vault, + "NotValidatorExitBus", + ); + }); + + tesWithdrawalRequestsBehavior(getWithdrawalCredentialsContract, getWithdrawalsPredeployedContract); + }); }); From 3bfe5ac02882cbb192aa12c92233a3e5038edca9 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 19 Dec 2024 19:45:24 +0100 Subject: [PATCH 007/405] feat: split full and partial withdrawals --- contracts/0.8.9/WithdrawalVault.sol | 20 +- .../IWithdrawalCredentialsRequests.sol | 11 - .../lib/WithdrawalCredentialsRequests.sol | 72 ---- contracts/0.8.9/lib/WithdrawalRequests.sol | 122 ++++++ .../WithdrawalCredentials_Harness.sol | 14 +- .../WithdrawalsPredeployed_Mock.sol | 17 +- .../withdrawalCredentials.test.ts | 21 +- .../withdrawalRequests.behavior.ts | 350 ++++++++++++++++++ .../withdrawalRequests.behaviour.ts | 217 ----------- test/0.8.9/withdrawalVault.test.ts | 14 +- 10 files changed, 518 insertions(+), 340 deletions(-) delete mode 100644 contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol delete mode 100644 contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol create mode 100644 contracts/0.8.9/lib/WithdrawalRequests.sol create mode 100644 test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts delete mode 100644 test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 2ba6867ba1..bc6d87e766 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -9,8 +9,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; -import {IWithdrawalCredentialsRequests} from "./interfaces/IWithdrawalCredentialsRequests.sol"; -import {WithdrawalCredentialsRequests} from "./lib/WithdrawalCredentialsRequests.sol"; +import {WithdrawalRequests} from "./lib/WithdrawalRequests.sol"; interface ILido { /** @@ -24,9 +23,8 @@ interface ILido { /** * @title A vault for temporary storage of withdrawals */ -contract WithdrawalVault is Versioned, IWithdrawalCredentialsRequests { +contract WithdrawalVault is Versioned { using SafeERC20 for IERC20; - using WithdrawalCredentialsRequests for *; ILido public immutable LIDO; address public immutable TREASURY; @@ -131,19 +129,23 @@ contract WithdrawalVault is Versioned, IWithdrawalCredentialsRequests { _token.transferFrom(address(this), TREASURY, _tokenId); } - function addWithdrawalRequests( - bytes[] calldata pubkeys, - uint64[] calldata amounts + /** + * @dev Adds full withdrawal requests for the provided public keys. + * The validator will fully withdraw and exit its duties as a validator. + * @param pubkeys An array of public keys for the validators requesting full withdrawals. + */ + function addFullWithdrawalRequests( + bytes[] calldata pubkeys ) external payable { if(msg.sender != address(VALIDATORS_EXIT_BUS)) { revert NotValidatorExitBus(); } - WithdrawalCredentialsRequests.addWithdrawalRequests(pubkeys, amounts); + WithdrawalRequests.addFullWithdrawalRequests(pubkeys); } function getWithdrawalRequestFee() external view returns (uint256) { - return WithdrawalCredentialsRequests.getWithdrawalRequestFee(); + return WithdrawalRequests.getWithdrawalRequestFee(); } function _assertNonZero(address _address) internal pure { diff --git a/contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol b/contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol deleted file mode 100644 index 130af0e9cd..0000000000 --- a/contracts/0.8.9/interfaces/IWithdrawalCredentialsRequests.sol +++ /dev/null @@ -1,11 +0,0 @@ -interface IWithdrawalCredentialsRequests { - function addWithdrawalRequests( - bytes[] calldata pubkeys, - uint64[] calldata amounts - ) external payable; - - // function addConsolidationRequests( - // bytes[] calldata sourcePubkeys, - // bytes[] calldata targetPubkeys - // ) external payable; -} diff --git a/contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol b/contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol deleted file mode 100644 index 502ffa7664..0000000000 --- a/contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Lido - -pragma solidity 0.8.9; - -library WithdrawalCredentialsRequests { - address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; - - error InvalidArrayLengths(uint256 lengthA, uint256 lengthB); - error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); - error WithdrawalRequestFeeReadFailed(); - - error InvalidPubkeyLength(bytes pubkey); - error WithdrawalRequestAdditionFailed(bytes pubkey, uint256 amount); - - event WithdrawalRequestAdded(bytes pubkey, uint256 amount); - - function addWithdrawalRequests( - bytes[] calldata pubkeys, - uint64[] calldata amounts - ) internal { - uint256 keysCount = pubkeys.length; - if (keysCount != amounts.length || keysCount == 0) { - revert InvalidArrayLengths(keysCount, amounts.length); - } - - uint256 minFeePerRequest = getWithdrawalRequestFee(); - if (minFeePerRequest * keysCount > msg.value) { - revert FeeNotEnough(minFeePerRequest, keysCount, msg.value); - } - - uint256 feePerRequest = msg.value / keysCount; - uint256 unallocatedFee = msg.value % keysCount; - uint256 prevBalance = address(this).balance - msg.value; - - - for (uint256 i = 0; i < keysCount; ++i) { - bytes memory pubkey = pubkeys[i]; - uint64 amount = amounts[i]; - - if(pubkey.length != 48) { - revert InvalidPubkeyLength(pubkey); - } - - uint256 feeToSend = feePerRequest; - - if (i == keysCount - 1) { - feeToSend += unallocatedFee; - } - - bytes memory callData = abi.encodePacked(pubkey, amount); - (bool success, ) = WITHDRAWAL_REQUEST.call{value: feeToSend}(callData); - - if (!success) { - revert WithdrawalRequestAdditionFailed(pubkey, amount); - } - - emit WithdrawalRequestAdded(pubkey, amount); - } - - assert(address(this).balance == prevBalance); - } - - function getWithdrawalRequestFee() internal view returns (uint256) { - (bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall(""); - - if (!success) { - revert WithdrawalRequestFeeReadFailed(); - } - - return abi.decode(feeData, (uint256)); - } -} diff --git a/contracts/0.8.9/lib/WithdrawalRequests.sol b/contracts/0.8.9/lib/WithdrawalRequests.sol new file mode 100644 index 0000000000..7973f118d7 --- /dev/null +++ b/contracts/0.8.9/lib/WithdrawalRequests.sol @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + +library WithdrawalRequests { + address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; + + error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); + error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); + + error WithdrawalRequestFeeReadFailed(); + error InvalidPubkeyLength(bytes pubkey); + error WithdrawalRequestAdditionFailed(bytes pubkey, uint256 amount); + error NoWithdrawalRequests(); + error PartialWithdrawalRequired(bytes pubkey); + + event WithdrawalRequestAdded(bytes pubkey, uint256 amount); + + /** + * @dev Adds full withdrawal requests for the provided public keys. + * The validator will fully withdraw and exit its duties as a validator. + * @param pubkeys An array of public keys for the validators requesting full withdrawals. + */ + function addFullWithdrawalRequests( + bytes[] calldata pubkeys + ) internal { + uint256 keysCount = pubkeys.length; + uint64[] memory amounts = new uint64[](keysCount); + + _addWithdrawalRequests(pubkeys, amounts); + } + + /** + * @dev Adds partial withdrawal requests for the provided public keys with corresponding amounts. + * A partial withdrawal is any withdrawal where the amount is greater than zero. + * This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn). + * However, the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. + * @param pubkeys An array of public keys for the validators requesting withdrawals. + * @param amounts An array of corresponding withdrawal amounts for each public key. + */ + function addPartialWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts + ) internal { + uint256 keysCount = pubkeys.length; + if (keysCount != amounts.length) { + revert MismatchedArrayLengths(keysCount, amounts.length); + } + + uint64[] memory _amounts = new uint64[](keysCount); + for (uint256 i = 0; i < keysCount; i++) { + if (amounts[i] == 0) { + revert PartialWithdrawalRequired(pubkeys[i]); + } + + _amounts[i] = amounts[i]; + } + + _addWithdrawalRequests(pubkeys, _amounts); + } + + /** + * @dev Retrieves the current withdrawal request fee. + * @return The minimum fee required per withdrawal request. + */ + function getWithdrawalRequestFee() internal view returns (uint256) { + (bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall(""); + + if (!success) { + revert WithdrawalRequestFeeReadFailed(); + } + + return abi.decode(feeData, (uint256)); + } + + function _addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] memory amounts + ) internal { + uint256 keysCount = pubkeys.length; + if (keysCount == 0) { + revert NoWithdrawalRequests(); + } + + uint256 minFeePerRequest = getWithdrawalRequestFee(); + if (minFeePerRequest * keysCount > msg.value) { + revert FeeNotEnough(minFeePerRequest, keysCount, msg.value); + } + + uint256 feePerRequest = msg.value / keysCount; + uint256 unallocatedFee = msg.value % keysCount; + uint256 prevBalance = address(this).balance - msg.value; + + + for (uint256 i = 0; i < keysCount; ++i) { + bytes memory pubkey = pubkeys[i]; + uint64 amount = amounts[i]; + + if(pubkey.length != 48) { + revert InvalidPubkeyLength(pubkey); + } + + uint256 feeToSend = feePerRequest; + + if (i == keysCount - 1) { + feeToSend += unallocatedFee; + } + + bytes memory callData = abi.encodePacked(pubkey, amount); + (bool success, ) = WITHDRAWAL_REQUEST.call{value: feeToSend}(callData); + + if (!success) { + revert WithdrawalRequestAdditionFailed(pubkey, amount); + } + + emit WithdrawalRequestAdded(pubkey, amount); + } + + assert(address(this).balance == prevBalance); + } +} diff --git a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol b/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol index 8bd8450f4f..1450f79e91 100644 --- a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol +++ b/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol @@ -1,16 +1,22 @@ pragma solidity 0.8.9; -import {WithdrawalCredentialsRequests} from "contracts/0.8.9/lib/WithdrawalCredentialsRequests.sol"; +import {WithdrawalRequests} from "contracts/0.8.9/lib/WithdrawalRequests.sol"; contract WithdrawalCredentials_Harness { - function addWithdrawalRequests( + function addFullWithdrawalRequests( + bytes[] calldata pubkeys + ) external payable { + WithdrawalRequests.addFullWithdrawalRequests(pubkeys); + } + + function addPartialWithdrawalRequests( bytes[] calldata pubkeys, uint64[] calldata amounts ) external payable { - WithdrawalCredentialsRequests.addWithdrawalRequests(pubkeys, amounts); + WithdrawalRequests.addPartialWithdrawalRequests(pubkeys, amounts); } function getWithdrawalRequestFee() external view returns (uint256) { - return WithdrawalCredentialsRequests.getWithdrawalRequestFee(); + return WithdrawalRequests.getWithdrawalRequestFee(); } } diff --git a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol index 9db24d0346..6c50f7d6ae 100644 --- a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol +++ b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol @@ -1,17 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.9; +/** + * @notice This is an mock of EIP-7002's pre-deploy contract. + */ contract WithdrawalsPredeployed_Mock { - event WithdrawalRequestedMetadata( - uint256 dataLength - ); - event WithdrawalRequested( - bytes pubKey, - uint64 amount, - uint256 feePaid, - address sender - ); - uint256 public fee; bool public failOnAddRequest; bool public failOnGetFee; @@ -33,9 +26,7 @@ contract WithdrawalsPredeployed_Mock { if (input.length == 0) { require(!failOnGetFee, "fail on get fee"); - uint256 currentFee = fee; - output = new bytes(32); - assembly { mstore(add(output, 32), currentFee) } + output = abi.encode(fee); return output; } diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts index 753cee30f6..744519a3f5 100644 --- a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts +++ b/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts @@ -6,7 +6,11 @@ import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "type import { Snapshot } from "test/suite"; -import { deployWithdrawalsPredeployedMock, tesWithdrawalRequestsBehavior } from "./withdrawalRequests.behaviour"; +import { + deployWithdrawalsPredeployedMock, + testFullWithdrawalRequestBehavior, + testPartialWithdrawalRequestBehavior, +} from "./withdrawalRequests.behavior"; describe("WithdrawalCredentials.sol", () => { let actor: HardhatEthersSigner; @@ -16,9 +20,6 @@ describe("WithdrawalCredentials.sol", () => { let originalState: string; - const getWithdrawalCredentialsContract = () => withdrawalCredentials.connect(actor); - const getWithdrawalsPredeployedContract = () => withdrawalsPredeployed.connect(actor); - before(async () => { [actor] = await ethers.getSigners(); @@ -30,7 +31,13 @@ describe("WithdrawalCredentials.sol", () => { afterEach(async () => await Snapshot.restore(originalState)); - context("max", () => { - tesWithdrawalRequestsBehavior(getWithdrawalCredentialsContract, getWithdrawalsPredeployedContract); - }); + testFullWithdrawalRequestBehavior( + () => withdrawalCredentials.connect(actor), + () => withdrawalsPredeployed.connect(actor), + ); + + testPartialWithdrawalRequestBehavior( + () => withdrawalCredentials.connect(actor), + () => withdrawalsPredeployed.connect(actor), + ); }); diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts new file mode 100644 index 0000000000..7eeafea9f6 --- /dev/null +++ b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts @@ -0,0 +1,350 @@ +import { expect } from "chai"; +import { BaseContract } from "ethers"; +import { ethers } from "hardhat"; + +import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; + +import { findEventsWithInterfaces } from "lib"; + +const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; +const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); + +const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; + +export async function deployWithdrawalsPredeployedMock(): Promise { + const withdrawalsPredeployed = await ethers.deployContract("WithdrawalsPredeployed_Mock"); + const withdrawalsPredeployedAddress = await withdrawalsPredeployed.getAddress(); + + await ethers.provider.send("hardhat_setCode", [ + withdrawalsPredeployedHardcodedAddress, + await ethers.provider.getCode(withdrawalsPredeployedAddress), + ]); + + const contract = await ethers.getContractAt("WithdrawalsPredeployed_Mock", withdrawalsPredeployedHardcodedAddress); + await contract.setFee(1n); + return contract; +} + +function toValidatorPubKey(num: number): string { + if (num < 0 || num > 0xffff) { + throw new Error("Number is out of the 2-byte range (0x0000 - 0xFFFF)."); + } + + return `0x${num.toString(16).padStart(4, "0").repeat(24)}`; +} + +const convertEthToGwei = (ethAmount: string | number): bigint => { + const ethString = ethAmount.toString(); + const wei = ethers.parseEther(ethString); + return wei / 1_000_000_000n; +}; + +function generateWithdrawalRequestPayload(numberOfRequests: number) { + const pubkeys: string[] = []; + const amounts: bigint[] = []; + for (let i = 1; i <= numberOfRequests; i++) { + pubkeys.push(toValidatorPubKey(i)); + amounts.push(convertEthToGwei(i)); + } + + return { pubkeys, amounts }; +} + +async function getFee( + contract: Pick, + requestsCount: number, +): Promise { + const fee = await contract.getWithdrawalRequestFee(); + + return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); +} + +async function getWithdrawalCredentialsContractBalance(contract: BaseContract): Promise { + const contractAddress = await contract.getAddress(); + return await ethers.provider.getBalance(contractAddress); +} + +export function testFullWithdrawalRequestBehavior( + getContract: () => BaseContract & + Pick, + getWithdrawalsPredeployedContract: () => WithdrawalsPredeployed_Mock, +) { + async function addFullWithdrawalRequests(requestCount: number, extraFee: bigint = 0n) { + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(contract); + + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + const fee = (await getFee(contract, pubkeys.length)) + extraFee; + const tx = await contract.addFullWithdrawalRequests(pubkeys, { value: fee }); + + expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); + + const receipt = await tx.wait(); + + const events = findEventsWithInterfaces(receipt!, "WithdrawalRequestAdded", [withdrawalRequestEventInterface]); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[0]).to.equal(pubkeys[i]); + expect(events[i].args[1]).to.equal(0n); + } + } + + context("addFullWithdrawalRequests", () => { + it("Should revert if empty arrays are provided", async function () { + const contract = getContract(); + + await expect(contract.addFullWithdrawalRequests([], { value: 1n })).to.be.revertedWithCustomError( + contract, + "NoWithdrawalRequests", + ); + }); + + it("Should revert if not enough fee is sent", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + const contract = getContract(); + + await getWithdrawalsPredeployedContract().setFee(3n); // Set fee to 3 gwei + + // Should revert if no fee is sent + await expect(contract.addFullWithdrawalRequests(pubkeys)).to.be.revertedWithCustomError(contract, "FeeNotEnough"); + + // Should revert if fee is less than required + const insufficientFee = 2n; + await expect( + contract.addFullWithdrawalRequests(pubkeys, { value: insufficientFee }), + ).to.be.revertedWithCustomError(contract, "FeeNotEnough"); + }); + + it("Should revert if any pubkey is not 48 bytes", async function () { + // Invalid pubkey (only 2 bytes) + const pubkeys = ["0x1234"]; + + const contract = getContract(); + const fee = await getFee(contract, pubkeys.length); + + await expect(contract.addFullWithdrawalRequests(pubkeys, { value: fee })) + .to.be.revertedWithCustomError(contract, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + }); + + it("Should revert if addition fails at the withdrawal request contract", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + const contract = getContract(); + + const fee = await getFee(contract, pubkeys.length); + + // Set mock to fail on add + await getWithdrawalsPredeployedContract().setFailOnAddRequest(true); + + await expect(contract.addFullWithdrawalRequests(pubkeys, { value: fee })).to.be.revertedWithCustomError( + contract, + "WithdrawalRequestAdditionFailed", + ); + }); + + it("Should accept exactly required fee without revert", async function () { + const requestCount = 1; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(contract); + + await getWithdrawalsPredeployedContract().setFee(3n); + expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); + const fee = 3n; + + await contract.addFullWithdrawalRequests(pubkeys, { value: fee }); + + expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); + }); + + it("Should accept exceed fee without revert", async function () { + const requestCount = 1; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(contract); + + await getWithdrawalsPredeployedContract().setFee(3n); + expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); + const fee = 3n + 1n; // 1 request * 3 gwei (fee) + 1 gwei (extra fee)= 4 gwei + + await contract.addFullWithdrawalRequests(pubkeys, { value: fee }); + expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); + }); + + it("Should successfully add requests and emit events", async function () { + await addFullWithdrawalRequests(1); + await addFullWithdrawalRequests(3); + await addFullWithdrawalRequests(10); + await addFullWithdrawalRequests(100); + }); + + it("Should successfully add requests with extra fee and not change contract balance", async function () { + await addFullWithdrawalRequests(1, 100n); + await addFullWithdrawalRequests(3, 1n); + await addFullWithdrawalRequests(10, 1_000_000n); + await addFullWithdrawalRequests(7, 3n); + await addFullWithdrawalRequests(100, 0n); + }); + }); +} + +export function testPartialWithdrawalRequestBehavior( + getContract: () => BaseContract & + Pick, + getWithdrawalsPredeployedContract: () => WithdrawalsPredeployed_Mock, +) { + async function addPartialWithdrawalRequests(requestCount: number, extraFee: bigint = 0n) { + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(contract); + + const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = (await getFee(contract, pubkeys.length)) + extraFee; + const tx = await contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }); + + expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); + + const receipt = await tx.wait(); + + const events = findEventsWithInterfaces(receipt!, "WithdrawalRequestAdded", [withdrawalRequestEventInterface]); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[0]).to.equal(pubkeys[i]); + expect(events[i].args[1]).to.equal(amounts[i]); + } + } + + context("addPartialWithdrawalRequests", () => { + it("Should revert if array lengths do not match or empty arrays are provided", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); + amounts.pop(); + + expect( + pubkeys.length !== amounts.length, + "Test setup error: pubkeys and amounts arrays should have different lengths.", + ); + + const contract = getContract(); + + const fee = await getFee(contract, pubkeys.length); + await expect(contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee })) + .to.be.revertedWithCustomError(contract, "MismatchedArrayLengths") + .withArgs(pubkeys.length, amounts.length); + + // Also test empty arrays + await expect(contract.addPartialWithdrawalRequests([], [], { value: fee })).to.be.revertedWithCustomError( + contract, + "NoWithdrawalRequests", + ); + }); + + it("Should revert if not enough fee is sent", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); + const contract = getContract(); + + await getWithdrawalsPredeployedContract().setFee(3n); // Set fee to 3 gwei + + // Should revert if no fee is sent + await expect(contract.addPartialWithdrawalRequests(pubkeys, amounts)).to.be.revertedWithCustomError( + contract, + "FeeNotEnough", + ); + + // Should revert if fee is less than required + const insufficientFee = 2n; + await expect( + contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: insufficientFee }), + ).to.be.revertedWithCustomError(contract, "FeeNotEnough"); + }); + + it("Should revert if any pubkey is not 48 bytes", async function () { + // Invalid pubkey (only 2 bytes) + const pubkeys = ["0x1234"]; + const amounts = [100n]; + + const contract = getContract(); + const fee = await getFee(contract, pubkeys.length); + + await expect(contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee })) + .to.be.revertedWithCustomError(contract, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + }); + + it("Should revert if addition fails at the withdrawal request contract", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); + const contract = getContract(); + const fee = await getFee(contract, pubkeys.length); + + // Set mock to fail on add + await getWithdrawalsPredeployedContract().setFailOnAddRequest(true); + + await expect( + contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }), + ).to.be.revertedWithCustomError(contract, "WithdrawalRequestAdditionFailed"); + }); + + it("Should revert if full withdrawal requested", async function () { + const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); + amounts[0] = 1n; // Partial withdrawal + amounts[1] = 0n; // Full withdrawal + + const contract = getContract(); + const fee = await getFee(contract, pubkeys.length); + + await expect( + contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }), + ).to.be.revertedWithCustomError(contract, "PartialWithdrawalRequired"); + }); + + it("Should accept exactly required fee without revert", async function () { + const requestCount = 1; + const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); + + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(contract); + + await getWithdrawalsPredeployedContract().setFee(3n); + expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); + const fee = 3n; + + await contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }); + + expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); + }); + + it("Should accept exceed fee without revert", async function () { + const requestCount = 1; + const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); + + const contract = getContract(); + const initialBalance = await getWithdrawalCredentialsContractBalance(contract); + + await getWithdrawalsPredeployedContract().setFee(3n); + expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); + const fee = 3n + 1n; // 1 request * 3 gwei (fee) + 1 gwei (extra fee)= 4 gwei + + await contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }); + expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); + }); + + it("Should successfully add requests and emit events", async function () { + await addPartialWithdrawalRequests(1); + await addPartialWithdrawalRequests(3); + await addPartialWithdrawalRequests(10); + await addPartialWithdrawalRequests(100); + }); + + it("Should successfully add requests with extra fee and not change contract balance", async function () { + await addPartialWithdrawalRequests(1, 100n); + await addPartialWithdrawalRequests(3, 1n); + await addPartialWithdrawalRequests(10, 1_000_000n); + await addPartialWithdrawalRequests(7, 3n); + await addPartialWithdrawalRequests(100, 0n); + }); + }); +} diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts deleted file mode 100644 index 34ff988737..0000000000 --- a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behaviour.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; - -import { findEventsWithInterfaces } from "lib"; - -const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; -const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); - -const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; - -export async function deployWithdrawalsPredeployedMock(): Promise { - const withdrawalsPredeployed = await ethers.deployContract("WithdrawalsPredeployed_Mock"); - const withdrawalsPredeployedAddress = await withdrawalsPredeployed.getAddress(); - - await ethers.provider.send("hardhat_setCode", [ - withdrawalsPredeployedHardcodedAddress, - await ethers.provider.getCode(withdrawalsPredeployedAddress), - ]); - - const contract = await ethers.getContractAt("WithdrawalsPredeployed_Mock", withdrawalsPredeployedHardcodedAddress); - await contract.setFee(1n); - return contract; -} - -function toValidatorPubKey(num: number): string { - if (num < 0 || num > 0xffff) { - throw new Error("Number is out of the 2-byte range (0x0000 - 0xFFFF)."); - } - - return `0x${num.toString(16).padStart(4, "0").repeat(24)}`; -} - -const convertEthToGwei = (ethAmount: string | number): bigint => { - const ethString = ethAmount.toString(); - const wei = ethers.parseEther(ethString); - return wei / 1_000_000_000n; -}; - -function generateWithdrawalRequestPayload(numberOfRequests: number) { - const pubkeys: string[] = []; - const amounts: bigint[] = []; - for (let i = 1; i <= numberOfRequests; i++) { - pubkeys.push(toValidatorPubKey(i)); - amounts.push(convertEthToGwei(i)); - } - - return { pubkeys, amounts }; -} - -export function tesWithdrawalRequestsBehavior( - getContract: () => WithdrawalCredentials_Harness, - getWithdrawalsPredeployedContract: () => WithdrawalsPredeployed_Mock, -) { - async function getFee(requestsCount: number): Promise { - const fee = await getContract().getWithdrawalRequestFee(); - - return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); - } - - async function getWithdrawalCredentialsContractBalance(): Promise { - const contract = getContract(); - const contractAddress = await contract.getAddress(); - return await ethers.provider.getBalance(contractAddress); - } - - async function addWithdrawalRequests(requestCount: number, extraFee: bigint = 0n) { - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(); - - const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); - - const fee = (await getFee(pubkeys.length)) + extraFee; - const tx = await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - - const receipt = await tx.wait(); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - - const events = findEventsWithInterfaces(receipt!, "WithdrawalRequestAdded", [withdrawalRequestEventInterface]); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[0]).to.equal(pubkeys[i]); - expect(events[i].args[1]).to.equal(amounts[i]); - } - } - - context("addWithdrawalRequests", async () => { - it("Should revert if array lengths do not match or empty arrays are provided", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); - amounts.pop(); - - expect( - pubkeys.length !== amounts.length, - "Test setup error: pubkeys and amounts arrays should have different lengths.", - ); - - const contract = getContract(); - - const fee = await getFee(pubkeys.length); - await expect(contract.addWithdrawalRequests(pubkeys, amounts, { value: fee })) - .to.be.revertedWithCustomError(contract, "InvalidArrayLengths") - .withArgs(pubkeys.length, amounts.length); - - // Also test empty arrays - await expect(contract.addWithdrawalRequests([], [], { value: fee })) - .to.be.revertedWithCustomError(contract, "InvalidArrayLengths") - .withArgs(0, 0); - }); - - it("Should revert if not enough fee is sent", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); - const contract = getContract(); - - await getWithdrawalsPredeployedContract().setFee(3n); // Set fee to 3 gwei - - // Should revert if no fee is sent - await expect(contract.addWithdrawalRequests(pubkeys, amounts)).to.be.revertedWithCustomError( - contract, - "FeeNotEnough", - ); - - // Should revert if fee is less than required - const insufficientFee = 2n; - await expect( - contract.addWithdrawalRequests(pubkeys, amounts, { value: insufficientFee }), - ).to.be.revertedWithCustomError(contract, "FeeNotEnough"); - }); - - it("Should revert if any pubkey is not 48 bytes", async function () { - // Invalid pubkey (only 2 bytes) - const pubkeys = ["0x1234"]; - const amounts = [100n]; - - const fee = await getFee(pubkeys.length); - const contract = getContract(); - await expect(contract.addWithdrawalRequests(pubkeys, amounts, { value: fee })) - .to.be.revertedWithCustomError(contract, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); - }); - - it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); - const fee = await getFee(pubkeys.length); - - // Set mock to fail on add - await getWithdrawalsPredeployedContract().setFailOnAddRequest(true); - const contract = getContract(); - - await expect(contract.addWithdrawalRequests(pubkeys, amounts, { value: fee })).to.be.revertedWithCustomError( - contract, - "WithdrawalRequestAdditionFailed", - ); - }); - - it("Should accept full and partial withdrawals", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); - amounts[0] = 0n; // Full withdrawal - amounts[1] = 1n; // Partial withdrawal - - const fee = await getFee(pubkeys.length); - const contract = getContract(); - - await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); - }); - - it("Should accept exactly required fee without revert", async function () { - const requestCount = 1; - const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); - - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(); - - await getWithdrawalsPredeployedContract().setFee(3n); - expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); - const fee = 3n; - - await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - }); - - it("Should accept exceed fee without revert", async function () { - const requestCount = 1; - const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); - - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(); - - await getWithdrawalsPredeployedContract().setFee(3n); - expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); - const fee = 3n + 1n; // 1 request * 3 gwei (fee) + 1 gwei (extra fee)= 4 gwei - - await contract.addWithdrawalRequests(pubkeys, amounts, { value: fee }); - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - }); - - it("Should successfully add requests and emit events", async function () { - await addWithdrawalRequests(1); - await addWithdrawalRequests(3); - await addWithdrawalRequests(10); - await addWithdrawalRequests(100); - }); - - it("Should successfully add requests with extra fee and not change contract balance", async function () { - await addWithdrawalRequests(1, 100n); - await addWithdrawalRequests(3, 1n); - await addWithdrawalRequests(10, 1_000_000n); - await addWithdrawalRequests(7, 3n); - await addWithdrawalRequests(100, 0n); - }); - }); -} diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 9f1d80aa4f..8180362011 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -19,8 +19,8 @@ import { Snapshot } from "test/suite"; import { deployWithdrawalsPredeployedMock, - tesWithdrawalRequestsBehavior, -} from "./lib/withdrawalCredentials/withdrawalRequests.behaviour"; + testFullWithdrawalRequestBehavior, +} from "./lib/withdrawalCredentials/withdrawalRequests.behavior"; const PETRIFIED_VERSION = MAX_UINT256; @@ -41,9 +41,6 @@ describe("WithdrawalVault.sol", () => { let vault: WithdrawalVault; let vaultAddress: string; - const getWithdrawalCredentialsContract = () => vault.connect(validatorsExitBus); - const getWithdrawalsPredeployedContract = () => withdrawalsPredeployed.connect(user); - before(async () => { [owner, user, treasury, validatorsExitBus] = await ethers.getSigners(); @@ -200,12 +197,15 @@ describe("WithdrawalVault.sol", () => { context("addWithdrawalRequests", () => { it("Reverts if the caller is not Validator Exit Bus", async () => { - await expect(vault.connect(user).addWithdrawalRequests(["0x1234"], [0n])).to.be.revertedWithCustomError( + await expect(vault.connect(user).addFullWithdrawalRequests(["0x1234"])).to.be.revertedWithCustomError( vault, "NotValidatorExitBus", ); }); - tesWithdrawalRequestsBehavior(getWithdrawalCredentialsContract, getWithdrawalsPredeployedContract); + testFullWithdrawalRequestBehavior( + () => vault.connect(validatorsExitBus), + () => withdrawalsPredeployed.connect(user), + ); }); }); From 4420a7cb4e616f94151f4d957c0f9fb1d6653b4b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Sat, 21 Dec 2024 21:16:52 +0100 Subject: [PATCH 008/405] feat: decouple fee allocation strategy from withdrawal request library --- contracts/0.8.9/WithdrawalVault.sol | 10 +- contracts/0.8.9/lib/WithdrawalRequests.sol | 72 +++- .../WithdrawalCredentials_Harness.sol | 28 +- .../lib/withdrawalCredentials/findEvents.ts | 13 + .../withdrawalCredentials.test.ts | 394 +++++++++++++++++- .../withdrawalRequests.behavior.ts | 329 +-------------- test/0.8.9/withdrawalVault.test.ts | 13 +- 7 files changed, 493 insertions(+), 366 deletions(-) create mode 100644 test/0.8.9/lib/withdrawalCredentials/findEvents.ts diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index bc6d87e766..0c5eaa1638 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -55,9 +55,9 @@ contract WithdrawalVault is Versioned { * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) */ constructor(address _lido, address _treasury, address _validatorsExitBus) { - _assertNonZero(_lido); - _assertNonZero(_treasury); - _assertNonZero(_validatorsExitBus); + _requireNonZero(_lido); + _requireNonZero(_treasury); + _requireNonZero(_validatorsExitBus); LIDO = ILido(_lido); TREASURY = _treasury; @@ -141,14 +141,14 @@ contract WithdrawalVault is Versioned { revert NotValidatorExitBus(); } - WithdrawalRequests.addFullWithdrawalRequests(pubkeys); + WithdrawalRequests.addFullWithdrawalRequests(pubkeys, msg.value); } function getWithdrawalRequestFee() external view returns (uint256) { return WithdrawalRequests.getWithdrawalRequestFee(); } - function _assertNonZero(address _address) internal pure { + function _requireNonZero(address _address) internal pure { if (_address == address(0)) revert ZeroAddress(); } } diff --git a/contracts/0.8.9/lib/WithdrawalRequests.sol b/contracts/0.8.9/lib/WithdrawalRequests.sol index 7973f118d7..8d0bc09791 100644 --- a/contracts/0.8.9/lib/WithdrawalRequests.sol +++ b/contracts/0.8.9/lib/WithdrawalRequests.sol @@ -7,7 +7,8 @@ library WithdrawalRequests { address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); - error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); + error InsufficientBalance(uint256 balance, uint256 totalWithdrawalFee); + error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 providedTotalFee); error WithdrawalRequestFeeReadFailed(); error InvalidPubkeyLength(bytes pubkey); @@ -23,17 +24,17 @@ library WithdrawalRequests { * @param pubkeys An array of public keys for the validators requesting full withdrawals. */ function addFullWithdrawalRequests( - bytes[] calldata pubkeys + bytes[] calldata pubkeys, + uint256 totalWithdrawalFee ) internal { - uint256 keysCount = pubkeys.length; - uint64[] memory amounts = new uint64[](keysCount); - - _addWithdrawalRequests(pubkeys, amounts); + uint64[] memory amounts = new uint64[](pubkeys.length); + _addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); } /** * @dev Adds partial withdrawal requests for the provided public keys with corresponding amounts. * A partial withdrawal is any withdrawal where the amount is greater than zero. + * A full withdrawal is any withdrawal where the amount is zero. * This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn). * However, the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. * @param pubkeys An array of public keys for the validators requesting withdrawals. @@ -41,23 +42,35 @@ library WithdrawalRequests { */ function addPartialWithdrawalRequests( bytes[] calldata pubkeys, - uint64[] calldata amounts + uint64[] calldata amounts, + uint256 totalWithdrawalFee ) internal { - uint256 keysCount = pubkeys.length; - if (keysCount != amounts.length) { - revert MismatchedArrayLengths(keysCount, amounts.length); - } + _requireArrayLengthsMatch(pubkeys, amounts); - uint64[] memory _amounts = new uint64[](keysCount); - for (uint256 i = 0; i < keysCount; i++) { + for (uint256 i = 0; i < amounts.length; i++) { if (amounts[i] == 0) { revert PartialWithdrawalRequired(pubkeys[i]); } - - _amounts[i] = amounts[i]; } - _addWithdrawalRequests(pubkeys, _amounts); + _addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + } + + /** + * @dev Adds partial or full withdrawal requests for the provided public keys with corresponding amounts. + * A partial withdrawal is any withdrawal where the amount is greater than zero. + * This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn). + * However, the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. + * @param pubkeys An array of public keys for the validators requesting withdrawals. + * @param amounts An array of corresponding withdrawal amounts for each public key. + */ + function addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts, + uint256 totalWithdrawalFee + ) internal { + _requireArrayLengthsMatch(pubkeys, amounts); + _addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); } /** @@ -76,22 +89,26 @@ library WithdrawalRequests { function _addWithdrawalRequests( bytes[] calldata pubkeys, - uint64[] memory amounts + uint64[] memory amounts, + uint256 totalWithdrawalFee ) internal { uint256 keysCount = pubkeys.length; if (keysCount == 0) { revert NoWithdrawalRequests(); } - uint256 minFeePerRequest = getWithdrawalRequestFee(); - if (minFeePerRequest * keysCount > msg.value) { - revert FeeNotEnough(minFeePerRequest, keysCount, msg.value); + if(address(this).balance < totalWithdrawalFee) { + revert InsufficientBalance(address(this).balance, totalWithdrawalFee); } - uint256 feePerRequest = msg.value / keysCount; - uint256 unallocatedFee = msg.value % keysCount; - uint256 prevBalance = address(this).balance - msg.value; + uint256 minFeePerRequest = getWithdrawalRequestFee(); + if (minFeePerRequest * keysCount > totalWithdrawalFee) { + revert FeeNotEnough(minFeePerRequest, keysCount, totalWithdrawalFee); + } + uint256 feePerRequest = totalWithdrawalFee / keysCount; + uint256 unallocatedFee = totalWithdrawalFee % keysCount; + uint256 prevBalance = address(this).balance - totalWithdrawalFee; for (uint256 i = 0; i < keysCount; ++i) { bytes memory pubkey = pubkeys[i]; @@ -119,4 +136,13 @@ library WithdrawalRequests { assert(address(this).balance == prevBalance); } + + function _requireArrayLengthsMatch( + bytes[] calldata pubkeys, + uint64[] calldata amounts + ) internal pure { + if (pubkeys.length != amounts.length) { + revert MismatchedArrayLengths(pubkeys.length, amounts.length); + } + } } diff --git a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol b/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol index 1450f79e91..b5e55c299a 100644 --- a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol +++ b/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol @@ -4,19 +4,35 @@ import {WithdrawalRequests} from "contracts/0.8.9/lib/WithdrawalRequests.sol"; contract WithdrawalCredentials_Harness { function addFullWithdrawalRequests( - bytes[] calldata pubkeys - ) external payable { - WithdrawalRequests.addFullWithdrawalRequests(pubkeys); + bytes[] calldata pubkeys, + uint256 totalWithdrawalFee + ) external { + WithdrawalRequests.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee); } function addPartialWithdrawalRequests( bytes[] calldata pubkeys, - uint64[] calldata amounts - ) external payable { - WithdrawalRequests.addPartialWithdrawalRequests(pubkeys, amounts); + uint64[] calldata amounts, + uint256 totalWithdrawalFee + ) external { + WithdrawalRequests.addPartialWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + } + + function addWithdrawalRequests( + bytes[] calldata pubkeys, + uint64[] calldata amounts, + uint256 totalWithdrawalFee + ) external { + WithdrawalRequests.addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); } function getWithdrawalRequestFee() external view returns (uint256) { return WithdrawalRequests.getWithdrawalRequestFee(); } + + function getWithdrawalsContractAddress() public pure returns (address) { + return WithdrawalRequests.WITHDRAWAL_REQUEST; + } + + function deposit() external payable {} } diff --git a/test/0.8.9/lib/withdrawalCredentials/findEvents.ts b/test/0.8.9/lib/withdrawalCredentials/findEvents.ts new file mode 100644 index 0000000000..9ee2581399 --- /dev/null +++ b/test/0.8.9/lib/withdrawalCredentials/findEvents.ts @@ -0,0 +1,13 @@ +import { ContractTransactionReceipt } from "ethers"; +import { ethers } from "hardhat"; + +import { findEventsWithInterfaces } from "lib"; + +const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; +const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); + +type WithdrawalRequestEvents = "WithdrawalRequestAdded"; + +export function findEvents(receipt: ContractTransactionReceipt, event: WithdrawalRequestEvents) { + return findEventsWithInterfaces(receipt!, event, [withdrawalRequestEventInterface]); +} diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts index 744519a3f5..2ee973b678 100644 --- a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts +++ b/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts @@ -1,15 +1,19 @@ +import { expect } from "chai"; +import { ContractTransactionResponse } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; import { Snapshot } from "test/suite"; +import { findEvents } from "./findEvents"; import { deployWithdrawalsPredeployedMock, - testFullWithdrawalRequestBehavior, - testPartialWithdrawalRequestBehavior, + generateWithdrawalRequestPayload, + withdrawalsPredeployedHardcodedAddress, } from "./withdrawalRequests.behavior"; describe("WithdrawalCredentials.sol", () => { @@ -20,24 +24,392 @@ describe("WithdrawalCredentials.sol", () => { let originalState: string; + async function getWithdrawalCredentialsContractBalance(): Promise { + const contractAddress = await withdrawalCredentials.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + + async function getWithdrawalsPredeployedContractBalance(): Promise { + const contractAddress = await withdrawalsPredeployed.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + before(async () => { [actor] = await ethers.getSigners(); - withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(); + withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(1n); withdrawalCredentials = await ethers.deployContract("WithdrawalCredentials_Harness"); + + expect(await withdrawalsPredeployed.getAddress()).to.equal(withdrawalsPredeployedHardcodedAddress); + + await withdrawalCredentials.connect(actor).deposit({ value: ethers.parseEther("1") }); }); beforeEach(async () => (originalState = await Snapshot.take())); afterEach(async () => await Snapshot.restore(originalState)); - testFullWithdrawalRequestBehavior( - () => withdrawalCredentials.connect(actor), - () => withdrawalsPredeployed.connect(actor), - ); + async function getFee(requestsCount: number): Promise { + const fee = await withdrawalCredentials.getWithdrawalRequestFee(); + + return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); + } + + context("eip 7002 contract", () => { + it("Should return the address of the EIP 7002 contract", async function () { + expect(await withdrawalCredentials.getWithdrawalsContractAddress()).to.equal( + withdrawalsPredeployedHardcodedAddress, + ); + }); + }); + + context("get withdrawal request fee", () => { + it("Should get fee from the EIP 7002 contract", async function () { + await withdrawalsPredeployed.setFee(333n); + expect( + (await withdrawalCredentials.getWithdrawalRequestFee()) == 333n, + "withdrawal request should use fee from the EIP 7002 contract", + ); + }); + + it("Should revert if fee read fails", async function () { + await withdrawalsPredeployed.setFailOnGetFee(true); + await expect(withdrawalCredentials.getWithdrawalRequestFee()).to.be.revertedWithCustomError( + withdrawalCredentials, + "WithdrawalRequestFeeReadFailed", + ); + }); + }); + + context("add withdrawal requests", () => { + it("Should revert if empty arrays are provided", async function () { + await expect(withdrawalCredentials.addFullWithdrawalRequests([], 1n)).to.be.revertedWithCustomError( + withdrawalCredentials, + "NoWithdrawalRequests", + ); + + await expect(withdrawalCredentials.addPartialWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( + withdrawalCredentials, + "NoWithdrawalRequests", + ); + + await expect(withdrawalCredentials.addWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( + withdrawalCredentials, + "NoWithdrawalRequests", + ); + }); + + it("Should revert if array lengths do not match", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(2); + const amounts = [1n]; + + const fee = await getFee(pubkeys.length); + + await expect(withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "MismatchedArrayLengths") + .withArgs(pubkeys.length, amounts.length); + + await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "MismatchedArrayLengths") + .withArgs(pubkeys.length, amounts.length); + }); + + it("Should revert if not enough fee is sent", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + const amounts = [10n]; + + await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei + + // 1. Should revert if no fee is sent + await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, 0n)).to.be.revertedWithCustomError( + withdrawalCredentials, + "FeeNotEnough", + ); + + await expect( + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, 0n), + ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + + await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, 0n)).to.be.revertedWithCustomError( + withdrawalCredentials, + "FeeNotEnough", + ); + + // 2. Should revert if fee is less than required + const insufficientFee = 2n; + await expect( + withdrawalCredentials.addFullWithdrawalRequests(pubkeys, insufficientFee), + ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + + await expect( + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, insufficientFee), + ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + + await expect( + withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, insufficientFee), + ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + }); + + it("Should revert if any pubkey is not 48 bytes", async function () { + // Invalid pubkey (only 2 bytes) + const pubkeys = ["0x1234"]; + const amounts = [10n]; + + const fee = await getFee(pubkeys.length); + + await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + + await expect(withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + + await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + }); + + it("Should revert if addition fails at the withdrawal request contract", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + const amounts = [10n]; + + const fee = await getFee(pubkeys.length); + + // Set mock to fail on add + await withdrawalsPredeployed.setFailOnAddRequest(true); + + await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee)).to.be.revertedWithCustomError( + withdrawalCredentials, + "WithdrawalRequestAdditionFailed", + ); + + await expect( + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee), + ).to.be.revertedWithCustomError(withdrawalCredentials, "WithdrawalRequestAdditionFailed"); + + await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, fee)).to.be.revertedWithCustomError( + withdrawalCredentials, + "WithdrawalRequestAdditionFailed", + ); + }); + + it("Should revert if full withdrawal requested in 'addPartialWithdrawalRequests'", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(2); + const amounts = [1n, 0n]; // Partial and Full withdrawal + const fee = await getFee(pubkeys.length); + + await expect( + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee), + ).to.be.revertedWithCustomError(withdrawalCredentials, "PartialWithdrawalRequired"); + }); - testPartialWithdrawalRequestBehavior( - () => withdrawalCredentials.connect(actor), - () => withdrawalsPredeployed.connect(actor), - ); + it("Should revert if contract balance insufficient'", async function () { + const { pubkeys, partialWithdrawalAmounts, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const fee = 10n; + const totalWithdrawalFee = 20n; + const balance = 19n; + + await withdrawalsPredeployed.setFee(fee); + await setBalance(await withdrawalCredentials.getAddress(), balance); + + await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "InsufficientBalance") + .withArgs(balance, totalWithdrawalFee); + + await expect( + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + ) + .to.be.revertedWithCustomError(withdrawalCredentials, "InsufficientBalance") + .withArgs(balance, totalWithdrawalFee); + + await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, totalWithdrawalFee)) + .to.be.revertedWithCustomError(withdrawalCredentials, "InsufficientBalance") + .withArgs(balance, totalWithdrawalFee); + }); + + it("Should accept exactly required fee without revert", async function () { + const requestCount = 3; + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const fee = 9n; + + await withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee); + await withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + }); + + it("Should accept exceed fee without revert", async function () { + const requestCount = 3; + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const fee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei + + await withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee); + await withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + }); + + it("Should deduct precise fee value from contract balance", async function () { + const requestCount = 3; + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const fee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei + + const testFeeDeduction = async (addRequests: () => Promise) => { + const initialBalance = await getWithdrawalCredentialsContractBalance(); + await addRequests(); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - fee); + }; + + await testFeeDeduction(() => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee)); + await testFeeDeduction(() => + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + ); + await testFeeDeduction(() => withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); + }); + + it("Should send all fee to eip 7002 withdrawal contract", async function () { + const requestCount = 3; + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const totalWithdrawalFee = 9n + 1n; + + const testFeeTransfer = async (addRequests: () => Promise) => { + const initialBalance = await getWithdrawalsPredeployedContractBalance(); + await addRequests(); + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + totalWithdrawalFee); + }; + + await testFeeTransfer(() => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)); + await testFeeTransfer(() => + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + ); + await testFeeTransfer(() => + withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + ); + }); + + it("should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { + const requestCount = 3; + const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + const fee = 10n; + + const testEventsEmit = async ( + addRequests: () => Promise, + expectedPubKeys: string[], + expectedAmounts: bigint[], + ) => { + const tx = await addRequests(); + + const receipt = await tx.wait(); + const events = findEvents(receipt!, "WithdrawalRequestAdded"); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[0]).to.equal(expectedPubKeys[i]); + expect(events[i].args[1]).to.equal(expectedAmounts[i]); + } + }; + + await testEventsEmit( + () => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee), + pubkeys, + fullWithdrawalAmounts, + ); + await testEventsEmit( + () => withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + pubkeys, + partialWithdrawalAmounts, + ); + await testEventsEmit( + () => withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), + pubkeys, + mixedWithdrawalAmounts, + ); + }); + + async function addWithdrawalRequests( + addRequests: () => Promise, + expectedPubkeys: string[], + expectedAmounts: bigint[], + expectedTotalWithdrawalFee: bigint, + ) { + const initialBalance = await getWithdrawalCredentialsContractBalance(); + + const tx = await addRequests(); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); + + const receipt = await tx.wait(); + + const events = findEvents(receipt!, "WithdrawalRequestAdded"); + expect(events.length).to.equal(expectedPubkeys.length); + + for (let i = 0; i < expectedPubkeys.length; i++) { + expect(events[i].args[0]).to.equal(expectedPubkeys[i]); + expect(events[i].args[1]).to.equal(expectedAmounts[i]); + } + } + + const testCasesForWithdrawalRequests = [ + { requestCount: 1, extraFee: 0n }, + { requestCount: 1, extraFee: 100n }, + { requestCount: 3, extraFee: 0n }, + { requestCount: 3, extraFee: 1n }, + { requestCount: 7, extraFee: 3n }, + { requestCount: 10, extraFee: 0n }, + { requestCount: 10, extraFee: 1_000_000n }, + { requestCount: 100, extraFee: 0n }, + ]; + + testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { + it(`Should successfully add ${requestCount} requests with extra fee ${extraFee} and emit events`, async () => { + const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + const totalWithdrawalFee = (await getFee(pubkeys.length)) + extraFee; + + await addWithdrawalRequests( + () => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), + pubkeys, + fullWithdrawalAmounts, + totalWithdrawalFee, + ); + + await addWithdrawalRequests( + () => + withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + pubkeys, + partialWithdrawalAmounts, + totalWithdrawalFee, + ); + + await addWithdrawalRequests( + () => withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + pubkeys, + mixedWithdrawalAmounts, + totalWithdrawalFee, + ); + }); + }); + + it("Should accept full and partial withdrawals requested", async function () { + const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(3); + const fee = await getFee(pubkeys.length); + + await withdrawalCredentials.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); + await withdrawalCredentials.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + }); + }); }); diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts index 7eeafea9f6..105c23e474 100644 --- a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts +++ b/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts @@ -1,17 +1,12 @@ -import { expect } from "chai"; -import { BaseContract } from "ethers"; import { ethers } from "hardhat"; -import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; +import { WithdrawalsPredeployed_Mock } from "typechain-types"; -import { findEventsWithInterfaces } from "lib"; +export const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; -const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; -const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); - -const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; - -export async function deployWithdrawalsPredeployedMock(): Promise { +export async function deployWithdrawalsPredeployedMock( + defaultRequestFee: bigint, +): Promise { const withdrawalsPredeployed = await ethers.deployContract("WithdrawalsPredeployed_Mock"); const withdrawalsPredeployedAddress = await withdrawalsPredeployed.getAddress(); @@ -21,7 +16,7 @@ export async function deployWithdrawalsPredeployedMock(): Promise { return wei / 1_000_000_000n; }; -function generateWithdrawalRequestPayload(numberOfRequests: number) { +export function generateWithdrawalRequestPayload(numberOfRequests: number) { const pubkeys: string[] = []; - const amounts: bigint[] = []; + const fullWithdrawalAmounts: bigint[] = []; + const partialWithdrawalAmounts: bigint[] = []; + const mixedWithdrawalAmounts: bigint[] = []; + for (let i = 1; i <= numberOfRequests; i++) { pubkeys.push(toValidatorPubKey(i)); - amounts.push(convertEthToGwei(i)); - } - - return { pubkeys, amounts }; -} - -async function getFee( - contract: Pick, - requestsCount: number, -): Promise { - const fee = await contract.getWithdrawalRequestFee(); - - return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); -} - -async function getWithdrawalCredentialsContractBalance(contract: BaseContract): Promise { - const contractAddress = await contract.getAddress(); - return await ethers.provider.getBalance(contractAddress); -} - -export function testFullWithdrawalRequestBehavior( - getContract: () => BaseContract & - Pick, - getWithdrawalsPredeployedContract: () => WithdrawalsPredeployed_Mock, -) { - async function addFullWithdrawalRequests(requestCount: number, extraFee: bigint = 0n) { - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(contract); - - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - - const fee = (await getFee(contract, pubkeys.length)) + extraFee; - const tx = await contract.addFullWithdrawalRequests(pubkeys, { value: fee }); - - expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); - - const receipt = await tx.wait(); - - const events = findEventsWithInterfaces(receipt!, "WithdrawalRequestAdded", [withdrawalRequestEventInterface]); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[0]).to.equal(pubkeys[i]); - expect(events[i].args[1]).to.equal(0n); - } + fullWithdrawalAmounts.push(0n); + partialWithdrawalAmounts.push(convertEthToGwei(i)); + mixedWithdrawalAmounts.push(i % 2 === 0 ? 0n : convertEthToGwei(i)); } - context("addFullWithdrawalRequests", () => { - it("Should revert if empty arrays are provided", async function () { - const contract = getContract(); - - await expect(contract.addFullWithdrawalRequests([], { value: 1n })).to.be.revertedWithCustomError( - contract, - "NoWithdrawalRequests", - ); - }); - - it("Should revert if not enough fee is sent", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); - const contract = getContract(); - - await getWithdrawalsPredeployedContract().setFee(3n); // Set fee to 3 gwei - - // Should revert if no fee is sent - await expect(contract.addFullWithdrawalRequests(pubkeys)).to.be.revertedWithCustomError(contract, "FeeNotEnough"); - - // Should revert if fee is less than required - const insufficientFee = 2n; - await expect( - contract.addFullWithdrawalRequests(pubkeys, { value: insufficientFee }), - ).to.be.revertedWithCustomError(contract, "FeeNotEnough"); - }); - - it("Should revert if any pubkey is not 48 bytes", async function () { - // Invalid pubkey (only 2 bytes) - const pubkeys = ["0x1234"]; - - const contract = getContract(); - const fee = await getFee(contract, pubkeys.length); - - await expect(contract.addFullWithdrawalRequests(pubkeys, { value: fee })) - .to.be.revertedWithCustomError(contract, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); - }); - - it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); - const contract = getContract(); - - const fee = await getFee(contract, pubkeys.length); - - // Set mock to fail on add - await getWithdrawalsPredeployedContract().setFailOnAddRequest(true); - - await expect(contract.addFullWithdrawalRequests(pubkeys, { value: fee })).to.be.revertedWithCustomError( - contract, - "WithdrawalRequestAdditionFailed", - ); - }); - - it("Should accept exactly required fee without revert", async function () { - const requestCount = 1; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(contract); - - await getWithdrawalsPredeployedContract().setFee(3n); - expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); - const fee = 3n; - - await contract.addFullWithdrawalRequests(pubkeys, { value: fee }); - - expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); - }); - - it("Should accept exceed fee without revert", async function () { - const requestCount = 1; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(contract); - - await getWithdrawalsPredeployedContract().setFee(3n); - expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); - const fee = 3n + 1n; // 1 request * 3 gwei (fee) + 1 gwei (extra fee)= 4 gwei - - await contract.addFullWithdrawalRequests(pubkeys, { value: fee }); - expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); - }); - - it("Should successfully add requests and emit events", async function () { - await addFullWithdrawalRequests(1); - await addFullWithdrawalRequests(3); - await addFullWithdrawalRequests(10); - await addFullWithdrawalRequests(100); - }); - - it("Should successfully add requests with extra fee and not change contract balance", async function () { - await addFullWithdrawalRequests(1, 100n); - await addFullWithdrawalRequests(3, 1n); - await addFullWithdrawalRequests(10, 1_000_000n); - await addFullWithdrawalRequests(7, 3n); - await addFullWithdrawalRequests(100, 0n); - }); - }); -} - -export function testPartialWithdrawalRequestBehavior( - getContract: () => BaseContract & - Pick, - getWithdrawalsPredeployedContract: () => WithdrawalsPredeployed_Mock, -) { - async function addPartialWithdrawalRequests(requestCount: number, extraFee: bigint = 0n) { - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(contract); - - const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); - - const fee = (await getFee(contract, pubkeys.length)) + extraFee; - const tx = await contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }); - - expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); - - const receipt = await tx.wait(); - - const events = findEventsWithInterfaces(receipt!, "WithdrawalRequestAdded", [withdrawalRequestEventInterface]); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[0]).to.equal(pubkeys[i]); - expect(events[i].args[1]).to.equal(amounts[i]); - } - } - - context("addPartialWithdrawalRequests", () => { - it("Should revert if array lengths do not match or empty arrays are provided", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); - amounts.pop(); - - expect( - pubkeys.length !== amounts.length, - "Test setup error: pubkeys and amounts arrays should have different lengths.", - ); - - const contract = getContract(); - - const fee = await getFee(contract, pubkeys.length); - await expect(contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee })) - .to.be.revertedWithCustomError(contract, "MismatchedArrayLengths") - .withArgs(pubkeys.length, amounts.length); - - // Also test empty arrays - await expect(contract.addPartialWithdrawalRequests([], [], { value: fee })).to.be.revertedWithCustomError( - contract, - "NoWithdrawalRequests", - ); - }); - - it("Should revert if not enough fee is sent", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); - const contract = getContract(); - - await getWithdrawalsPredeployedContract().setFee(3n); // Set fee to 3 gwei - - // Should revert if no fee is sent - await expect(contract.addPartialWithdrawalRequests(pubkeys, amounts)).to.be.revertedWithCustomError( - contract, - "FeeNotEnough", - ); - - // Should revert if fee is less than required - const insufficientFee = 2n; - await expect( - contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: insufficientFee }), - ).to.be.revertedWithCustomError(contract, "FeeNotEnough"); - }); - - it("Should revert if any pubkey is not 48 bytes", async function () { - // Invalid pubkey (only 2 bytes) - const pubkeys = ["0x1234"]; - const amounts = [100n]; - - const contract = getContract(); - const fee = await getFee(contract, pubkeys.length); - - await expect(contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee })) - .to.be.revertedWithCustomError(contract, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); - }); - - it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(1); - const contract = getContract(); - const fee = await getFee(contract, pubkeys.length); - - // Set mock to fail on add - await getWithdrawalsPredeployedContract().setFailOnAddRequest(true); - - await expect( - contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }), - ).to.be.revertedWithCustomError(contract, "WithdrawalRequestAdditionFailed"); - }); - - it("Should revert if full withdrawal requested", async function () { - const { pubkeys, amounts } = generateWithdrawalRequestPayload(2); - amounts[0] = 1n; // Partial withdrawal - amounts[1] = 0n; // Full withdrawal - - const contract = getContract(); - const fee = await getFee(contract, pubkeys.length); - - await expect( - contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }), - ).to.be.revertedWithCustomError(contract, "PartialWithdrawalRequired"); - }); - - it("Should accept exactly required fee without revert", async function () { - const requestCount = 1; - const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); - - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(contract); - - await getWithdrawalsPredeployedContract().setFee(3n); - expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); - const fee = 3n; - - await contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }); - - expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); - }); - - it("Should accept exceed fee without revert", async function () { - const requestCount = 1; - const { pubkeys, amounts } = generateWithdrawalRequestPayload(requestCount); - - const contract = getContract(); - const initialBalance = await getWithdrawalCredentialsContractBalance(contract); - - await getWithdrawalsPredeployedContract().setFee(3n); - expect((await contract.getWithdrawalRequestFee()) == 3n, "Test setup error: invalid withdrawal request fee."); - const fee = 3n + 1n; // 1 request * 3 gwei (fee) + 1 gwei (extra fee)= 4 gwei - - await contract.addPartialWithdrawalRequests(pubkeys, amounts, { value: fee }); - expect(await getWithdrawalCredentialsContractBalance(contract)).to.equal(initialBalance); - }); - - it("Should successfully add requests and emit events", async function () { - await addPartialWithdrawalRequests(1); - await addPartialWithdrawalRequests(3); - await addPartialWithdrawalRequests(10); - await addPartialWithdrawalRequests(100); - }); - - it("Should successfully add requests with extra fee and not change contract balance", async function () { - await addPartialWithdrawalRequests(1, 100n); - await addPartialWithdrawalRequests(3, 1n); - await addPartialWithdrawalRequests(10, 1_000_000n); - await addPartialWithdrawalRequests(7, 3n); - await addPartialWithdrawalRequests(100, 0n); - }); - }); + return { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts }; } diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 8180362011..85396970d9 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -19,7 +19,7 @@ import { Snapshot } from "test/suite"; import { deployWithdrawalsPredeployedMock, - testFullWithdrawalRequestBehavior, + withdrawalsPredeployedHardcodedAddress, } from "./lib/withdrawalCredentials/withdrawalRequests.behavior"; const PETRIFIED_VERSION = MAX_UINT256; @@ -44,7 +44,9 @@ describe("WithdrawalVault.sol", () => { before(async () => { [owner, user, treasury, validatorsExitBus] = await ethers.getSigners(); - withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(); + withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(1n); + + expect(await withdrawalsPredeployed.getAddress()).to.equal(withdrawalsPredeployedHardcodedAddress); lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); lidoAddress = await lido.getAddress(); @@ -195,7 +197,7 @@ describe("WithdrawalVault.sol", () => { }); }); - context("addWithdrawalRequests", () => { + context("eip 7002 withdrawal requests", () => { it("Reverts if the caller is not Validator Exit Bus", async () => { await expect(vault.connect(user).addFullWithdrawalRequests(["0x1234"])).to.be.revertedWithCustomError( vault, @@ -203,9 +205,6 @@ describe("WithdrawalVault.sol", () => { ); }); - testFullWithdrawalRequestBehavior( - () => vault.connect(validatorsExitBus), - () => withdrawalsPredeployed.connect(user), - ); + // ToDo: add tests... }); }); From 1a394bfaed7d32e48f570011367520caf2579df1 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 23 Dec 2024 14:17:02 +0100 Subject: [PATCH 009/405] feat: rename triggerable withdrawals lib --- contracts/0.8.9/WithdrawalVault.sol | 6 +- ...equests.sol => TriggerableWithdrawals.sol} | 2 +- ...sol => TriggerableWithdrawals_Harness.sol} | 14 +- .../findEvents.ts | 0 .../triggerableWithdrawals.test.ts} | 152 +++++++++--------- .../utils.ts} | 0 test/0.8.9/withdrawalVault.test.ts | 4 +- 7 files changed, 89 insertions(+), 89 deletions(-) rename contracts/0.8.9/lib/{WithdrawalRequests.sol => TriggerableWithdrawals.sol} (99%) rename test/0.8.9/contracts/{WithdrawalCredentials_Harness.sol => TriggerableWithdrawals_Harness.sol} (56%) rename test/0.8.9/lib/{withdrawalCredentials => triggerableWithdrawals}/findEvents.ts (100%) rename test/0.8.9/lib/{withdrawalCredentials/withdrawalCredentials.test.ts => triggerableWithdrawals/triggerableWithdrawals.test.ts} (61%) rename test/0.8.9/lib/{withdrawalCredentials/withdrawalRequests.behavior.ts => triggerableWithdrawals/utils.ts} (100%) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 0c5eaa1638..9789bf54a4 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -9,7 +9,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; -import {WithdrawalRequests} from "./lib/WithdrawalRequests.sol"; +import {TriggerableWithdrawals} from "./lib/TriggerableWithdrawals.sol"; interface ILido { /** @@ -141,11 +141,11 @@ contract WithdrawalVault is Versioned { revert NotValidatorExitBus(); } - WithdrawalRequests.addFullWithdrawalRequests(pubkeys, msg.value); + TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, msg.value); } function getWithdrawalRequestFee() external view returns (uint256) { - return WithdrawalRequests.getWithdrawalRequestFee(); + return TriggerableWithdrawals.getWithdrawalRequestFee(); } function _requireNonZero(address _address) internal pure { diff --git a/contracts/0.8.9/lib/WithdrawalRequests.sol b/contracts/0.8.9/lib/TriggerableWithdrawals.sol similarity index 99% rename from contracts/0.8.9/lib/WithdrawalRequests.sol rename to contracts/0.8.9/lib/TriggerableWithdrawals.sol index 8d0bc09791..ab46819837 100644 --- a/contracts/0.8.9/lib/WithdrawalRequests.sol +++ b/contracts/0.8.9/lib/TriggerableWithdrawals.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.9; -library WithdrawalRequests { +library TriggerableWithdrawals { address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); diff --git a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol similarity index 56% rename from test/0.8.9/contracts/WithdrawalCredentials_Harness.sol rename to test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol index b5e55c299a..261f1a8cd0 100644 --- a/test/0.8.9/contracts/WithdrawalCredentials_Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol @@ -1,13 +1,13 @@ pragma solidity 0.8.9; -import {WithdrawalRequests} from "contracts/0.8.9/lib/WithdrawalRequests.sol"; +import {TriggerableWithdrawals} from "contracts/0.8.9/lib/TriggerableWithdrawals.sol"; -contract WithdrawalCredentials_Harness { +contract TriggerableWithdrawals_Harness { function addFullWithdrawalRequests( bytes[] calldata pubkeys, uint256 totalWithdrawalFee ) external { - WithdrawalRequests.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee); + TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee); } function addPartialWithdrawalRequests( @@ -15,7 +15,7 @@ contract WithdrawalCredentials_Harness { uint64[] calldata amounts, uint256 totalWithdrawalFee ) external { - WithdrawalRequests.addPartialWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); } function addWithdrawalRequests( @@ -23,15 +23,15 @@ contract WithdrawalCredentials_Harness { uint64[] calldata amounts, uint256 totalWithdrawalFee ) external { - WithdrawalRequests.addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + TriggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); } function getWithdrawalRequestFee() external view returns (uint256) { - return WithdrawalRequests.getWithdrawalRequestFee(); + return TriggerableWithdrawals.getWithdrawalRequestFee(); } function getWithdrawalsContractAddress() public pure returns (address) { - return WithdrawalRequests.WITHDRAWAL_REQUEST; + return TriggerableWithdrawals.WITHDRAWAL_REQUEST; } function deposit() external payable {} diff --git a/test/0.8.9/lib/withdrawalCredentials/findEvents.ts b/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts similarity index 100% rename from test/0.8.9/lib/withdrawalCredentials/findEvents.ts rename to test/0.8.9/lib/triggerableWithdrawals/findEvents.ts diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts similarity index 61% rename from test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts rename to test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index 2ee973b678..ce83a29215 100644 --- a/test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -5,7 +5,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; +import { TriggerableWithdrawals_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; import { Snapshot } from "test/suite"; @@ -14,18 +14,18 @@ import { deployWithdrawalsPredeployedMock, generateWithdrawalRequestPayload, withdrawalsPredeployedHardcodedAddress, -} from "./withdrawalRequests.behavior"; +} from "./utils"; -describe("WithdrawalCredentials.sol", () => { +describe("TriggerableWithdrawals.sol", () => { let actor: HardhatEthersSigner; let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; - let withdrawalCredentials: WithdrawalCredentials_Harness; + let triggerableWithdrawals: TriggerableWithdrawals_Harness; let originalState: string; async function getWithdrawalCredentialsContractBalance(): Promise { - const contractAddress = await withdrawalCredentials.getAddress(); + const contractAddress = await triggerableWithdrawals.getAddress(); return await ethers.provider.getBalance(contractAddress); } @@ -38,11 +38,11 @@ describe("WithdrawalCredentials.sol", () => { [actor] = await ethers.getSigners(); withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(1n); - withdrawalCredentials = await ethers.deployContract("WithdrawalCredentials_Harness"); + triggerableWithdrawals = await ethers.deployContract("TriggerableWithdrawals_Harness"); expect(await withdrawalsPredeployed.getAddress()).to.equal(withdrawalsPredeployedHardcodedAddress); - await withdrawalCredentials.connect(actor).deposit({ value: ethers.parseEther("1") }); + await triggerableWithdrawals.connect(actor).deposit({ value: ethers.parseEther("1") }); }); beforeEach(async () => (originalState = await Snapshot.take())); @@ -50,14 +50,14 @@ describe("WithdrawalCredentials.sol", () => { afterEach(async () => await Snapshot.restore(originalState)); async function getFee(requestsCount: number): Promise { - const fee = await withdrawalCredentials.getWithdrawalRequestFee(); + const fee = await triggerableWithdrawals.getWithdrawalRequestFee(); return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); } context("eip 7002 contract", () => { it("Should return the address of the EIP 7002 contract", async function () { - expect(await withdrawalCredentials.getWithdrawalsContractAddress()).to.equal( + expect(await triggerableWithdrawals.getWithdrawalsContractAddress()).to.equal( withdrawalsPredeployedHardcodedAddress, ); }); @@ -67,15 +67,15 @@ describe("WithdrawalCredentials.sol", () => { it("Should get fee from the EIP 7002 contract", async function () { await withdrawalsPredeployed.setFee(333n); expect( - (await withdrawalCredentials.getWithdrawalRequestFee()) == 333n, + (await triggerableWithdrawals.getWithdrawalRequestFee()) == 333n, "withdrawal request should use fee from the EIP 7002 contract", ); }); it("Should revert if fee read fails", async function () { await withdrawalsPredeployed.setFailOnGetFee(true); - await expect(withdrawalCredentials.getWithdrawalRequestFee()).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( + triggerableWithdrawals, "WithdrawalRequestFeeReadFailed", ); }); @@ -83,18 +83,18 @@ describe("WithdrawalCredentials.sol", () => { context("add withdrawal requests", () => { it("Should revert if empty arrays are provided", async function () { - await expect(withdrawalCredentials.addFullWithdrawalRequests([], 1n)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addFullWithdrawalRequests([], 1n)).to.be.revertedWithCustomError( + triggerableWithdrawals, "NoWithdrawalRequests", ); - await expect(withdrawalCredentials.addPartialWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addPartialWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( + triggerableWithdrawals, "NoWithdrawalRequests", ); - await expect(withdrawalCredentials.addWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( + triggerableWithdrawals, "NoWithdrawalRequests", ); }); @@ -105,12 +105,12 @@ describe("WithdrawalCredentials.sol", () => { const fee = await getFee(pubkeys.length); - await expect(withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "MismatchedArrayLengths") + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") .withArgs(pubkeys.length, amounts.length); - await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, fee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "MismatchedArrayLengths") + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") .withArgs(pubkeys.length, amounts.length); }); @@ -121,33 +121,33 @@ describe("WithdrawalCredentials.sol", () => { await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent - await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, 0n)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, 0n)).to.be.revertedWithCustomError( + triggerableWithdrawals, "FeeNotEnough", ); await expect( - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, 0n), - ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, 0n), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); - await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, 0n)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, 0n)).to.be.revertedWithCustomError( + triggerableWithdrawals, "FeeNotEnough", ); // 2. Should revert if fee is less than required const insufficientFee = 2n; await expect( - withdrawalCredentials.addFullWithdrawalRequests(pubkeys, insufficientFee), - ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, insufficientFee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); await expect( - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, insufficientFee), - ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, insufficientFee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); await expect( - withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, insufficientFee), - ).to.be.revertedWithCustomError(withdrawalCredentials, "FeeNotEnough"); + triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, insufficientFee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); }); it("Should revert if any pubkey is not 48 bytes", async function () { @@ -157,16 +157,16 @@ describe("WithdrawalCredentials.sol", () => { const fee = await getFee(pubkeys.length); - await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "InvalidPubkeyLength") + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") .withArgs(pubkeys[0]); - await expect(withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "InvalidPubkeyLength") + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") .withArgs(pubkeys[0]); - await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, fee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "InvalidPubkeyLength") + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") .withArgs(pubkeys[0]); }); @@ -179,17 +179,17 @@ describe("WithdrawalCredentials.sol", () => { // Set mock to fail on add await withdrawalsPredeployed.setFailOnAddRequest(true); - await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)).to.be.revertedWithCustomError( + triggerableWithdrawals, "WithdrawalRequestAdditionFailed", ); await expect( - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee), - ).to.be.revertedWithCustomError(withdrawalCredentials, "WithdrawalRequestAdditionFailed"); + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, amounts, fee)).to.be.revertedWithCustomError( - withdrawalCredentials, + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)).to.be.revertedWithCustomError( + triggerableWithdrawals, "WithdrawalRequestAdditionFailed", ); }); @@ -200,8 +200,8 @@ describe("WithdrawalCredentials.sol", () => { const fee = await getFee(pubkeys.length); await expect( - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, amounts, fee), - ).to.be.revertedWithCustomError(withdrawalCredentials, "PartialWithdrawalRequired"); + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "PartialWithdrawalRequired"); }); it("Should revert if contract balance insufficient'", async function () { @@ -211,20 +211,20 @@ describe("WithdrawalCredentials.sol", () => { const balance = 19n; await withdrawalsPredeployed.setFee(fee); - await setBalance(await withdrawalCredentials.getAddress(), balance); + await setBalance(await triggerableWithdrawals.getAddress(), balance); - await expect(withdrawalCredentials.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "InsufficientBalance") + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, totalWithdrawalFee); await expect( - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), ) - .to.be.revertedWithCustomError(withdrawalCredentials, "InsufficientBalance") + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, totalWithdrawalFee); - await expect(withdrawalCredentials.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, totalWithdrawalFee)) - .to.be.revertedWithCustomError(withdrawalCredentials, "InsufficientBalance") + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, totalWithdrawalFee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, totalWithdrawalFee); }); @@ -236,9 +236,9 @@ describe("WithdrawalCredentials.sol", () => { await withdrawalsPredeployed.setFee(3n); const fee = 9n; - await withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee); - await withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); }); it("Should accept exceed fee without revert", async function () { @@ -249,9 +249,9 @@ describe("WithdrawalCredentials.sol", () => { await withdrawalsPredeployed.setFee(3n); const fee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei - await withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee); - await withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); }); it("Should deduct precise fee value from contract balance", async function () { @@ -268,11 +268,11 @@ describe("WithdrawalCredentials.sol", () => { expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - fee); }; - await testFeeDeduction(() => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee)); + await testFeeDeduction(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); await testFeeDeduction(() => - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), ); - await testFeeDeduction(() => withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); + await testFeeDeduction(() => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); }); it("Should send all fee to eip 7002 withdrawal contract", async function () { @@ -289,12 +289,12 @@ describe("WithdrawalCredentials.sol", () => { expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + totalWithdrawalFee); }; - await testFeeTransfer(() => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)); + await testFeeTransfer(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)); await testFeeTransfer(() => - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), ); await testFeeTransfer(() => - withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), ); }); @@ -322,17 +322,17 @@ describe("WithdrawalCredentials.sol", () => { }; await testEventsEmit( - () => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, fee), + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee), pubkeys, fullWithdrawalAmounts, ); await testEventsEmit( - () => withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), pubkeys, partialWithdrawalAmounts, ); await testEventsEmit( - () => withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), + () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), pubkeys, mixedWithdrawalAmounts, ); @@ -379,7 +379,7 @@ describe("WithdrawalCredentials.sol", () => { const totalWithdrawalFee = (await getFee(pubkeys.length)) + extraFee; await addWithdrawalRequests( - () => withdrawalCredentials.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), pubkeys, fullWithdrawalAmounts, totalWithdrawalFee, @@ -387,14 +387,14 @@ describe("WithdrawalCredentials.sol", () => { await addWithdrawalRequests( () => - withdrawalCredentials.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), pubkeys, partialWithdrawalAmounts, totalWithdrawalFee, ); await addWithdrawalRequests( - () => withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee, @@ -407,9 +407,9 @@ describe("WithdrawalCredentials.sol", () => { generateWithdrawalRequestPayload(3); const fee = await getFee(pubkeys.length); - await withdrawalCredentials.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); - await withdrawalCredentials.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await withdrawalCredentials.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); }); }); }); diff --git a/test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts b/test/0.8.9/lib/triggerableWithdrawals/utils.ts similarity index 100% rename from test/0.8.9/lib/withdrawalCredentials/withdrawalRequests.behavior.ts rename to test/0.8.9/lib/triggerableWithdrawals/utils.ts diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 85396970d9..6ac41d8ace 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -20,7 +20,7 @@ import { Snapshot } from "test/suite"; import { deployWithdrawalsPredeployedMock, withdrawalsPredeployedHardcodedAddress, -} from "./lib/withdrawalCredentials/withdrawalRequests.behavior"; +} from "./lib/triggerableWithdrawals/utils"; const PETRIFIED_VERSION = MAX_UINT256; @@ -197,7 +197,7 @@ describe("WithdrawalVault.sol", () => { }); }); - context("eip 7002 withdrawal requests", () => { + context("eip 7002 triggerable withdrawals", () => { it("Reverts if the caller is not Validator Exit Bus", async () => { await expect(vault.connect(user).addFullWithdrawalRequests(["0x1234"])).to.be.revertedWithCustomError( vault, From 5183e89f235746c31300b5cd5542294cbd009de1 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 26 Dec 2024 14:45:41 +0100 Subject: [PATCH 010/405] feat: add unit tests for triggerable withdrawals lib --- .../WithdrawalsPredeployed_Mock.sol | 7 + .../lib/triggerableWithdrawals/findEvents.ts | 12 +- .../triggerableWithdrawals.test.ts | 223 ++++++++++++++++-- 3 files changed, 216 insertions(+), 26 deletions(-) diff --git a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol index 6c50f7d6ae..f4b580b142 100644 --- a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol +++ b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol @@ -9,6 +9,8 @@ contract WithdrawalsPredeployed_Mock { bool public failOnAddRequest; bool public failOnGetFee; + event eip7002WithdrawalRequestAdded(bytes request, uint256 fee); + function setFailOnAddRequest(bool _failOnAddRequest) external { failOnAddRequest = _failOnAddRequest; } @@ -33,5 +35,10 @@ contract WithdrawalsPredeployed_Mock { require(!failOnAddRequest, "fail on add request"); require(input.length == 56, "Invalid callData length"); + + emit eip7002WithdrawalRequestAdded( + input, + msg.value + ); } } diff --git a/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts b/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts index 9ee2581399..82047e8c16 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts @@ -5,9 +5,19 @@ import { findEventsWithInterfaces } from "lib"; const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); - type WithdrawalRequestEvents = "WithdrawalRequestAdded"; export function findEvents(receipt: ContractTransactionReceipt, event: WithdrawalRequestEvents) { return findEventsWithInterfaces(receipt!, event, [withdrawalRequestEventInterface]); } + +const eip7002TriggerableWithdrawalMockEventABI = ["event eip7002WithdrawalRequestAdded(bytes request, uint256 fee)"]; +const eip7002TriggerableWithdrawalMockInterface = new ethers.Interface(eip7002TriggerableWithdrawalMockEventABI); +type Eip7002WithdrawalEvents = "eip7002WithdrawalRequestAdded"; + +export function findEip7002TriggerableWithdrawalMockEvents( + receipt: ContractTransactionReceipt, + event: Eip7002WithdrawalEvents, +) { + return findEventsWithInterfaces(receipt!, event, [eip7002TriggerableWithdrawalMockInterface]); +} diff --git a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index ce83a29215..3ae0aa3ce9 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -9,7 +9,7 @@ import { TriggerableWithdrawals_Harness, WithdrawalsPredeployed_Mock } from "typ import { Snapshot } from "test/suite"; -import { findEvents } from "./findEvents"; +import { findEip7002TriggerableWithdrawalMockEvents, findEvents } from "./findEvents"; import { deployWithdrawalsPredeployedMock, generateWithdrawalRequestPayload, @@ -34,6 +34,8 @@ describe("TriggerableWithdrawals.sol", () => { return await ethers.provider.getBalance(contractAddress); } + const MAX_UINT64 = (1n << 64n) - 1n; + before(async () => { [actor] = await ethers.getSigners(); @@ -109,9 +111,25 @@ describe("TriggerableWithdrawals.sol", () => { .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") .withArgs(pubkeys.length, amounts.length); + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, [], fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") + .withArgs(pubkeys.length, 0); + + await expect(triggerableWithdrawals.addPartialWithdrawalRequests([], amounts, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") + .withArgs(0, amounts.length); + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") .withArgs(pubkeys.length, amounts.length); + + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, [], fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") + .withArgs(pubkeys.length, 0); + + await expect(triggerableWithdrawals.addWithdrawalRequests([], amounts, fee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") + .withArgs(0, amounts.length); }); it("Should revert if not enough fee is sent", async function () { @@ -194,7 +212,7 @@ describe("TriggerableWithdrawals.sol", () => { ); }); - it("Should revert if full withdrawal requested in 'addPartialWithdrawalRequests'", async function () { + it("Should revert when a full withdrawal amount is included in 'addPartialWithdrawalRequests'", async function () { const { pubkeys } = generateWithdrawalRequestPayload(2); const amounts = [1n, 0n]; // Partial and Full withdrawal const fee = await getFee(pubkeys.length); @@ -204,8 +222,8 @@ describe("TriggerableWithdrawals.sol", () => { ).to.be.revertedWithCustomError(triggerableWithdrawals, "PartialWithdrawalRequired"); }); - it("Should revert if contract balance insufficient'", async function () { - const { pubkeys, partialWithdrawalAmounts, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + it("Should revert when balance is less than total withdrawal fee", async function () { + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); const fee = 10n; const totalWithdrawalFee = 20n; const balance = 19n; @@ -223,25 +241,59 @@ describe("TriggerableWithdrawals.sol", () => { .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, totalWithdrawalFee); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, totalWithdrawalFee)) + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, totalWithdrawalFee); }); - it("Should accept exactly required fee without revert", async function () { + it("Should revert when fee read fails", async function () { + await withdrawalsPredeployed.setFailOnGetFee(true); + + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const fee = 10n; + + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)).to.be.revertedWithCustomError( + triggerableWithdrawals, + "WithdrawalRequestFeeReadFailed", + ); + + await expect( + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); + + await expect( + triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); + }); + + it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); await withdrawalsPredeployed.setFee(3n); - const fee = 9n; + const totalWithdrawalFee = 9n; - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee); + + // Check extremely high fee + await withdrawalsPredeployed.setFee(ethers.parseEther("10")); + const largeTotalWithdrawalFee = ethers.parseEther("30"); + + await triggerableWithdrawals.connect(actor).deposit({ value: largeTotalWithdrawalFee * BigInt(requestCount) }); + + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, largeTotalWithdrawalFee); + await triggerableWithdrawals.addPartialWithdrawalRequests( + pubkeys, + partialWithdrawalAmounts, + largeTotalWithdrawalFee, + ); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, largeTotalWithdrawalFee); }); - it("Should accept exceed fee without revert", async function () { + it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { const requestCount = 3; const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); @@ -252,9 +304,21 @@ describe("TriggerableWithdrawals.sol", () => { await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + + // Check when the provided fee extremely exceeds the required amount + const largeTotalWithdrawalFee = ethers.parseEther("10"); + await triggerableWithdrawals.connect(actor).deposit({ value: largeTotalWithdrawalFee * BigInt(requestCount) }); + + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, largeTotalWithdrawalFee); + await triggerableWithdrawals.addPartialWithdrawalRequests( + pubkeys, + partialWithdrawalAmounts, + largeTotalWithdrawalFee, + ); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, largeTotalWithdrawalFee); }); - it("Should deduct precise fee value from contract balance", async function () { + it("Should correctly deduct the exact fee amount from the contract balance", async function () { const requestCount = 3; const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); @@ -275,7 +339,7 @@ describe("TriggerableWithdrawals.sol", () => { await testFeeDeduction(() => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); }); - it("Should send all fee to eip 7002 withdrawal contract", async function () { + it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); @@ -298,7 +362,25 @@ describe("TriggerableWithdrawals.sol", () => { ); }); - it("should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { + it("Should accept full, partial, and mixed withdrawal requests via 'addWithdrawalRequests' function", async function () { + const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(3); + const fee = await getFee(pubkeys.length); + + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + }); + + it("Should handle maximum uint64 withdrawal amount in partial withdrawal requests", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + const amounts = [MAX_UINT64]; + + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, 10n); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, 10n); + }); + + it("Should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { const requestCount = 3; const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); @@ -338,6 +420,95 @@ describe("TriggerableWithdrawals.sol", () => { ); }); + it("Should verify correct fee distribution among requests", async function () { + await withdrawalsPredeployed.setFee(2n); + + const requestCount = 5; + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + + const testFeeDistribution = async (totalWithdrawalFee: bigint, expectedFeePerRequest: bigint[]) => { + const checkEip7002MockEvents = async (addRequests: () => Promise) => { + const tx = await addRequests(); + + const receipt = await tx.wait(); + const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[1]).to.equal(expectedFeePerRequest[i]); + } + }; + + await checkEip7002MockEvents(() => + triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), + ); + + await checkEip7002MockEvents(() => + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + ); + + await checkEip7002MockEvents(() => + triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + ); + }; + + await testFeeDistribution(10n, [2n, 2n, 2n, 2n, 2n]); + await testFeeDistribution(11n, [2n, 2n, 2n, 2n, 3n]); + await testFeeDistribution(14n, [2n, 2n, 2n, 2n, 6n]); + await testFeeDistribution(15n, [3n, 3n, 3n, 3n, 3n]); + }); + + it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { + const requestCount = 16; + const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + const totalWithdrawalFee = 333n; + + const normalize = (hex: string) => (hex.startsWith("0x") ? hex.slice(2).toLowerCase() : hex.toLowerCase()); + + const testEncoding = async ( + addRequests: () => Promise, + expectedPubKeys: string[], + expectedAmounts: bigint[], + ) => { + const tx = await addRequests(); + + const receipt = await tx.wait(); + + const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + const encodedRequest = events[i].args[0]; + // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters + expect(encodedRequest.length).to.equal(114); + + expect(normalize(encodedRequest.substring(0, 98))).to.equal(normalize(expectedPubKeys[i])); + expect(normalize(encodedRequest.substring(98, 114))).to.equal( + expectedAmounts[i].toString(16).padStart(16, "0"), + ); + } + }; + + await testEncoding( + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), + pubkeys, + fullWithdrawalAmounts, + ); + await testEncoding( + () => + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + pubkeys, + partialWithdrawalAmounts, + ); + await testEncoding( + () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + pubkeys, + mixedWithdrawalAmounts, + ); + }); + async function addWithdrawalRequests( addRequests: () => Promise, expectedPubkeys: string[], @@ -359,16 +530,28 @@ describe("TriggerableWithdrawals.sol", () => { expect(events[i].args[0]).to.equal(expectedPubkeys[i]); expect(events[i].args[1]).to.equal(expectedAmounts[i]); } + + const eip7002TriggerableWithdrawalMockEvents = findEip7002TriggerableWithdrawalMockEvents( + receipt!, + "eip7002WithdrawalRequestAdded", + ); + expect(eip7002TriggerableWithdrawalMockEvents.length).to.equal(expectedPubkeys.length); + for (let i = 0; i < expectedPubkeys.length; i++) { + expect(eip7002TriggerableWithdrawalMockEvents[i].args[0]).to.equal( + expectedPubkeys[i].concat(expectedAmounts[i].toString(16).padStart(16, "0")), + ); + } } const testCasesForWithdrawalRequests = [ { requestCount: 1, extraFee: 0n }, { requestCount: 1, extraFee: 100n }, + { requestCount: 1, extraFee: 100_000_000_000n }, { requestCount: 3, extraFee: 0n }, { requestCount: 3, extraFee: 1n }, { requestCount: 7, extraFee: 3n }, { requestCount: 10, extraFee: 0n }, - { requestCount: 10, extraFee: 1_000_000n }, + { requestCount: 10, extraFee: 100_000_000_000n }, { requestCount: 100, extraFee: 0n }, ]; @@ -401,15 +584,5 @@ describe("TriggerableWithdrawals.sol", () => { ); }); }); - - it("Should accept full and partial withdrawals requested", async function () { - const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(3); - const fee = await getFee(pubkeys.length); - - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); - }); }); }); From 2fc90ece48aaba7ec6871c6483b4f15562de7fd2 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 26 Dec 2024 16:19:06 +0100 Subject: [PATCH 011/405] feat: add unit tests for triggerable withdrawals in the withdrawal vault contract --- .../triggerableWithdrawals.test.ts | 4 +- test/0.8.9/withdrawalVault.test.ts | 268 +++++++++++++++++- 2 files changed, 267 insertions(+), 5 deletions(-) diff --git a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index 3ae0aa3ce9..83c57ca260 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -65,7 +65,7 @@ describe("TriggerableWithdrawals.sol", () => { }); }); - context("get withdrawal request fee", () => { + context("get triggerable withdrawal request fee", () => { it("Should get fee from the EIP 7002 contract", async function () { await withdrawalsPredeployed.setFee(333n); expect( @@ -83,7 +83,7 @@ describe("TriggerableWithdrawals.sol", () => { }); }); - context("add withdrawal requests", () => { + context("add triggerable withdrawal requests", () => { it("Should revert if empty arrays are provided", async function () { await expect(triggerableWithdrawals.addFullWithdrawalRequests([], 1n)).to.be.revertedWithCustomError( triggerableWithdrawals, diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 6ac41d8ace..9402b7f66d 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -17,8 +17,10 @@ import { MAX_UINT256, proxify } from "lib"; import { Snapshot } from "test/suite"; +import { findEip7002TriggerableWithdrawalMockEvents, findEvents } from "./lib/triggerableWithdrawals/findEvents"; import { deployWithdrawalsPredeployedMock, + generateWithdrawalRequestPayload, withdrawalsPredeployedHardcodedAddress, } from "./lib/triggerableWithdrawals/utils"; @@ -197,14 +199,274 @@ describe("WithdrawalVault.sol", () => { }); }); - context("eip 7002 triggerable withdrawals", () => { - it("Reverts if the caller is not Validator Exit Bus", async () => { + context("get triggerable withdrawal request fee", () => { + it("Should get fee from the EIP 7002 contract", async function () { + await withdrawalsPredeployed.setFee(333n); + expect( + (await vault.getWithdrawalRequestFee()) == 333n, + "withdrawal request should use fee from the EIP 7002 contract", + ); + }); + + it("Should revert if fee read fails", async function () { + await withdrawalsPredeployed.setFailOnGetFee(true); + await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError( + vault, + "WithdrawalRequestFeeReadFailed", + ); + }); + }); + + async function getFee(requestsCount: number): Promise { + const fee = await vault.getWithdrawalRequestFee(); + + return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); + } + + async function getWithdrawalCredentialsContractBalance(): Promise { + const contractAddress = await vault.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + + async function getWithdrawalsPredeployedContractBalance(): Promise { + const contractAddress = await withdrawalsPredeployed.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + + context("add triggerable withdrawal requests", () => { + it("Should revert if the caller is not Validator Exit Bus", async () => { await expect(vault.connect(user).addFullWithdrawalRequests(["0x1234"])).to.be.revertedWithCustomError( vault, "NotValidatorExitBus", ); }); - // ToDo: add tests... + it("Should revert if empty arrays are provided", async function () { + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests([], { value: 1n }), + ).to.be.revertedWithCustomError(vault, "NoWithdrawalRequests"); + }); + + it("Should revert if not enough fee is sent", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + + await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei + + // 1. Should revert if no fee is sent + await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys)).to.be.revertedWithCustomError( + vault, + "FeeNotEnough", + ); + + // 2. Should revert if fee is less than required + const insufficientFee = 2n; + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: insufficientFee }), + ).to.be.revertedWithCustomError(vault, "FeeNotEnough"); + }); + + it("Should revert if any pubkey is not 48 bytes", async function () { + // Invalid pubkey (only 2 bytes) + const pubkeys = ["0x1234"]; + + const fee = await getFee(pubkeys.length); + + await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee })) + .to.be.revertedWithCustomError(vault, "InvalidPubkeyLength") + .withArgs(pubkeys[0]); + }); + + it("Should revert if addition fails at the withdrawal request contract", async function () { + const { pubkeys } = generateWithdrawalRequestPayload(1); + const fee = await getFee(pubkeys.length); + + // Set mock to fail on add + await withdrawalsPredeployed.setFailOnAddRequest(true); + + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }), + ).to.be.revertedWithCustomError(vault, "WithdrawalRequestAdditionFailed"); + }); + + it("Should revert when fee read fails", async function () { + await withdrawalsPredeployed.setFailOnGetFee(true); + + const { pubkeys } = generateWithdrawalRequestPayload(2); + const fee = 10n; + + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }), + ).to.be.revertedWithCustomError(vault, "WithdrawalRequestFeeReadFailed"); + }); + + it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { + const requestCount = 3; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const totalWithdrawalFee = 9n; + + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + + // Check extremely high fee + await withdrawalsPredeployed.setFee(ethers.parseEther("10")); + const largeTotalWithdrawalFee = ethers.parseEther("30"); + + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: largeTotalWithdrawalFee }); + }); + + it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { + const requestCount = 3; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const fee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei + + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }); + + // Check when the provided fee extremely exceeds the required amount + const largeTotalWithdrawalFee = ethers.parseEther("10"); + + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: largeTotalWithdrawalFee }); + }); + + it("Should correctly deduct the exact fee amount from the contract balance", async function () { + const requestCount = 3; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const fee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei + + const initialBalance = await getWithdrawalCredentialsContractBalance(); + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + }); + + it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { + const requestCount = 3; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + await withdrawalsPredeployed.setFee(3n); + const totalWithdrawalFee = 9n + 1n; + + const initialBalance = await getWithdrawalsPredeployedContractBalance(); + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + totalWithdrawalFee); + }); + + it("Should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { + const requestCount = 3; + const { pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const fee = 10n; + + const tx = await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }); + + const receipt = await tx.wait(); + const events = findEvents(receipt!, "WithdrawalRequestAdded"); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[0]).to.equal(pubkeys[i]); + expect(events[i].args[1]).to.equal(fullWithdrawalAmounts[i]); + } + }); + + it("Should verify correct fee distribution among requests", async function () { + await withdrawalsPredeployed.setFee(2n); + + const requestCount = 5; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + + const testFeeDistribution = async (totalWithdrawalFee: bigint, expectedFeePerRequest: bigint[]) => { + const tx = await vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + + const receipt = await tx.wait(); + const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + expect(events[i].args[1]).to.equal(expectedFeePerRequest[i]); + } + }; + + await testFeeDistribution(10n, [2n, 2n, 2n, 2n, 2n]); + await testFeeDistribution(11n, [2n, 2n, 2n, 2n, 3n]); + await testFeeDistribution(14n, [2n, 2n, 2n, 2n, 6n]); + await testFeeDistribution(15n, [3n, 3n, 3n, 3n, 3n]); + }); + + it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { + const requestCount = 16; + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const totalWithdrawalFee = 333n; + + const normalize = (hex: string) => (hex.startsWith("0x") ? hex.slice(2).toLowerCase() : hex.toLowerCase()); + + const tx = await vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + + const receipt = await tx.wait(); + + const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + const encodedRequest = events[i].args[0]; + // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters + expect(encodedRequest.length).to.equal(114); + + expect(normalize(encodedRequest.substring(0, 98))).to.equal(normalize(pubkeys[i])); + expect(normalize(encodedRequest.substring(98, 114))).to.equal("0".repeat(16)); + } + }); + + const testCasesForWithdrawalRequests = [ + { requestCount: 1, extraFee: 0n }, + { requestCount: 1, extraFee: 100n }, + { requestCount: 1, extraFee: 100_000_000_000n }, + { requestCount: 3, extraFee: 0n }, + { requestCount: 3, extraFee: 1n }, + { requestCount: 7, extraFee: 3n }, + { requestCount: 10, extraFee: 0n }, + { requestCount: 10, extraFee: 100_000_000_000n }, + { requestCount: 100, extraFee: 0n }, + ]; + + testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { + it(`Should successfully add ${requestCount} requests with extra fee ${extraFee} and emit events`, async () => { + const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const totalWithdrawalFee = (await getFee(pubkeys.length)) + extraFee; + + const initialBalance = await getWithdrawalCredentialsContractBalance(); + + const tx = await vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + + const receipt = await tx.wait(); + + const events = findEvents(receipt!, "WithdrawalRequestAdded"); + expect(events.length).to.equal(pubkeys.length); + + for (let i = 0; i < pubkeys.length; i++) { + expect(events[i].args[0]).to.equal(pubkeys[i]); + expect(events[i].args[1]).to.equal(0); + } + + const eip7002TriggerableWithdrawalMockEvents = findEip7002TriggerableWithdrawalMockEvents( + receipt!, + "eip7002WithdrawalRequestAdded", + ); + expect(eip7002TriggerableWithdrawalMockEvents.length).to.equal(pubkeys.length); + for (let i = 0; i < pubkeys.length; i++) { + expect(eip7002TriggerableWithdrawalMockEvents[i].args[0]).to.equal(pubkeys[i].concat("0".repeat(16))); + } + }); + }); }); }); From 5888facad18ad425aba9f36f827790cf35d77e1a Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 10 Jan 2025 12:24:25 +0100 Subject: [PATCH 012/405] feat: use lido locator instead of direct VEB address --- contracts/0.8.9/WithdrawalVault.sol | 11 ++++++----- test/0.8.9/withdrawalVault.test.ts | 12 ++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 9789bf54a4..350d6bd1af 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; import {TriggerableWithdrawals} from "./lib/TriggerableWithdrawals.sol"; +import { ILidoLocator } from "../common/interfaces/ILidoLocator.sol"; interface ILido { /** @@ -28,7 +29,7 @@ contract WithdrawalVault is Versioned { ILido public immutable LIDO; address public immutable TREASURY; - address public immutable VALIDATORS_EXIT_BUS; + ILidoLocator public immutable LOCATOR; // Events /** @@ -54,14 +55,14 @@ contract WithdrawalVault is Versioned { * @param _lido the Lido token (stETH) address * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) */ - constructor(address _lido, address _treasury, address _validatorsExitBus) { + constructor(address _lido, address _treasury, address _locator) { _requireNonZero(_lido); _requireNonZero(_treasury); - _requireNonZero(_validatorsExitBus); + _requireNonZero(_locator); LIDO = ILido(_lido); TREASURY = _treasury; - VALIDATORS_EXIT_BUS = _validatorsExitBus; + LOCATOR = ILidoLocator(_locator); } /** @@ -137,7 +138,7 @@ contract WithdrawalVault is Versioned { function addFullWithdrawalRequests( bytes[] calldata pubkeys ) external payable { - if(msg.sender != address(VALIDATORS_EXIT_BUS)) { + if(msg.sender != LOCATOR.validatorsExitBusOracle()) { revert NotValidatorExitBus(); } diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 9402b7f66d..3069e04930 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -9,12 +9,14 @@ import { ERC20__Harness, ERC721__Harness, Lido__MockForWithdrawalVault, + LidoLocator, WithdrawalsPredeployed_Mock, WithdrawalVault, } from "typechain-types"; import { MAX_UINT256, proxify } from "lib"; +import { deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; import { findEip7002TriggerableWithdrawalMockEvents, findEvents } from "./lib/triggerableWithdrawals/findEvents"; @@ -37,6 +39,9 @@ describe("WithdrawalVault.sol", () => { let lido: Lido__MockForWithdrawalVault; let lidoAddress: string; + let locator: LidoLocator; + let locatorAddress: string; + let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; let impl: WithdrawalVault; @@ -53,7 +58,10 @@ describe("WithdrawalVault.sol", () => { lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); lidoAddress = await lido.getAddress(); - impl = await ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, validatorsExitBus.address]); + locator = await deployLidoLocator({ lido, validatorsExitBusOracle: validatorsExitBus }); + locatorAddress = await locator.getAddress(); + + impl = await ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, locatorAddress]); [vault] = await proxify({ impl, admin: owner }); @@ -86,7 +94,7 @@ describe("WithdrawalVault.sol", () => { it("Sets initial properties", async () => { expect(await vault.LIDO()).to.equal(lidoAddress, "Lido address"); expect(await vault.TREASURY()).to.equal(treasury.address, "Treasury address"); - expect(await vault.VALIDATORS_EXIT_BUS()).to.equal(validatorsExitBus.address, "Validator exit bus address"); + expect(await vault.LOCATOR()).to.equal(locatorAddress, "Validator exit bus address"); }); it("Petrifies the implementation", async () => { From c251b90a7aeef171b419bac4397e58b4f13ea94c Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 14 Jan 2025 15:13:51 +0100 Subject: [PATCH 013/405] feat: add access control to WithdrawalVault contract Add role ADD_FULL_WITHDRAWAL_REQUEST_ROLE for full withdrawal requests. --- contracts/0.8.9/WithdrawalVault.sol | 45 +++--- .../0120-initialize-non-aragon-contracts.ts | 5 + .../contracts/WithdrawalVault__Harness.sol | 15 ++ test/0.8.9/withdrawalVault.test.ts | 149 +++++++++++++----- 4 files changed, 154 insertions(+), 60 deletions(-) create mode 100644 test/0.8.9/contracts/WithdrawalVault__Harness.sol diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 350d6bd1af..0e8b7dc06b 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -9,6 +9,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; +import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {TriggerableWithdrawals} from "./lib/TriggerableWithdrawals.sol"; import { ILidoLocator } from "../common/interfaces/ILidoLocator.sol"; @@ -24,12 +25,13 @@ interface ILido { /** * @title A vault for temporary storage of withdrawals */ -contract WithdrawalVault is Versioned { +contract WithdrawalVault is AccessControlEnumerable, Versioned { using SafeERC20 for IERC20; ILido public immutable LIDO; address public immutable TREASURY; - ILidoLocator public immutable LOCATOR; + + bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); // Events /** @@ -47,7 +49,6 @@ contract WithdrawalVault is Versioned { // Errors error ZeroAddress(); error NotLido(); - error NotValidatorExitBus(); error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); @@ -55,27 +56,32 @@ contract WithdrawalVault is Versioned { * @param _lido the Lido token (stETH) address * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) */ - constructor(address _lido, address _treasury, address _locator) { + constructor(address _lido, address _treasury) { _requireNonZero(_lido); _requireNonZero(_treasury); - _requireNonZero(_locator); LIDO = ILido(_lido); TREASURY = _treasury; - LOCATOR = ILidoLocator(_locator); } - /** - * @notice Initialize the contract explicitly. - * Sets the contract version to '1'. - */ - function initialize() external { - _initializeContractVersionTo(1); - _updateContractVersion(2); + /// @notice Initializes the contract. Can be called only once. + /// @param _admin Lido DAO Aragon agent contract address. + /// @dev Proxy initialization method. + function initialize(address _admin) external { + // Initializations for v0 --> v2 + _checkContractVersion(0); + + _initialize_v2(_admin); + _initializeContractVersionTo(2); } - function finalizeUpgrade_v2() external { + /// @notice Finalizes upgrade to v2 (from v1). Can be called only once. + /// @param _admin Lido DAO Aragon agent contract address. + function finalizeUpgrade_v2(address _admin) external { + // Finalization for v1 --> v2 _checkContractVersion(1); + + _initialize_v2(_admin); _updateContractVersion(2); } @@ -137,11 +143,7 @@ contract WithdrawalVault is Versioned { */ function addFullWithdrawalRequests( bytes[] calldata pubkeys - ) external payable { - if(msg.sender != LOCATOR.validatorsExitBusOracle()) { - revert NotValidatorExitBus(); - } - + ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) { TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, msg.value); } @@ -152,4 +154,9 @@ contract WithdrawalVault is Versioned { function _requireNonZero(address _address) internal pure { if (_address == address(0)) revert ZeroAddress(); } + + function _initialize_v2(address _admin) internal { + _requireNonZero(_admin); + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + } } diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index dab37394b5..bd8eff9eba 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -35,6 +35,7 @@ export async function main() { const exitBusOracleAdmin = testnetAdmin; const stakingRouterAdmin = testnetAdmin; const withdrawalQueueAdmin = testnetAdmin; + const withdrawalVaultAdmin = testnetAdmin; // Initialize NodeOperatorsRegistry @@ -108,6 +109,10 @@ export async function main() { { from: deployer }, ); + // Initialize WithdrawalVault + const withdrawalVault = await loadContract("WithdrawalVault", withdrawalVaultAddress); + await makeTx(withdrawalVault, "initialize", [withdrawalVaultAdmin], { from: deployer }); + // Initialize WithdrawalQueue const withdrawalQueue = await loadContract("WithdrawalQueueERC721", withdrawalQueueAddress); await makeTx(withdrawalQueue, "initialize", [withdrawalQueueAdmin], { from: deployer }); diff --git a/test/0.8.9/contracts/WithdrawalVault__Harness.sol b/test/0.8.9/contracts/WithdrawalVault__Harness.sol new file mode 100644 index 0000000000..229e33c9a9 --- /dev/null +++ b/test/0.8.9/contracts/WithdrawalVault__Harness.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +import {WithdrawalVault} from "contracts/0.8.9/WithdrawalVault.sol"; + +contract WithdrawalVault__Harness is WithdrawalVault { + constructor(address _lido, address _treasury) WithdrawalVault(_lido, _treasury) { + } + + function harness__initializeContractVersionTo(uint256 _version) external { + _initializeContractVersionTo(_version); + } +} diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 3069e04930..0ed3542ddc 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -9,14 +9,12 @@ import { ERC20__Harness, ERC721__Harness, Lido__MockForWithdrawalVault, - LidoLocator, WithdrawalsPredeployed_Mock, - WithdrawalVault, + WithdrawalVault__Harness, } from "typechain-types"; -import { MAX_UINT256, proxify } from "lib"; +import { MAX_UINT256, proxify, streccak } from "lib"; -import { deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; import { findEip7002TriggerableWithdrawalMockEvents, findEvents } from "./lib/triggerableWithdrawals/findEvents"; @@ -28,28 +26,27 @@ import { const PETRIFIED_VERSION = MAX_UINT256; +const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); + describe("WithdrawalVault.sol", () => { let owner: HardhatEthersSigner; - let user: HardhatEthersSigner; let treasury: HardhatEthersSigner; let validatorsExitBus: HardhatEthersSigner; + let stranger: HardhatEthersSigner; let originalState: string; let lido: Lido__MockForWithdrawalVault; let lidoAddress: string; - let locator: LidoLocator; - let locatorAddress: string; - let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; - let impl: WithdrawalVault; - let vault: WithdrawalVault; + let impl: WithdrawalVault__Harness; + let vault: WithdrawalVault__Harness; let vaultAddress: string; before(async () => { - [owner, user, treasury, validatorsExitBus] = await ethers.getSigners(); + [owner, treasury, validatorsExitBus, stranger] = await ethers.getSigners(); withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(1n); @@ -58,13 +55,9 @@ describe("WithdrawalVault.sol", () => { lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); lidoAddress = await lido.getAddress(); - locator = await deployLidoLocator({ lido, validatorsExitBusOracle: validatorsExitBus }); - locatorAddress = await locator.getAddress(); - - impl = await ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, locatorAddress]); + impl = await ethers.deployContract("WithdrawalVault__Harness", [lidoAddress, treasury.address], owner); [vault] = await proxify({ impl, admin: owner }); - vaultAddress = await vault.getAddress(); }); @@ -75,26 +68,20 @@ describe("WithdrawalVault.sol", () => { context("Constructor", () => { it("Reverts if the Lido address is zero", async () => { await expect( - ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address, validatorsExitBus.address]), + ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address]), ).to.be.revertedWithCustomError(vault, "ZeroAddress"); }); it("Reverts if the treasury address is zero", async () => { - await expect( - ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress, validatorsExitBus.address]), - ).to.be.revertedWithCustomError(vault, "ZeroAddress"); - }); - - it("Reverts if the validator exit buss address is zero", async () => { - await expect( - ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, ZeroAddress]), - ).to.be.revertedWithCustomError(vault, "ZeroAddress"); + await expect(ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress])).to.be.revertedWithCustomError( + vault, + "ZeroAddress", + ); }); it("Sets initial properties", async () => { expect(await vault.LIDO()).to.equal(lidoAddress, "Lido address"); expect(await vault.TREASURY()).to.equal(treasury.address, "Treasury address"); - expect(await vault.LOCATOR()).to.equal(locatorAddress, "Validator exit bus address"); }); it("Petrifies the implementation", async () => { @@ -107,26 +94,102 @@ describe("WithdrawalVault.sol", () => { }); context("initialize", () => { - it("Reverts if the contract is already initialized", async () => { - await vault.initialize(); + it("Should revert if the contract is already initialized", async () => { + await vault.initialize(owner); - await expect(vault.initialize()).to.be.revertedWithCustomError(vault, "NonZeroContractVersionOnInit"); + await expect(vault.initialize(owner)) + .to.be.revertedWithCustomError(vault, "UnexpectedContractVersion") + .withArgs(2, 0); }); it("Initializes the contract", async () => { - await expect(vault.initialize()) - .to.emit(vault, "ContractVersionSet") - .withArgs(1) - .and.to.emit(vault, "ContractVersionSet") - .withArgs(2); + await expect(vault.initialize(owner)).to.emit(vault, "ContractVersionSet").withArgs(2); + }); + + it("Should revert if admin address is zero", async () => { + await expect(vault.initialize(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Should set admin role during initialization", async () => { + const adminRole = await vault.DEFAULT_ADMIN_ROLE(); + expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); + expect(await vault.hasRole(adminRole, owner)).to.equal(false); + + await vault.initialize(owner); + + expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); + expect(await vault.hasRole(adminRole, owner)).to.equal(true); + expect(await vault.hasRole(adminRole, stranger)).to.equal(false); + }); + }); + + context("finalizeUpgrade_v2()", () => { + it("Should revert with UnexpectedContractVersion error when called on implementation", async () => { + await expect(impl.finalizeUpgrade_v2(owner)) + .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") + .withArgs(MAX_UINT256, 1); + }); + + it("Should revert with UnexpectedContractVersion error when called on deployed from scratch WithdrawalVaultV2", async () => { + await vault.initialize(owner); + + await expect(vault.finalizeUpgrade_v2(owner)) + .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") + .withArgs(2, 1); + }); + + context("Simulate upgrade from v1", () => { + beforeEach(async () => { + await vault.harness__initializeContractVersionTo(1); + }); + + it("Should revert if admin address is zero", async () => { + await expect(vault.finalizeUpgrade_v2(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Should set correct contract version", async () => { + expect(await vault.getContractVersion()).to.equal(1); + await vault.finalizeUpgrade_v2(owner); + expect(await vault.getContractVersion()).to.be.equal(2); + }); + + it("Should set admin role during finalization", async () => { + const adminRole = await vault.DEFAULT_ADMIN_ROLE(); + expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); + expect(await vault.hasRole(adminRole, owner)).to.equal(false); + + await vault.finalizeUpgrade_v2(owner); + + expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); + expect(await vault.hasRole(adminRole, owner)).to.equal(true); + expect(await vault.hasRole(adminRole, stranger)).to.equal(false); + }); + }); + }); + + context("Access control", () => { + it("Returns ACL roles", async () => { + expect(await vault.ADD_FULL_WITHDRAWAL_REQUEST_ROLE()).to.equal(ADD_FULL_WITHDRAWAL_REQUEST_ROLE); + }); + + it("Sets up roles", async () => { + await vault.initialize(owner); + + expect(await vault.getRoleMemberCount(ADD_FULL_WITHDRAWAL_REQUEST_ROLE)).to.equal(0); + expect(await vault.hasRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(false); + + await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + + expect(await vault.getRoleMemberCount(ADD_FULL_WITHDRAWAL_REQUEST_ROLE)).to.equal(1); + expect(await vault.hasRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(true); }); }); context("withdrawWithdrawals", () => { - beforeEach(async () => await vault.initialize()); + beforeEach(async () => await vault.initialize(owner)); it("Reverts if the caller is not Lido", async () => { - await expect(vault.connect(user).withdrawWithdrawals(0)).to.be.revertedWithCustomError(vault, "NotLido"); + await expect(vault.connect(stranger).withdrawWithdrawals(0)).to.be.revertedWithCustomError(vault, "NotLido"); }); it("Reverts if amount is 0", async () => { @@ -242,11 +305,15 @@ describe("WithdrawalVault.sol", () => { } context("add triggerable withdrawal requests", () => { + beforeEach(async () => { + await vault.initialize(owner); + await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + }); + it("Should revert if the caller is not Validator Exit Bus", async () => { - await expect(vault.connect(user).addFullWithdrawalRequests(["0x1234"])).to.be.revertedWithCustomError( - vault, - "NotValidatorExitBus", - ); + await expect( + vault.connect(stranger).addFullWithdrawalRequests(["0x1234"]), + ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_FULL_WITHDRAWAL_REQUEST_ROLE); }); it("Should revert if empty arrays are provided", async function () { From 1b2dd97db2da66e569c4cfc013b5ee255daf1bf4 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 15 Jan 2025 09:45:39 +0100 Subject: [PATCH 014/405] refactor: remove unnecessary memory allocation Access pubkeys and amounts directly instead of copying them to memory. --- contracts/0.8.9/lib/TriggerableWithdrawals.sol | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.9/lib/TriggerableWithdrawals.sol b/contracts/0.8.9/lib/TriggerableWithdrawals.sol index ab46819837..875b7beb70 100644 --- a/contracts/0.8.9/lib/TriggerableWithdrawals.sol +++ b/contracts/0.8.9/lib/TriggerableWithdrawals.sol @@ -111,11 +111,8 @@ library TriggerableWithdrawals { uint256 prevBalance = address(this).balance - totalWithdrawalFee; for (uint256 i = 0; i < keysCount; ++i) { - bytes memory pubkey = pubkeys[i]; - uint64 amount = amounts[i]; - - if(pubkey.length != 48) { - revert InvalidPubkeyLength(pubkey); + if(pubkeys[i].length != 48) { + revert InvalidPubkeyLength(pubkeys[i]); } uint256 feeToSend = feePerRequest; @@ -124,14 +121,14 @@ library TriggerableWithdrawals { feeToSend += unallocatedFee; } - bytes memory callData = abi.encodePacked(pubkey, amount); + bytes memory callData = abi.encodePacked(pubkeys[i], amounts[i]); (bool success, ) = WITHDRAWAL_REQUEST.call{value: feeToSend}(callData); if (!success) { - revert WithdrawalRequestAdditionFailed(pubkey, amount); + revert WithdrawalRequestAdditionFailed(pubkeys[i], amounts[i]); } - emit WithdrawalRequestAdded(pubkey, amount); + emit WithdrawalRequestAdded(pubkeys[i], amounts[i]); } assert(address(this).balance == prevBalance); From 9ca083257e68fecbcfbbfb88f27452dce2568e5a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 13 Jan 2025 17:20:02 +0400 Subject: [PATCH 015/405] feat: exit request data hash storage, triggerExitHashVerify method, happyPath and triggerExitHashVerify tests --- contracts/0.8.9/oracle/IWithdrawalVault.sol | 5 + contracts/0.8.9/oracle/ValidatorsExitBus.sol | 134 ++++++++++ .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 78 +++--- .../contracts/WithdrawalValut_MockForVebo.sol | 10 + ...alidator-exit-bus-oracle.happyPath.test.ts | 54 ++-- ...t-bus-oracle.triggerExitHashVerify.test.ts | 238 ++++++++++++++++++ test/deploy/validatorExitBusOracle.ts | 18 +- 7 files changed, 473 insertions(+), 64 deletions(-) create mode 100644 contracts/0.8.9/oracle/IWithdrawalVault.sol create mode 100644 contracts/0.8.9/oracle/ValidatorsExitBus.sol create mode 100644 test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts diff --git a/contracts/0.8.9/oracle/IWithdrawalVault.sol b/contracts/0.8.9/oracle/IWithdrawalVault.sol new file mode 100644 index 0000000000..7df3e01c5f --- /dev/null +++ b/contracts/0.8.9/oracle/IWithdrawalVault.sol @@ -0,0 +1,5 @@ +pragma solidity 0.8.9; + +interface IWithdrawalVault { + function addFullWithdrawalRequests(bytes[] calldata pubkeys) external; +} \ No newline at end of file diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol new file mode 100644 index 0000000000..d914b98d9a --- /dev/null +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; +import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; +import { IWithdrawalVault } from "./IWithdrawalVault.sol"; + +contract ValidatorsExitBus is AccessControlEnumerable { + using UnstructuredStorage for bytes32; + + /// @dev Errors + // error DuplicateExitRequest(); + error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); + error ZeroAddress(); + + /// Part of report data + struct ExitRequestData { + /// @dev Total number of validator exit requests in this report. Must not be greater + /// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport. + uint256 requestsCount; + + /// @dev Format of the validator exit requests data. Currently, only the + /// DATA_FORMAT_LIST=1 is supported. + uint256 dataFormat; + + /// @dev Validator exit requests data. Can differ based on the data format, + /// see the constant defining a specific data format below for more info. + bytes data; + } + + // TODO: make type optimization + struct DeliveryHistory { + uint256 blockNumber; + /// @dev Key index in exit request array + uint256 lastDeliveredKeyIndex; + } + // TODO: make type optimization + struct RequestStatus { + // Total items count in report (by default type(uint32).max, update on first report unpack) + uint256 totalItemsCount; + // Total processed items in report (by default 0) + uint256 deliveredItemsCount; + // Vebo contract version at the time of hash submittion + uint256 contractVersion; + DeliveryHistory[] deliverHistory; + } + + /// @notice The list format of the validator exit requests data. Used when all + /// requests fit into a single transaction. + /// + /// Each validator exit request is described by the following 64-byte array: + /// + /// MSB <------------------------------------------------------- LSB + /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | + /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | + /// + /// All requests are tightly packed into a byte array where requests follow + /// one another without any separator or padding, and passed to the `data` + /// field of the report structure. + /// + /// Requests must be sorted in the ascending order by the following compound + /// key: (moduleId, nodeOpId, validatorIndex). + /// + uint256 public constant DATA_FORMAT_LIST = 1; + + /// Length in bytes of packed request + uint256 internal constant PACKED_REQUEST_LENGTH = 64; + + /// Hash constant for mapping exit requests storage + bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = + keccak256("lido.ValidatorsExitBus.reportHashes"); + + /// @dev Storage slot: address withdrawalVaultContract + bytes32 internal constant WITHDRAWAL_VAULT_CONTRACT_POSITION = + keccak256("lido.ValidatorsExitBus.withdrawalVaultContract"); + + // ILidoLocator internal immutable LOCATOR; + + // TODO: read WV via locator + function _initialize_v2(address withdrawalVaultAddr) internal { + _setWithdrawalVault(withdrawalVaultAddr); + } + + function _setWithdrawalVault(address addr) internal { + if (addr == address(0)) revert ZeroAddress(); + + WITHDRAWAL_VAULT_CONTRACT_POSITION.setStorageAddress(addr); + } + + function triggerExitHashVerify(ExitRequestData calldata exitRequestData, uint256[] calldata keyIndexes) external payable { + bytes32 dataHash = keccak256(abi.encode(exitRequestData)); + RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash]; + + uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; + + uint256 offset; + bytes calldata data = exitRequestData.data; + bytes[] memory pubkeys = new bytes[](keyIndexes.length); + + assembly { + offset := data.offset + } + + for (uint256 i = 0; i < keyIndexes.length; i++) { + if (keyIndexes[i] > lastDeliveredKeyIndex) { + revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex); + } + uint256 requestOffset = offset + keyIndexes[i] * 64; + + bytes calldata pubkey; + + assembly { + pubkey.offset := add(requestOffset, 16) + pubkey.length := 48 + } + pubkeys[i] = pubkey; + + } + + address withdrawalVaultAddr = WITHDRAWAL_VAULT_CONTRACT_POSITION.getStorageAddress(); + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests(pubkeys); + } + + /// Storage helpers + function _storageExitRequestsHashes() internal pure returns ( + mapping(bytes32 => RequestStatus) storage r + ) { + bytes32 position = EXIT_REQUESTS_HASHES_POSITION; + assembly { + r.slot := position + } + } +} \ No newline at end of file diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 1937aff61f..8754839ebb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -10,6 +10,7 @@ import { PausableUntil } from "../utils/PausableUntil.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { BaseOracle } from "./BaseOracle.sol"; +import { ValidatorsExitBus } from "./ValidatorsExitBus.sol"; interface IOracleReportSanityChecker { @@ -17,7 +18,7 @@ interface IOracleReportSanityChecker { } -contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { +contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus { using UnstructuredStorage for bytes32; using SafeCast for uint256; @@ -109,6 +110,12 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { _initialize(consensusContract, consensusVersion, lastProcessingRefSlot); } + // TODO: replace with locator + function finalizeUpgrade_v2(address withdrawalVaultAddress) external { + _updateContractVersion(2); + _initialize_v2(withdrawalVaultAddress); + } + /// @notice Resume accepting validator exit requests /// /// @dev Reverts with `PausedExpected()` if contract is already resumed @@ -161,40 +168,9 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { /// Requests data /// - /// @dev Total number of validator exit requests in this report. Must not be greater - /// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport. - uint256 requestsCount; - - /// @dev Format of the validator exit requests data. Currently, only the - /// DATA_FORMAT_LIST=1 is supported. - uint256 dataFormat; - - /// @dev Validator exit requests data. Can differ based on the data format, - /// see the constant defining a specific data format below for more info. - bytes data; + ExitRequestData exitRequestData; } - /// @notice The list format of the validator exit requests data. Used when all - /// requests fit into a single transaction. - /// - /// Each validator exit request is described by the following 64-byte array: - /// - /// MSB <------------------------------------------------------- LSB - /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | - /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | - /// - /// All requests are tightly packed into a byte array where requests follow - /// one another without any separator or padding, and passed to the `data` - /// field of the report structure. - /// - /// Requests must be sorted in the ascending order by the following compound - /// key: (moduleId, nodeOpId, validatorIndex). - /// - uint256 public constant DATA_FORMAT_LIST = 1; - - /// Length in bytes of packed request - uint256 internal constant PACKED_REQUEST_LENGTH = 64; - /// @notice Submits report data for processing. /// /// @param data The data. See the `ReportData` structure's docs for details. @@ -216,10 +192,12 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { { _checkMsgSenderIsAllowedToSubmitData(); _checkContractVersion(contractVersion); + bytes32 exitRequestDataHash = keccak256(abi.encode(data.exitRequestData)); // it's a waste of gas to copy the whole calldata into mem but seems there's no way around - _checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data))); + _checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data.consensusVersion, data.refSlot, exitRequestDataHash))); _startProcessing(); _handleConsensusReportData(data); + _storeOracleExitRequestHash(exitRequestDataHash, data, contractVersion); } /// @notice Returns the total number of validator exit requests ever processed @@ -328,36 +306,37 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { } function _handleConsensusReportData(ReportData calldata data) internal { - if (data.dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(data.dataFormat); + if (data.exitRequestData.dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(data.exitRequestData.dataFormat); } - if (data.data.length % PACKED_REQUEST_LENGTH != 0) { + if (data.exitRequestData.data.length % PACKED_REQUEST_LENGTH != 0) { revert InvalidRequestsDataLength(); } + // TODO: next iteration will check ref slot deliveredReportAmount IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) - .checkExitBusOracleReport(data.requestsCount); + .checkExitBusOracleReport(data.exitRequestData.requestsCount); - if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { + if (data.exitRequestData.data.length / PACKED_REQUEST_LENGTH != data.exitRequestData.requestsCount) { revert UnexpectedRequestsDataLength(); } - _processExitRequestsList(data.data); + _processExitRequestsList(data.exitRequestData.data); _storageDataProcessingState().value = DataProcessingState({ refSlot: data.refSlot.toUint64(), - requestsCount: data.requestsCount.toUint64(), - requestsProcessed: data.requestsCount.toUint64(), + requestsCount: data.exitRequestData.requestsCount.toUint64(), + requestsProcessed: data.exitRequestData.requestsCount.toUint64(), dataFormat: uint16(DATA_FORMAT_LIST) }); - if (data.requestsCount == 0) { + if (data.exitRequestData.requestsCount == 0) { return; } TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256( - TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.requestsCount + TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.exitRequestData.requestsCount ); } @@ -439,6 +418,17 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil { return (moduleId << 40) | nodeOpId; } + function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { + mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); + // if (hashes[hash].itemsCount > 0 ) revert DuplicateExitRequest(); + + RequestStatus storage request = hashes[exitRequestHash]; + request.totalItemsCount = report.exitRequestData.requestsCount; + request.deliveredItemsCount = report.exitRequestData.requestsCount; + request.contractVersion = contractVersion; + request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.exitRequestData.requestsCount - 1})); + } + /// /// Storage helpers /// diff --git a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol new file mode 100644 index 0000000000..9d60ad048d --- /dev/null +++ b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol @@ -0,0 +1,10 @@ +pragma solidity 0.8.9; + +contract WithdrawalVault__MockForVebo { + + event AddFullWithdrawalRequestsCalled(bytes[] pubkeys); + + function addFullWithdrawalRequests(bytes[] calldata pubkeys) external { + emit AddFullWithdrawalRequestsCalled(pubkeys); + } +} \ No newline at end of file diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 615050cf49..8cab0dac2a 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -29,11 +29,11 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; + let withdrawalVault: WithdrawalVault__MockForVebo; let oracleVersion: bigint; let exitRequests: ExitRequest[]; let reportFields: ReportFields; - let reportItems: ReturnType; let reportHash: string; let member1: HardhatEthersSigner; @@ -50,21 +50,29 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { valPubkey: string; } - interface ReportFields { - consensusVersion: bigint; - refSlot: bigint; + interface ExitRequestData { requestsCount: number; dataFormat: number; data: string; } - const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); - return ethers.keccak256(data); - }; + interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + exitRequestData: ExitRequestData; + } - const getValidatorsExitBusReportDataItems = (r: ReportFields) => { - return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; + const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { + const exitRequestItems = [ + items.exitRequestData.requestsCount, + items.exitRequestData.dataFormat, + items.exitRequestData.data, + ]; + const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); + const dataHash = ethers.keccak256(exitRequestData); + const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); + return ethers.keccak256(data); }; const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { @@ -81,11 +89,13 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; + withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, consensus, + withdrawalVault, resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); @@ -146,13 +156,14 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, refSlot: refSlot, - requestsCount: exitRequests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), + exitRequestData: { + requestsCount: exitRequests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }, }; - reportItems = getValidatorsExitBusReportDataItems(reportFields); - reportHash = calcValidatorsExitBusReportDataHash(reportItems); + reportHash = calcValidatorsExitBusReportDataHash(reportFields); await triggerConsensusOnHash(reportHash); }); @@ -202,9 +213,14 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { }); it("a data not matching the consensus hash cannot be submitted", async () => { - const invalidReport = { ...reportFields, requestsCount: reportFields.requestsCount + 1 }; - const invalidReportItems = getValidatorsExitBusReportDataItems(invalidReport); - const invalidReportHash = calcValidatorsExitBusReportDataHash(invalidReportItems); + const invalidReport = { + ...reportFields, + exitRequestData: { + ...reportFields.exitRequestData, + requestsCount: reportFields.exitRequestData.requestsCount + 1, + }, + }; + const invalidReportHash = calcValidatorsExitBusReportDataHash(invalidReport); await expect(oracle.connect(member1).submitReportData(invalidReport, oracleVersion)) .to.be.revertedWithCustomError(oracle, "UnexpectedDataHash") diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts new file mode 100644 index 0000000000..a8e701b77f --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -0,0 +1,238 @@ +import { expect } from "chai"; +import { ZeroHash } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; + +import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; + +import { + computeTimestampAtSlot, + DATA_FORMAT_LIST, + deployVEBO, + initVEBO, + SECONDS_PER_FRAME, + SLOTS_PER_FRAME, +} from "test/deploy"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; + +describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + let withdrawalVault: WithdrawalVault__MockForVebo; + + let oracleVersion: bigint; + let exitRequests: ExitRequest[]; + let reportFields: ReportFields; + let reportHash: string; + + let member1: HardhatEthersSigner; + let member2: HardhatEthersSigner; + let member3: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + const LAST_PROCESSING_REF_SLOT = 1; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ExitRequestData { + requestsCount: number; + dataFormat: number; + data: string; + } + + interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + exitRequestData: ExitRequestData; + } + + const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { + const exitRequestItems = [ + items.exitRequestData.requestsCount, + items.exitRequestData.dataFormat, + items.exitRequestData.data, + ]; + const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); + const dataHash = ethers.keccak256(exitRequestData); + const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); + return ethers.keccak256(data); + }; + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + withdrawalVault = deployed.withdrawalVault; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + withdrawalVault, + resumeAfterDeploy: true, + lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, + }); + + oracleVersion = await oracle.getContractVersion(); + + await consensus.addMember(member1, 1); + await consensus.addMember(member2, 2); + await consensus.addMember(member3, 2); + }; + + before(async () => { + [admin, member1, member2, member3, stranger] = await ethers.getSigners(); + + await deploy(); + }); + + const triggerConsensusOnHash = async (hash: string) => { + const { refSlot } = await consensus.getCurrentFrame(); + await consensus.connect(member1).submitReport(refSlot, hash, CONSENSUS_VERSION); + await consensus.connect(member3).submitReport(refSlot, hash, CONSENSUS_VERSION); + expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); + }; + + it("initially, consensus report is empty and is not being processed", async () => { + const report = await oracle.getConsensusReport(); + expect(report.hash).to.equal(ZeroHash); + + expect(report.processingDeadlineTime).to.equal(0); + expect(report.processingStarted).to.equal(false); + + const frame = await consensus.getCurrentFrame(); + const procState = await oracle.getProcessingState(); + + expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); + expect(procState.dataHash).to.equal(ZeroHash); + expect(procState.processingDeadlineTime).to.equal(0); + expect(procState.dataSubmitted).to.equal(false); + expect(procState.dataFormat).to.equal(0); + expect(procState.requestsCount).to.equal(0); + expect(procState.requestsSubmitted).to.equal(0); + }); + + it("reference slot of the empty initial consensus report is set to the last processing slot passed to the initialize function", async () => { + const report = await oracle.getConsensusReport(); + expect(report.refSlot).to.equal(LAST_PROCESSING_REF_SLOT); + }); + + it("committee reaches consensus on a report hash", async () => { + const { refSlot } = await consensus.getCurrentFrame(); + + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + ]; + + reportFields = { + consensusVersion: CONSENSUS_VERSION, + refSlot: refSlot, + exitRequestData: { + requestsCount: exitRequests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }, + }; + + reportHash = calcValidatorsExitBusReportDataHash(reportFields); + + await triggerConsensusOnHash(reportHash); + }); + + it("oracle gets the report hash", async () => { + const report = await oracle.getConsensusReport(); + expect(report.hash).to.equal(reportHash); + expect(report.refSlot).to.equal(reportFields.refSlot); + expect(report.processingDeadlineTime).to.equal(computeTimestampAtSlot(report.refSlot + SLOTS_PER_FRAME)); + + expect(report.processingStarted).to.equal(false); + + const frame = await consensus.getCurrentFrame(); + const procState = await oracle.getProcessingState(); + + expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); + expect(procState.dataHash).to.equal(reportHash); + expect(procState.processingDeadlineTime).to.equal(computeTimestampAtSlot(frame.reportProcessingDeadlineSlot)); + expect(procState.dataSubmitted).to.equal(false); + expect(procState.dataFormat).to.equal(0); + expect(procState.requestsCount).to.equal(0); + expect(procState.requestsSubmitted).to.equal(0); + }); + + it("some time passes", async () => { + await consensus.advanceTimeBy(SECONDS_PER_FRAME / 3n); + }); + + it("a committee member submits the report data, exit requests are emitted", async () => { + const tx = await oracle.connect(member1).submitReportData(reportFields, oracleVersion); + + await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); + expect((await oracle.getConsensusReport()).processingStarted).to.equal(true); + + const timestamp = await oracle.getTime(); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + }); + + it("reports are marked as processed", async () => { + const frame = await consensus.getCurrentFrame(); + const procState = await oracle.getProcessingState(); + + expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); + expect(procState.dataHash).to.equal(reportHash); + expect(procState.processingDeadlineTime).to.equal(computeTimestampAtSlot(frame.reportProcessingDeadlineSlot)); + expect(procState.dataSubmitted).to.equal(true); + expect(procState.dataFormat).to.equal(DATA_FORMAT_LIST); + expect(procState.requestsCount).to.equal(exitRequests.length); + expect(procState.requestsSubmitted).to.equal(exitRequests.length); + }); + + it("last requested validator indices are updated", async () => { + const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n]); + const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n]); + + expect([...indices1]).to.have.ordered.members([2n, -1n, -1n]); + expect([...indices2]).to.have.ordered.members([1n, -1n, -1n]); + }); + + it("someone submitted exit report data and triggered exit", async () => { + const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 2]); + + await expect(tx) + .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") + .withArgs([PUBKEYS[0], PUBKEYS[1], PUBKEYS[2]]); + }); +}); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 1b5e0e2805..7acca3e918 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -1,7 +1,12 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { HashConsensus__Harness, ReportProcessor__Mock, ValidatorsExitBusOracle } from "typechain-types"; +import { + HashConsensus__Harness, + ReportProcessor__Mock, + ValidatorsExitBusOracle, + WithdrawalVault__MockForVebo, +} from "typechain-types"; import { CONSENSUS_VERSION, @@ -34,6 +39,10 @@ async function deployOracleReportSanityCheckerForExitBus(lidoLocator: string, ad return await ethers.deployContract("OracleReportSanityChecker", [lidoLocator, admin, limitsList]); } +async function deployWithdrawalVault() { + return await ethers.deployContract("WithdrawalVault__MockForVebo"); +} + export async function deployVEBO( admin: string, { @@ -72,11 +81,14 @@ export async function deployVEBO( await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot); + const withdrawalVault = await deployWithdrawalVault(); + return { locatorAddr, oracle, consensus, oracleReportSanityChecker, + withdrawalVault, }; } @@ -84,6 +96,7 @@ interface VEBOConfig { admin: string; oracle: ValidatorsExitBusOracle; consensus: HashConsensus__Harness; + withdrawalVault: WithdrawalVault__MockForVebo; dataSubmitter?: string; consensusVersion?: bigint; lastProcessingRefSlot?: number; @@ -94,6 +107,7 @@ export async function initVEBO({ admin, oracle, consensus, + withdrawalVault, dataSubmitter = undefined, consensusVersion = CONSENSUS_VERSION, lastProcessingRefSlot = 0, @@ -101,6 +115,8 @@ export async function initVEBO({ }: VEBOConfig) { const initTx = await oracle.initialize(admin, await consensus.getAddress(), consensusVersion, lastProcessingRefSlot); + await oracle.finalizeUpgrade_v2(withdrawalVault); + await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_VERSION_ROLE(), admin); await oracle.grantRole(await oracle.PAUSE_ROLE(), admin); From b0b402cde3a0c85eead990b2ba94364f3a9af5c4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 17 Jan 2025 01:54:47 +0400 Subject: [PATCH 016/405] Use of withdrawal vault contract in veb --- contracts/0.8.9/oracle/IWithdrawalVault.sol | 5 --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 44 +++++++++++++------ .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 6 +-- .../contracts/WithdrawalValut_MockForVebo.sol | 4 ++ ...t-bus-oracle.triggerExitHashVerify.test.ts | 11 +++-- test/deploy/validatorExitBusOracle.ts | 8 ++-- 6 files changed, 49 insertions(+), 29 deletions(-) delete mode 100644 contracts/0.8.9/oracle/IWithdrawalVault.sol diff --git a/contracts/0.8.9/oracle/IWithdrawalVault.sol b/contracts/0.8.9/oracle/IWithdrawalVault.sol deleted file mode 100644 index 7df3e01c5f..0000000000 --- a/contracts/0.8.9/oracle/IWithdrawalVault.sol +++ /dev/null @@ -1,5 +0,0 @@ -pragma solidity 0.8.9; - -interface IWithdrawalVault { - function addFullWithdrawalRequests(bytes[] calldata pubkeys) external; -} \ No newline at end of file diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d914b98d9a..8a722aa118 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -4,15 +4,22 @@ pragma solidity 0.8.9; import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; -import { IWithdrawalVault } from "./IWithdrawalVault.sol"; +import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; + +interface IWithdrawalVault { + function addFullWithdrawalRequests(bytes[] calldata pubkeys) external payable; + + function getWithdrawalRequestFee() external view returns (uint256); +} + contract ValidatorsExitBus is AccessControlEnumerable { using UnstructuredStorage for bytes32; /// @dev Errors - // error DuplicateExitRequest(); error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); error ZeroAddress(); + error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); /// Part of report data struct ExitRequestData { @@ -71,27 +78,33 @@ contract ValidatorsExitBus is AccessControlEnumerable { bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); - /// @dev Storage slot: address withdrawalVaultContract - bytes32 internal constant WITHDRAWAL_VAULT_CONTRACT_POSITION = - keccak256("lido.ValidatorsExitBus.withdrawalVaultContract"); + bytes32 private constant LOCATOR_CONTRACT_POSITION = keccak256("lido.ValidatorsExitBus.locatorContract"); - // ILidoLocator internal immutable LOCATOR; - - // TODO: read WV via locator - function _initialize_v2(address withdrawalVaultAddr) internal { - _setWithdrawalVault(withdrawalVaultAddr); + function _initialize_v2(address locatorAddr) internal { + _setLocatorAddress(locatorAddr); } - function _setWithdrawalVault(address addr) internal { + function _setLocatorAddress(address addr) internal { if (addr == address(0)) revert ZeroAddress(); - WITHDRAWAL_VAULT_CONTRACT_POSITION.setStorageAddress(addr); + LOCATOR_CONTRACT_POSITION.setStorageAddress(addr); } function triggerExitHashVerify(ExitRequestData calldata exitRequestData, uint256[] calldata keyIndexes) external payable { bytes32 dataHash = keccak256(abi.encode(exitRequestData)); RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash]; + address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); + address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); + uint256 fee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); + uint requestsFee = keyIndexes.length * fee; + + if (msg.value < requestsFee) { + revert FeeNotEnough(fee, keyIndexes.length, msg.value); + } + + uint256 refund = msg.value - requestsFee; + uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; uint256 offset; @@ -118,8 +131,13 @@ contract ValidatorsExitBus is AccessControlEnumerable { } - address withdrawalVaultAddr = WITHDRAWAL_VAULT_CONTRACT_POSITION.getStorageAddress(); IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests(pubkeys); + + if (refund > 0) { + (bool success, ) = msg.sender.call{value: refund}(""); + require(success, "Refund failed"); + } + } /// Storage helpers diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 8754839ebb..bc65df9349 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -110,10 +110,9 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus _initialize(consensusContract, consensusVersion, lastProcessingRefSlot); } - // TODO: replace with locator - function finalizeUpgrade_v2(address withdrawalVaultAddress) external { + function finalizeUpgrade_v2() external { _updateContractVersion(2); - _initialize_v2(withdrawalVaultAddress); + _initialize_v2(address(LOCATOR)); } /// @notice Resume accepting validator exit requests @@ -420,7 +419,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); - // if (hashes[hash].itemsCount > 0 ) revert DuplicateExitRequest(); RequestStatus storage request = hashes[exitRequestHash]; request.totalItemsCount = report.exitRequestData.requestsCount; diff --git a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol index 9d60ad048d..97ca6e6011 100644 --- a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol +++ b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol @@ -7,4 +7,8 @@ contract WithdrawalVault__MockForVebo { function addFullWithdrawalRequests(bytes[] calldata pubkeys) external { emit AddFullWithdrawalRequestsCalled(pubkeys); } + + function getWithdrawalRequestFee() external view returns (uint256) { + return 1; + } } \ No newline at end of file diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index a8e701b77f..d9dc51c8a9 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -39,7 +39,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { let member1: HardhatEthersSigner; let member2: HardhatEthersSigner; let member3: HardhatEthersSigner; - let stranger: HardhatEthersSigner; const LAST_PROCESSING_REF_SLOT = 1; @@ -108,7 +107,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }; before(async () => { - [admin, member1, member2, member3, stranger] = await ethers.getSigners(); + [admin, member1, member2, member3] = await ethers.getSigners(); await deploy(); }); @@ -229,10 +228,16 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("someone submitted exit report data and triggered exit", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 2]); + const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 2], { value: 3 }); await expect(tx) .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") .withArgs([PUBKEYS[0], PUBKEYS[1], PUBKEYS[2]]); }); + + it("someone submitted exit report data and triggered exit again", async () => { + const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1], { value: 2 }); + + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs([PUBKEYS[0], PUBKEYS[1]]); + }); }); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 7acca3e918..fd95c7bd92 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -67,9 +67,12 @@ export async function deployVEBO( const { ao, lido } = await deployMockAccountingOracle(secondsPerSlot, genesisTime); + const withdrawalVault = await deployWithdrawalVault(); + await updateLidoLocatorImplementation(locatorAddr, { lido: await lido.getAddress(), accountingOracle: await ao.getAddress(), + withdrawalVault, }); const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locatorAddr, admin); @@ -81,8 +84,6 @@ export async function deployVEBO( await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot); - const withdrawalVault = await deployWithdrawalVault(); - return { locatorAddr, oracle, @@ -107,7 +108,6 @@ export async function initVEBO({ admin, oracle, consensus, - withdrawalVault, dataSubmitter = undefined, consensusVersion = CONSENSUS_VERSION, lastProcessingRefSlot = 0, @@ -115,7 +115,7 @@ export async function initVEBO({ }: VEBOConfig) { const initTx = await oracle.initialize(admin, await consensus.getAddress(), consensusVersion, lastProcessingRefSlot); - await oracle.finalizeUpgrade_v2(withdrawalVault); + await oracle.finalizeUpgrade_v2(); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_VERSION_ROLE(), admin); From d26dddced348163edfc490794638496f8e07a68c Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 17 Jan 2025 12:06:21 +0100 Subject: [PATCH 017/405] feat: specify fee per request instead of total fee in TW library --- contracts/0.8.9/WithdrawalVault.sol | 20 +- .../0.8.9/lib/TriggerableWithdrawals.sol | 45 ++-- .../TriggerableWithdrawals_Harness.sol | 12 +- .../triggerableWithdrawals.test.ts | 200 ++++++++---------- test/0.8.9/withdrawalVault.test.ts | 98 +++++---- 5 files changed, 186 insertions(+), 189 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 0e8b7dc06b..f9f060e544 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -52,6 +52,8 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); + error InsufficientTriggerableWithdrawalFee(uint256 providedTotalFee, uint256 requiredTotalFee, uint256 requestCount); + /** * @param _lido the Lido token (stETH) address * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) @@ -144,7 +146,23 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { function addFullWithdrawalRequests( bytes[] calldata pubkeys ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) { - TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, msg.value); + uint256 prevBalance = address(this).balance - msg.value; + + uint256 minFeePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); + uint256 totalFee = pubkeys.length * minFeePerRequest; + + if(totalFee > msg.value) { + revert InsufficientTriggerableWithdrawalFee(msg.value, totalFee, pubkeys.length); + } + + TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, minFeePerRequest); + + uint256 refund = msg.value - totalFee; + if (refund > 0) { + msg.sender.call{value: refund}(""); + } + + assert(address(this).balance == prevBalance); } function getWithdrawalRequestFee() external view returns (uint256) { diff --git a/contracts/0.8.9/lib/TriggerableWithdrawals.sol b/contracts/0.8.9/lib/TriggerableWithdrawals.sol index 875b7beb70..ff3bd43b4d 100644 --- a/contracts/0.8.9/lib/TriggerableWithdrawals.sol +++ b/contracts/0.8.9/lib/TriggerableWithdrawals.sol @@ -8,7 +8,7 @@ library TriggerableWithdrawals { error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); error InsufficientBalance(uint256 balance, uint256 totalWithdrawalFee); - error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 providedTotalFee); + error InsufficientRequestFee(uint256 feePerRequest, uint256 minFeePerRequest); error WithdrawalRequestFeeReadFailed(); error InvalidPubkeyLength(bytes pubkey); @@ -25,10 +25,10 @@ library TriggerableWithdrawals { */ function addFullWithdrawalRequests( bytes[] calldata pubkeys, - uint256 totalWithdrawalFee + uint256 feePerRequest ) internal { uint64[] memory amounts = new uint64[](pubkeys.length); - _addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + _addWithdrawalRequests(pubkeys, amounts, feePerRequest); } /** @@ -43,7 +43,7 @@ library TriggerableWithdrawals { function addPartialWithdrawalRequests( bytes[] calldata pubkeys, uint64[] calldata amounts, - uint256 totalWithdrawalFee + uint256 feePerRequest ) internal { _requireArrayLengthsMatch(pubkeys, amounts); @@ -53,7 +53,7 @@ library TriggerableWithdrawals { } } - _addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + _addWithdrawalRequests(pubkeys, amounts, feePerRequest); } /** @@ -67,10 +67,10 @@ library TriggerableWithdrawals { function addWithdrawalRequests( bytes[] calldata pubkeys, uint64[] calldata amounts, - uint256 totalWithdrawalFee + uint256 feePerRequest ) internal { _requireArrayLengthsMatch(pubkeys, amounts); - _addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + _addWithdrawalRequests(pubkeys, amounts, feePerRequest); } /** @@ -90,39 +90,36 @@ library TriggerableWithdrawals { function _addWithdrawalRequests( bytes[] calldata pubkeys, uint64[] memory amounts, - uint256 totalWithdrawalFee + uint256 feePerRequest ) internal { uint256 keysCount = pubkeys.length; if (keysCount == 0) { revert NoWithdrawalRequests(); } - if(address(this).balance < totalWithdrawalFee) { - revert InsufficientBalance(address(this).balance, totalWithdrawalFee); + uint256 minFeePerRequest = getWithdrawalRequestFee(); + + if (feePerRequest == 0) { + feePerRequest = minFeePerRequest; } - uint256 minFeePerRequest = getWithdrawalRequestFee(); - if (minFeePerRequest * keysCount > totalWithdrawalFee) { - revert FeeNotEnough(minFeePerRequest, keysCount, totalWithdrawalFee); + if (feePerRequest < minFeePerRequest) { + revert InsufficientRequestFee(feePerRequest, minFeePerRequest); } - uint256 feePerRequest = totalWithdrawalFee / keysCount; - uint256 unallocatedFee = totalWithdrawalFee % keysCount; - uint256 prevBalance = address(this).balance - totalWithdrawalFee; + uint256 totalWithdrawalFee = feePerRequest * keysCount; + + if(address(this).balance < totalWithdrawalFee) { + revert InsufficientBalance(address(this).balance, totalWithdrawalFee); + } for (uint256 i = 0; i < keysCount; ++i) { if(pubkeys[i].length != 48) { revert InvalidPubkeyLength(pubkeys[i]); } - uint256 feeToSend = feePerRequest; - - if (i == keysCount - 1) { - feeToSend += unallocatedFee; - } - bytes memory callData = abi.encodePacked(pubkeys[i], amounts[i]); - (bool success, ) = WITHDRAWAL_REQUEST.call{value: feeToSend}(callData); + (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(callData); if (!success) { revert WithdrawalRequestAdditionFailed(pubkeys[i], amounts[i]); @@ -130,8 +127,6 @@ library TriggerableWithdrawals { emit WithdrawalRequestAdded(pubkeys[i], amounts[i]); } - - assert(address(this).balance == prevBalance); } function _requireArrayLengthsMatch( diff --git a/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol index 261f1a8cd0..82e4b308f5 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol @@ -5,25 +5,25 @@ import {TriggerableWithdrawals} from "contracts/0.8.9/lib/TriggerableWithdrawals contract TriggerableWithdrawals_Harness { function addFullWithdrawalRequests( bytes[] calldata pubkeys, - uint256 totalWithdrawalFee + uint256 feePerRequest ) external { - TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee); + TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); } function addPartialWithdrawalRequests( bytes[] calldata pubkeys, uint64[] calldata amounts, - uint256 totalWithdrawalFee + uint256 feePerRequest ) external { - TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, feePerRequest); } function addWithdrawalRequests( bytes[] calldata pubkeys, uint64[] calldata amounts, - uint256 totalWithdrawalFee + uint256 feePerRequest ) external { - TriggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, totalWithdrawalFee); + TriggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, feePerRequest); } function getWithdrawalRequestFee() external view returns (uint256) { diff --git a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index 83c57ca260..af1325180a 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -51,10 +51,8 @@ describe("TriggerableWithdrawals.sol", () => { afterEach(async () => await Snapshot.restore(originalState)); - async function getFee(requestsCount: number): Promise { - const fee = await triggerableWithdrawals.getWithdrawalRequestFee(); - - return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); + async function getFee(): Promise { + return await triggerableWithdrawals.getWithdrawalRequestFee(); } context("eip 7002 contract", () => { @@ -105,7 +103,7 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeys } = generateWithdrawalRequestPayload(2); const amounts = [1n]; - const fee = await getFee(pubkeys.length); + const fee = await getFee(); await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") @@ -138,34 +136,19 @@ describe("TriggerableWithdrawals.sol", () => { await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei - // 1. Should revert if no fee is sent - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, 0n)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "FeeNotEnough", - ); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, 0n), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); - - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, 0n)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "FeeNotEnough", - ); - // 2. Should revert if fee is less than required const insufficientFee = 2n; - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, insufficientFee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, insufficientFee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") + .withArgs(2n, 3n); - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, insufficientFee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, insufficientFee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") + .withArgs(2n, 3n); - await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, insufficientFee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "FeeNotEnough"); + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, insufficientFee)) + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") + .withArgs(2n, 3n); }); it("Should revert if any pubkey is not 48 bytes", async function () { @@ -173,7 +156,7 @@ describe("TriggerableWithdrawals.sol", () => { const pubkeys = ["0x1234"]; const amounts = [10n]; - const fee = await getFee(pubkeys.length); + const fee = await getFee(); await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") @@ -192,7 +175,7 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeys } = generateWithdrawalRequestPayload(1); const amounts = [10n]; - const fee = await getFee(pubkeys.length); + const fee = await getFee(); // Set mock to fail on add await withdrawalsPredeployed.setFailOnAddRequest(true); @@ -215,7 +198,7 @@ describe("TriggerableWithdrawals.sol", () => { it("Should revert when a full withdrawal amount is included in 'addPartialWithdrawalRequests'", async function () { const { pubkeys } = generateWithdrawalRequestPayload(2); const amounts = [1n, 0n]; // Partial and Full withdrawal - const fee = await getFee(pubkeys.length); + const fee = await getFee(); await expect( triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee), @@ -223,27 +206,27 @@ describe("TriggerableWithdrawals.sol", () => { }); it("Should revert when balance is less than total withdrawal fee", async function () { - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const keysCount = 2; const fee = 10n; - const totalWithdrawalFee = 20n; const balance = 19n; + const expectedMinimalBalance = 20n; + + const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(keysCount); await withdrawalsPredeployed.setFee(fee); await setBalance(await triggerableWithdrawals.getAddress(), balance); - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)) + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") - .withArgs(balance, totalWithdrawalFee); + .withArgs(balance, expectedMinimalBalance); - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), - ) + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") - .withArgs(balance, totalWithdrawalFee); + .withArgs(balance, expectedMinimalBalance); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee)) + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") - .withArgs(balance, totalWithdrawalFee); + .withArgs(balance, expectedMinimalBalance); }); it("Should revert when fee read fails", async function () { @@ -266,31 +249,29 @@ describe("TriggerableWithdrawals.sol", () => { ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); }); + // ToDo: should accept when fee not defined + it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const fee = 3n; await withdrawalsPredeployed.setFee(3n); - const totalWithdrawalFee = 9n; - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee); - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee); + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); // Check extremely high fee - await withdrawalsPredeployed.setFee(ethers.parseEther("10")); - const largeTotalWithdrawalFee = ethers.parseEther("30"); + const highFee = ethers.parseEther("10"); + await withdrawalsPredeployed.setFee(highFee); - await triggerableWithdrawals.connect(actor).deposit({ value: largeTotalWithdrawalFee * BigInt(requestCount) }); + await triggerableWithdrawals.connect(actor).deposit({ value: highFee * BigInt(requestCount) * 3n }); - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, largeTotalWithdrawalFee); - await triggerableWithdrawals.addPartialWithdrawalRequests( - pubkeys, - partialWithdrawalAmounts, - largeTotalWithdrawalFee, - ); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, largeTotalWithdrawalFee); + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, highFee); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, highFee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, highFee); }); it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { @@ -299,23 +280,19 @@ describe("TriggerableWithdrawals.sol", () => { generateWithdrawalRequestPayload(requestCount); await withdrawalsPredeployed.setFee(3n); - const fee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei + const fee = 4n; await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); // Check when the provided fee extremely exceeds the required amount - const largeTotalWithdrawalFee = ethers.parseEther("10"); - await triggerableWithdrawals.connect(actor).deposit({ value: largeTotalWithdrawalFee * BigInt(requestCount) }); + const largeFee = ethers.parseEther("10"); + await triggerableWithdrawals.connect(actor).deposit({ value: largeFee * BigInt(requestCount) * 3n }); - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, largeTotalWithdrawalFee); - await triggerableWithdrawals.addPartialWithdrawalRequests( - pubkeys, - partialWithdrawalAmounts, - largeTotalWithdrawalFee, - ); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, largeTotalWithdrawalFee); + await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, largeFee); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, largeFee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, largeFee); }); it("Should correctly deduct the exact fee amount from the contract balance", async function () { @@ -323,13 +300,13 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - await withdrawalsPredeployed.setFee(3n); - const fee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei + const fee = 4n; + const expectedTotalWithdrawalFee = 12n; // fee * requestCount; const testFeeDeduction = async (addRequests: () => Promise) => { const initialBalance = await getWithdrawalCredentialsContractBalance(); await addRequests(); - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - fee); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); }; await testFeeDeduction(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); @@ -344,28 +321,26 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - await withdrawalsPredeployed.setFee(3n); - const totalWithdrawalFee = 9n + 1n; + const fee = 3n; + const expectedTotalWithdrawalFee = 9n; // fee * requestCount; const testFeeTransfer = async (addRequests: () => Promise) => { const initialBalance = await getWithdrawalsPredeployedContractBalance(); await addRequests(); - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + totalWithdrawalFee); + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); }; - await testFeeTransfer(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee)); + await testFeeTransfer(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); await testFeeTransfer(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), - ); - await testFeeTransfer(() => - triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), ); + await testFeeTransfer(() => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); }); it("Should accept full, partial, and mixed withdrawal requests via 'addWithdrawalRequests' function", async function () { const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(3); - const fee = await getFee(pubkeys.length); + const fee = await getFee(); await triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); await triggerableWithdrawals.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); @@ -421,13 +396,11 @@ describe("TriggerableWithdrawals.sol", () => { }); it("Should verify correct fee distribution among requests", async function () { - await withdrawalsPredeployed.setFee(2n); - const requestCount = 5; const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const testFeeDistribution = async (totalWithdrawalFee: bigint, expectedFeePerRequest: bigint[]) => { + const testFeeDistribution = async (fee: bigint) => { const checkEip7002MockEvents = async (addRequests: () => Promise) => { const tx = await addRequests(); @@ -436,34 +409,31 @@ describe("TriggerableWithdrawals.sol", () => { expect(events.length).to.equal(requestCount); for (let i = 0; i < requestCount; i++) { - expect(events[i].args[1]).to.equal(expectedFeePerRequest[i]); + expect(events[i].args[1]).to.equal(fee); } }; - await checkEip7002MockEvents(() => - triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), - ); + await checkEip7002MockEvents(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); await checkEip7002MockEvents(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), ); await checkEip7002MockEvents(() => - triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), ); }; - await testFeeDistribution(10n, [2n, 2n, 2n, 2n, 2n]); - await testFeeDistribution(11n, [2n, 2n, 2n, 2n, 3n]); - await testFeeDistribution(14n, [2n, 2n, 2n, 2n, 6n]); - await testFeeDistribution(15n, [3n, 3n, 3n, 3n, 3n]); + await testFeeDistribution(1n); + await testFeeDistribution(2n); + await testFeeDistribution(3n); }); it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { const requestCount = 16; const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const totalWithdrawalFee = 333n; + const fee = 333n; const normalize = (hex: string) => (hex.startsWith("0x") ? hex.slice(2).toLowerCase() : hex.toLowerCase()); @@ -492,18 +462,17 @@ describe("TriggerableWithdrawals.sol", () => { }; await testEncoding( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee), pubkeys, fullWithdrawalAmounts, ); await testEncoding( - () => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), pubkeys, partialWithdrawalAmounts, ); await testEncoding( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), pubkeys, mixedWithdrawalAmounts, ); @@ -544,43 +513,44 @@ describe("TriggerableWithdrawals.sol", () => { } const testCasesForWithdrawalRequests = [ - { requestCount: 1, extraFee: 0n }, - { requestCount: 1, extraFee: 100n }, - { requestCount: 1, extraFee: 100_000_000_000n }, - { requestCount: 3, extraFee: 0n }, - { requestCount: 3, extraFee: 1n }, - { requestCount: 7, extraFee: 3n }, - { requestCount: 10, extraFee: 0n }, - { requestCount: 10, extraFee: 100_000_000_000n }, - { requestCount: 100, extraFee: 0n }, + { requestCount: 1, fee: 0n }, + { requestCount: 1, fee: 100n }, + { requestCount: 1, fee: 100_000_000_000n }, + { requestCount: 3, fee: 0n }, + { requestCount: 3, fee: 1n }, + { requestCount: 7, fee: 3n }, + { requestCount: 10, fee: 0n }, + { requestCount: 10, fee: 100_000_000_000n }, + { requestCount: 100, fee: 0n }, ]; - testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { - it(`Should successfully add ${requestCount} requests with extra fee ${extraFee} and emit events`, async () => { + testCasesForWithdrawalRequests.forEach(({ requestCount, fee }) => { + it(`Should successfully add ${requestCount} requests with fee ${fee} and emit events`, async () => { const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const totalWithdrawalFee = (await getFee(pubkeys.length)) + extraFee; + + const requestFee = fee == 0n ? await getFee() : fee; + const expectedTotalWithdrawalFee = requestFee * BigInt(requestCount); await addWithdrawalRequests( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, totalWithdrawalFee), + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee), pubkeys, fullWithdrawalAmounts, - totalWithdrawalFee, + expectedTotalWithdrawalFee, ); await addWithdrawalRequests( - () => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, totalWithdrawalFee), + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), pubkeys, partialWithdrawalAmounts, - totalWithdrawalFee, + expectedTotalWithdrawalFee, ); await addWithdrawalRequests( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, totalWithdrawalFee), + () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), pubkeys, mixedWithdrawalAmounts, - totalWithdrawalFee, + expectedTotalWithdrawalFee, ); }); }); diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 0ed3542ddc..d0bf1ab28f 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -288,10 +288,10 @@ describe("WithdrawalVault.sol", () => { }); }); - async function getFee(requestsCount: number): Promise { + async function getFee(): Promise { const fee = await vault.getWithdrawalRequestFee(); - return ethers.parseUnits((fee * BigInt(requestsCount)).toString(), "wei"); + return ethers.parseUnits(fee.toString(), "wei"); } async function getWithdrawalCredentialsContractBalance(): Promise { @@ -328,23 +328,22 @@ describe("WithdrawalVault.sol", () => { await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent - await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys)).to.be.revertedWithCustomError( - vault, - "FeeNotEnough", - ); + await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys)) + .to.be.revertedWithCustomError(vault, "InsufficientTriggerableWithdrawalFee") + .withArgs(0, 3n, 1); // 2. Should revert if fee is less than required const insufficientFee = 2n; - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: insufficientFee }), - ).to.be.revertedWithCustomError(vault, "FeeNotEnough"); + await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: insufficientFee })) + .to.be.revertedWithCustomError(vault, "InsufficientTriggerableWithdrawalFee") + .withArgs(2n, 3n, 1); }); it("Should revert if any pubkey is not 48 bytes", async function () { // Invalid pubkey (only 2 bytes) const pubkeys = ["0x1234"]; - const fee = await getFee(pubkeys.length); + const fee = await getFee(); await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee })) .to.be.revertedWithCustomError(vault, "InvalidPubkeyLength") @@ -353,7 +352,7 @@ describe("WithdrawalVault.sol", () => { it("Should revert if addition fails at the withdrawal request contract", async function () { const { pubkeys } = generateWithdrawalRequestPayload(1); - const fee = await getFee(pubkeys.length); + const fee = await getFee(); // Set mock to fail on add await withdrawalsPredeployed.setFailOnAddRequest(true); @@ -379,15 +378,17 @@ describe("WithdrawalVault.sol", () => { const { pubkeys } = generateWithdrawalRequestPayload(requestCount); await withdrawalsPredeployed.setFee(3n); - const totalWithdrawalFee = 9n; + const expectedTotalWithdrawalFee = 9n; - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); // Check extremely high fee await withdrawalsPredeployed.setFee(ethers.parseEther("10")); - const largeTotalWithdrawalFee = ethers.parseEther("30"); + const expectedLargeTotalWithdrawalFee = ethers.parseEther("30"); - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: largeTotalWithdrawalFee }); + await vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeys, { value: expectedLargeTotalWithdrawalFee }); }); it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { @@ -405,28 +406,40 @@ describe("WithdrawalVault.sol", () => { await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: largeTotalWithdrawalFee }); }); - it("Should correctly deduct the exact fee amount from the contract balance", async function () { + it("Should not affect contract balance", async function () { const requestCount = 3; const { pubkeys } = generateWithdrawalRequestPayload(requestCount); await withdrawalsPredeployed.setFee(3n); - const fee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei + const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei const initialBalance = await getWithdrawalCredentialsContractBalance(); - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }); + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + + const excessTotalWithdrawalFee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: excessTotalWithdrawalFee }); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); }); + // ToDo: should return back the excess fee + it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; const { pubkeys } = generateWithdrawalRequestPayload(requestCount); await withdrawalsPredeployed.setFee(3n); - const totalWithdrawalFee = 9n + 1n; + const expectedTotalWithdrawalFee = 9n; + const excessTotalWithdrawalFee = 9n + 1n; - const initialBalance = await getWithdrawalsPredeployedContractBalance(); - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + totalWithdrawalFee); + let initialBalance = await getWithdrawalsPredeployedContractBalance(); + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); + + initialBalance = await getWithdrawalsPredeployedContractBalance(); + await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: excessTotalWithdrawalFee }); + // Only the expected fee should be transferred + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); }); it("Should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { @@ -447,12 +460,13 @@ describe("WithdrawalVault.sol", () => { }); it("Should verify correct fee distribution among requests", async function () { - await withdrawalsPredeployed.setFee(2n); + const withdrawalFee = 2n; + await withdrawalsPredeployed.setFee(withdrawalFee); const requestCount = 5; const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - const testFeeDistribution = async (totalWithdrawalFee: bigint, expectedFeePerRequest: bigint[]) => { + const testFeeDistribution = async (totalWithdrawalFee: bigint) => { const tx = await vault .connect(validatorsExitBus) .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); @@ -462,14 +476,13 @@ describe("WithdrawalVault.sol", () => { expect(events.length).to.equal(requestCount); for (let i = 0; i < requestCount; i++) { - expect(events[i].args[1]).to.equal(expectedFeePerRequest[i]); + expect(events[i].args[1]).to.equal(withdrawalFee); } }; - await testFeeDistribution(10n, [2n, 2n, 2n, 2n, 2n]); - await testFeeDistribution(11n, [2n, 2n, 2n, 2n, 3n]); - await testFeeDistribution(14n, [2n, 2n, 2n, 2n, 6n]); - await testFeeDistribution(15n, [3n, 3n, 3n, 3n, 3n]); + await testFeeDistribution(10n); + await testFeeDistribution(11n); + await testFeeDistribution(14n); }); it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { @@ -499,27 +512,28 @@ describe("WithdrawalVault.sol", () => { }); const testCasesForWithdrawalRequests = [ - { requestCount: 1, extraFee: 0n }, - { requestCount: 1, extraFee: 100n }, - { requestCount: 1, extraFee: 100_000_000_000n }, - { requestCount: 3, extraFee: 0n }, - { requestCount: 3, extraFee: 1n }, - { requestCount: 7, extraFee: 3n }, - { requestCount: 10, extraFee: 0n }, - { requestCount: 10, extraFee: 100_000_000_000n }, - { requestCount: 100, extraFee: 0n }, + { requestCount: 1, fee: 0n }, + { requestCount: 1, fee: 100n }, + { requestCount: 1, fee: 100_000_000_000n }, + { requestCount: 3, fee: 0n }, + { requestCount: 3, fee: 1n }, + { requestCount: 7, fee: 3n }, + { requestCount: 10, fee: 0n }, + { requestCount: 10, fee: 100_000_000_000n }, + { requestCount: 100, fee: 0n }, ]; - testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { - it(`Should successfully add ${requestCount} requests with extra fee ${extraFee} and emit events`, async () => { + testCasesForWithdrawalRequests.forEach(({ requestCount, fee }) => { + it(`Should successfully add ${requestCount} requests with extra fee ${fee} and emit events`, async () => { const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - const totalWithdrawalFee = (await getFee(pubkeys.length)) + extraFee; + const requestFee = fee == 0n ? await getFee() : fee; + const expectedTotalWithdrawalFee = requestFee * BigInt(requestCount); const initialBalance = await getWithdrawalCredentialsContractBalance(); const tx = await vault .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + .addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); From 96962ecaa6e894e88d34c60b0158413591dc24ba Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 19 Jan 2025 22:41:31 +0400 Subject: [PATCH 018/405] fix: tests after changing ReportData --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 + .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 4 + ...ator-exit-bus-oracle.accessControl.test.ts | 45 ++++++---- .../validator-exit-bus-oracle.deploy.test.ts | 10 ++- .../validator-exit-bus-oracle.gas.test.ts | 42 +++++---- ...r-exit-bus-oracle.submitReportData.test.ts | 86 +++++++++++-------- 6 files changed, 120 insertions(+), 69 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 8a722aa118..bc3d49d547 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -41,6 +41,8 @@ contract ValidatorsExitBus is AccessControlEnumerable { uint256 blockNumber; /// @dev Key index in exit request array uint256 lastDeliveredKeyIndex; + + // TODO: timestamp } // TODO: make type optimization struct RequestStatus { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index bc65df9349..b6f15cb776 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -418,6 +418,10 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus } function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { + if (report.exitRequestData.requestsCount == 0) { + return; + } + mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); RequestStatus storage request = hashes[exitRequestHash]; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index 53c0e1e297..8498ac4c21 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -22,12 +22,12 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; let originalState: string; + let withdrawalVault: WithdrawalVault__MockForVebo; let initTx: ContractTransactionResponse; let oracleVersion: bigint; let exitRequests: ExitRequest[]; let reportFields: ReportFields; - let reportItems: ReturnType; let reportHash: string; let member1: HardhatEthersSigner; @@ -42,24 +42,31 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { valIndex: number; valPubkey: string; } + interface ExitRequestData { + requestsCount: number; + dataFormat: number; + data: string; + } interface ReportFields { consensusVersion: bigint; refSlot: bigint; - requestsCount: number; - dataFormat: number; - data: string; + exitRequestData: ExitRequestData; } - const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { + const exitRequestItems = [ + items.exitRequestData.requestsCount, + items.exitRequestData.dataFormat, + items.exitRequestData.data, + ]; + const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); + const dataHash = ethers.keccak256(exitRequestData); + const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); return ethers.keccak256(data); }; - const getValidatorsExitBusReportDataItems = (r: ReportFields) => { - return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; - }; - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { const pubkeyHex = de0x(valPubkey); expect(pubkeyHex.length).to.equal(48 * 2); @@ -74,8 +81,9 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; + withdrawalVault = deployed.withdrawalVault; - initTx = await initVEBO({ admin: admin.address, oracle, consensus, resumeAfterDeploy: true }); + initTx = await initVEBO({ admin: admin.address, oracle, consensus, withdrawalVault, resumeAfterDeploy: true }); oracleVersion = await oracle.getContractVersion(); @@ -92,14 +100,16 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, - dataFormat: DATA_FORMAT_LIST, refSlot: refSlot, - requestsCount: exitRequests.length, - data: encodeExitRequestsDataList(exitRequests), + exitRequestData: { + dataFormat: DATA_FORMAT_LIST, + requestsCount: exitRequests.length, + data: encodeExitRequestsDataList(exitRequests), + }, }; - reportItems = getValidatorsExitBusReportDataItems(reportFields); - reportHash = calcValidatorsExitBusReportDataHash(reportItems); + // reportItems = getValidatorsExitBusReportDataItems(reportFields); + reportHash = calcValidatorsExitBusReportDataHash(reportFields); await consensus.connect(member1).submitReport(refSlot, reportHash, CONSENSUS_VERSION); await consensus.connect(member3).submitReport(refSlot, reportHash, CONSENSUS_VERSION); @@ -123,7 +133,6 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { expect(oracleVersion).to.be.not.null; expect(exitRequests).to.be.not.null; expect(reportFields).to.be.not.null; - expect(reportItems).to.be.not.null; expect(reportHash).to.be.not.null; }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts index 48dee32afc..e8691852ea 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts @@ -4,7 +4,12 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, ValidatorsExitBusOracle } from "typechain-types"; +import { + HashConsensus__Harness, + ValidatorsExitBus__Harness, + ValidatorsExitBusOracle, + WithdrawalVault__MockForVebo, +} from "typechain-types"; import { CONSENSUS_VERSION, SECONDS_PER_SLOT } from "lib"; @@ -38,12 +43,15 @@ describe("ValidatorsExitBusOracle.sol:deploy", () => { context("deployment and init finishes successfully (default setup)", async () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; + let withdrawalVault: WithdrawalVault__MockForVebo; before(async () => { const deployed = await deployVEBO(admin.address); + withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle: deployed.oracle, + withdrawalVault, consensus: deployed.consensus, }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index 44a7080bab..aa13f46881 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; import { trace } from "lib"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -31,6 +31,7 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; + let withdrawalVault: WithdrawalVault__MockForVebo; let oracleVersion: bigint; @@ -49,24 +50,31 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { valIndex: number; valPubkey: string; } + interface ExitRequestData { + requestsCount: number; + dataFormat: number; + data: string; + } interface ReportFields { consensusVersion: bigint; refSlot: bigint; - requestsCount: number; - dataFormat: number; - data: string; + exitRequestData: ExitRequestData; } - const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { + const exitRequestItems = [ + items.exitRequestData.requestsCount, + items.exitRequestData.dataFormat, + items.exitRequestData.data, + ]; + const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); + const dataHash = ethers.keccak256(exitRequestData); + const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); return ethers.keccak256(data); }; - const getValidatorsExitBusReportDataItems = (r: ReportFields) => { - return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; - }; - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { const pubkeyHex = de0x(valPubkey); expect(pubkeyHex.length).to.equal(48 * 2); @@ -81,10 +89,12 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; + withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, + withdrawalVault, consensus, resumeAfterDeploy: true, }); @@ -142,7 +152,6 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { context(`Total requests: ${totalRequests}`, () => { let exitRequests: { requests: ExitRequest[]; requestsPerModule: number; requestsPerNodeOp: number }; let reportFields: ReportFields; - let reportItems: ReturnType; let reportHash: string; let originalState: string; @@ -171,13 +180,14 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, refSlot: refSlot, - requestsCount: exitRequests.requests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests.requests), + exitRequestData: { + requestsCount: exitRequests.requests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests.requests), + }, }; - reportItems = getValidatorsExitBusReportDataItems(reportFields); - reportHash = calcValidatorsExitBusReportDataHash(reportItems); + reportHash = calcValidatorsExitBusReportDataHash(reportFields); await triggerConsensusOnHash(reportHash); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 5a79f8ab1d..53d45f301e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -4,7 +4,12 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, OracleReportSanityChecker, ValidatorsExitBus__Harness } from "typechain-types"; +import { + HashConsensus__Harness, + OracleReportSanityChecker, + ValidatorsExitBus__Harness, + WithdrawalVault__MockForVebo, +} from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -25,6 +30,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; let oracleReportSanityChecker: OracleReportSanityChecker; + let withdrawalVault: WithdrawalVault__MockForVebo; let oracleVersion: bigint; @@ -41,24 +47,31 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { valIndex: number; valPubkey: string; } + interface ExitRequestData { + requestsCount: number; + dataFormat: number; + data: string; + } interface ReportFields { consensusVersion: bigint; refSlot: bigint; - requestsCount: number; - dataFormat: number; - data: string; + exitRequestData: ExitRequestData; } - const calcValidatorsExitBusReportDataHash = (items: ReturnType) => { - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [items]); + const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { + const exitRequestItems = [ + items.exitRequestData.requestsCount, + items.exitRequestData.dataFormat, + items.exitRequestData.data, + ]; + const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); + const dataHash = ethers.keccak256(exitRequestData); + const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; + const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); return ethers.keccak256(data); }; - const getValidatorsExitBusReportDataItems = (r: ReportFields) => { - return [r.consensusVersion, r.refSlot, r.requestsCount, r.dataFormat, r.data]; - }; - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { const pubkeyHex = de0x(valPubkey); expect(pubkeyHex.length).to.equal(48 * 2); @@ -78,25 +91,27 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const prepareReportAndSubmitHash = async ( requests = [{ moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }], - options = { reportFields: {} }, + options?: Partial> & { + exitRequestData?: Partial; + }, ) => { const { refSlot } = await consensus.getCurrentFrame(); - const reportData = { - consensusVersion: CONSENSUS_VERSION, - dataFormat: DATA_FORMAT_LIST, - refSlot, - requestsCount: requests.length, - data: encodeExitRequestsDataList(requests), - ...options.reportFields, + consensusVersion: options?.consensusVersion || CONSENSUS_VERSION, + refSlot: options?.refSlot || refSlot, + exitRequestData: { + requestsCount: requests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(requests), + ...options?.exitRequestData, + }, }; - const reportItems = getValidatorsExitBusReportDataItems(reportData); - const reportHash = calcValidatorsExitBusReportDataHash(reportItems); + const reportHash = calcValidatorsExitBusReportDataHash(reportData); await triggerConsensusOnHash(reportHash); - return { reportData, reportHash, reportItems }; + return { reportData, reportHash }; }; async function getLastRequestedValidatorIndex(moduleId: number, nodeOpId: number) { @@ -108,11 +123,13 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { oracle = deployed.oracle; consensus = deployed.consensus; oracleReportSanityChecker = deployed.oracleReportSanityChecker; + withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, consensus, + withdrawalVault, resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); @@ -182,7 +199,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const dataFormatUnsupported = 0; const { reportData } = await prepareReportAndSubmitHash( [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], - { reportFields: { dataFormat: dataFormatUnsupported } }, + { exitRequestData: { dataFormat: dataFormatUnsupported } }, ); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) @@ -194,7 +211,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const dataFormatUnsupported = 2; const { reportData } = await prepareReportAndSubmitHash( [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], - { reportFields: { dataFormat: dataFormatUnsupported } }, + { exitRequestData: { dataFormat: dataFormatUnsupported } }, ); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) @@ -215,7 +232,8 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const { refSlot } = await consensus.getCurrentFrame(); const exitRequests = [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }]; const { reportData } = await prepareReportAndSubmitHash(exitRequests, { - reportFields: { data: encodeExitRequestsDataList(exitRequests) + "aaaaaaaaaaaaaaaaaa", refSlot }, + exitRequestData: { data: encodeExitRequestsDataList(exitRequests) + "aaaaaaaaaaaaaaaaaa" }, + refSlot, }); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( @@ -230,10 +248,10 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const data = encodeExitRequestsDataList(exitRequests); const { reportData } = await prepareReportAndSubmitHash(exitRequests, { - reportFields: { + exitRequestData: { data: data.slice(0, data.length - 18), - refSlot, }, + refSlot, }); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( @@ -283,7 +301,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { it("reverts if requestsCount does not match with encoded data size", async () => { const { reportData } = await prepareReportAndSubmitHash( [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], - { reportFields: { requestsCount: 2 } }, + { exitRequestData: { requestsCount: 2 } }, ); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( @@ -375,6 +393,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const requestsStep3: ExitRequest[] = []; const { reportData: reportStep3 } = await prepareReportAndSubmitHash(requestsStep3); await oracle.connect(member1).submitReportData(reportStep3, oracleVersion); + const countStep3 = await oracle.getTotalRequestsProcessed(); currentCount += requestsStep3.length; expect(countStep3).to.equal(currentCount); @@ -493,7 +512,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { it("reverts on stranger", async () => { const { reportData } = await prepareReportAndSubmitHash(); - await expect(oracle.connect(stranger).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( oracle, "SenderNotAllowed", @@ -519,7 +537,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const PAUSE_INFINITELY = await oracle.PAUSE_INFINITELY(); await oracle.pauseFor(PAUSE_INFINITELY, { from: admin }); const { reportData } = await prepareReportAndSubmitHash(); - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( oracle, "ResumedExpected", @@ -554,14 +571,15 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { // change pubkey const reportData = { consensusVersion: CONSENSUS_VERSION, - dataFormat: DATA_FORMAT_LIST, refSlot, - requestsCount: newRequests.length, - data: encodeExitRequestsDataList(newRequests), + exitRequestData: { + requestsCount: requests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(newRequests), + }, }; - const reportItems = getValidatorsExitBusReportDataItems(reportData); - const changedReportHash = calcValidatorsExitBusReportDataHash(reportItems); + const changedReportHash = calcValidatorsExitBusReportDataHash(reportData); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) .to.be.revertedWithCustomError(oracle, "UnexpectedDataHash") From 66ccbcfc7067e1ec43b31c41ce3b90a2060471b6 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 20 Jan 2025 18:01:44 +0100 Subject: [PATCH 019/405] feat: tightly pack pubkeys pass pubkeys as array of bytes --- contracts/0.8.9/WithdrawalVault.sol | 18 +- .../0.8.9/lib/TriggerableWithdrawals.sol | 125 +++-- .../TriggerableWithdrawals_Harness.sol | 6 +- .../WithdrawalsPredeployed_Mock.sol | 4 +- .../lib/triggerableWithdrawals/eip7002Mock.ts | 41 ++ .../lib/triggerableWithdrawals/findEvents.ts | 23 - .../triggerableWithdrawals.test.ts | 459 ++++++++++-------- .../0.8.9/lib/triggerableWithdrawals/utils.ts | 12 +- test/0.8.9/withdrawalVault.test.ts | 269 +++++----- 9 files changed, 536 insertions(+), 421 deletions(-) create mode 100644 test/0.8.9/lib/triggerableWithdrawals/eip7002Mock.ts delete mode 100644 test/0.8.9/lib/triggerableWithdrawals/findEvents.ts diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index f9f060e544..f1f02a2b02 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -51,8 +51,8 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { error NotLido(); error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); - error InsufficientTriggerableWithdrawalFee(uint256 providedTotalFee, uint256 requiredTotalFee, uint256 requestCount); + error TriggerableWithdrawalRefundFailed(); /** * @param _lido the Lido token (stETH) address @@ -144,22 +144,30 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { * @param pubkeys An array of public keys for the validators requesting full withdrawals. */ function addFullWithdrawalRequests( - bytes[] calldata pubkeys + bytes calldata pubkeys ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) { uint256 prevBalance = address(this).balance - msg.value; uint256 minFeePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); - uint256 totalFee = pubkeys.length * minFeePerRequest; + uint256 totalFee = pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH * minFeePerRequest; if(totalFee > msg.value) { - revert InsufficientTriggerableWithdrawalFee(msg.value, totalFee, pubkeys.length); + revert InsufficientTriggerableWithdrawalFee( + msg.value, + totalFee, + pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH + ); } TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, minFeePerRequest); uint256 refund = msg.value - totalFee; if (refund > 0) { - msg.sender.call{value: refund}(""); + (bool success, ) = msg.sender.call{value: refund}(""); + + if (!success) { + revert TriggerableWithdrawalRefundFailed(); + } } assert(address(this).balance == prevBalance); diff --git a/contracts/0.8.9/lib/TriggerableWithdrawals.sol b/contracts/0.8.9/lib/TriggerableWithdrawals.sol index ff3bd43b4d..a601a59304 100644 --- a/contracts/0.8.9/lib/TriggerableWithdrawals.sol +++ b/contracts/0.8.9/lib/TriggerableWithdrawals.sol @@ -2,21 +2,21 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; - library TriggerableWithdrawals { address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; + uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; + uint256 internal constant PUBLIC_KEY_LENGTH = 48; + uint256 internal constant WITHDRAWAL_AMOUNT_LENGTH = 8; error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); error InsufficientBalance(uint256 balance, uint256 totalWithdrawalFee); error InsufficientRequestFee(uint256 feePerRequest, uint256 minFeePerRequest); error WithdrawalRequestFeeReadFailed(); - error InvalidPubkeyLength(bytes pubkey); - error WithdrawalRequestAdditionFailed(bytes pubkey, uint256 amount); + error WithdrawalRequestAdditionFailed(bytes callData); error NoWithdrawalRequests(); - error PartialWithdrawalRequired(bytes pubkey); - - event WithdrawalRequestAdded(bytes pubkey, uint256 amount); + error PartialWithdrawalRequired(uint256 index); + error InvalidPublicKeyLength(); /** * @dev Adds full withdrawal requests for the provided public keys. @@ -24,11 +24,23 @@ library TriggerableWithdrawals { * @param pubkeys An array of public keys for the validators requesting full withdrawals. */ function addFullWithdrawalRequests( - bytes[] calldata pubkeys, + bytes calldata pubkeys, uint256 feePerRequest ) internal { - uint64[] memory amounts = new uint64[](pubkeys.length); - _addWithdrawalRequests(pubkeys, amounts, feePerRequest); + uint256 keysCount = _validateAndCountPubkeys(pubkeys); + feePerRequest = _validateAndAdjustFee(feePerRequest, keysCount); + + bytes memory callData = new bytes(56); + + for (uint256 i = 0; i < keysCount; i++) { + _copyPubkeyToMemory(pubkeys, callData, i); + + (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(callData); + + if (!success) { + revert WithdrawalRequestAdditionFailed(callData); + } + } } /** @@ -41,22 +53,20 @@ library TriggerableWithdrawals { * @param amounts An array of corresponding withdrawal amounts for each public key. */ function addPartialWithdrawalRequests( - bytes[] calldata pubkeys, + bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest ) internal { - _requireArrayLengthsMatch(pubkeys, amounts); - for (uint256 i = 0; i < amounts.length; i++) { if (amounts[i] == 0) { - revert PartialWithdrawalRequired(pubkeys[i]); + revert PartialWithdrawalRequired(i); } } - _addWithdrawalRequests(pubkeys, amounts, feePerRequest); + addWithdrawalRequests(pubkeys, amounts, feePerRequest); } - /** + /** * @dev Adds partial or full withdrawal requests for the provided public keys with corresponding amounts. * A partial withdrawal is any withdrawal where the amount is greater than zero. * This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn). @@ -65,12 +75,29 @@ library TriggerableWithdrawals { * @param amounts An array of corresponding withdrawal amounts for each public key. */ function addWithdrawalRequests( - bytes[] calldata pubkeys, + bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest ) internal { - _requireArrayLengthsMatch(pubkeys, amounts); - _addWithdrawalRequests(pubkeys, amounts, feePerRequest); + uint256 keysCount = _validateAndCountPubkeys(pubkeys); + + if (keysCount != amounts.length) { + revert MismatchedArrayLengths(keysCount, amounts.length); + } + + feePerRequest = _validateAndAdjustFee(feePerRequest, keysCount); + + bytes memory callData = new bytes(56); + for (uint256 i = 0; i < keysCount; i++) { + _copyPubkeyToMemory(pubkeys, callData, i); + _copyAmountToMemory(callData, amounts[i]); + + (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(callData); + + if (!success) { + revert WithdrawalRequestAdditionFailed(callData); + } + } } /** @@ -87,16 +114,36 @@ library TriggerableWithdrawals { return abi.decode(feeData, (uint256)); } - function _addWithdrawalRequests( - bytes[] calldata pubkeys, - uint64[] memory amounts, - uint256 feePerRequest - ) internal { - uint256 keysCount = pubkeys.length; + function _copyPubkeyToMemory(bytes calldata pubkeys, bytes memory target, uint256 keyIndex) private pure { + assembly { + calldatacopy( + add(target, 32), + add(pubkeys.offset, mul(keyIndex, PUBLIC_KEY_LENGTH)), + PUBLIC_KEY_LENGTH + ) + } + } + + function _copyAmountToMemory(bytes memory target, uint64 amount) private pure { + assembly { + mstore(add(target, 80), shl(192, amount)) + } + } + + function _validateAndCountPubkeys(bytes calldata pubkeys) private pure returns (uint256) { + if(pubkeys.length % PUBLIC_KEY_LENGTH != 0) { + revert InvalidPublicKeyLength(); + } + + uint256 keysCount = pubkeys.length / PUBLIC_KEY_LENGTH; if (keysCount == 0) { revert NoWithdrawalRequests(); } + return keysCount; + } + + function _validateAndAdjustFee(uint256 feePerRequest, uint256 keysCount) private view returns (uint256) { uint256 minFeePerRequest = getWithdrawalRequestFee(); if (feePerRequest == 0) { @@ -107,34 +154,10 @@ library TriggerableWithdrawals { revert InsufficientRequestFee(feePerRequest, minFeePerRequest); } - uint256 totalWithdrawalFee = feePerRequest * keysCount; - - if(address(this).balance < totalWithdrawalFee) { - revert InsufficientBalance(address(this).balance, totalWithdrawalFee); + if(address(this).balance < feePerRequest * keysCount) { + revert InsufficientBalance(address(this).balance, feePerRequest * keysCount); } - for (uint256 i = 0; i < keysCount; ++i) { - if(pubkeys[i].length != 48) { - revert InvalidPubkeyLength(pubkeys[i]); - } - - bytes memory callData = abi.encodePacked(pubkeys[i], amounts[i]); - (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(callData); - - if (!success) { - revert WithdrawalRequestAdditionFailed(pubkeys[i], amounts[i]); - } - - emit WithdrawalRequestAdded(pubkeys[i], amounts[i]); - } - } - - function _requireArrayLengthsMatch( - bytes[] calldata pubkeys, - uint64[] calldata amounts - ) internal pure { - if (pubkeys.length != amounts.length) { - revert MismatchedArrayLengths(pubkeys.length, amounts.length); - } + return feePerRequest; } } diff --git a/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol index 82e4b308f5..1ea18a48bf 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol @@ -4,14 +4,14 @@ import {TriggerableWithdrawals} from "contracts/0.8.9/lib/TriggerableWithdrawals contract TriggerableWithdrawals_Harness { function addFullWithdrawalRequests( - bytes[] calldata pubkeys, + bytes calldata pubkeys, uint256 feePerRequest ) external { TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); } function addPartialWithdrawalRequests( - bytes[] calldata pubkeys, + bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest ) external { @@ -19,7 +19,7 @@ contract TriggerableWithdrawals_Harness { } function addWithdrawalRequests( - bytes[] calldata pubkeys, + bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest ) external { diff --git a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol index f4b580b142..25581ff798 100644 --- a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol +++ b/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol @@ -9,7 +9,7 @@ contract WithdrawalsPredeployed_Mock { bool public failOnAddRequest; bool public failOnGetFee; - event eip7002WithdrawalRequestAdded(bytes request, uint256 fee); + event eip7002MockRequestAdded(bytes request, uint256 fee); function setFailOnAddRequest(bool _failOnAddRequest) external { failOnAddRequest = _failOnAddRequest; @@ -36,7 +36,7 @@ contract WithdrawalsPredeployed_Mock { require(input.length == 56, "Invalid callData length"); - emit eip7002WithdrawalRequestAdded( + emit eip7002MockRequestAdded( input, msg.value ); diff --git a/test/0.8.9/lib/triggerableWithdrawals/eip7002Mock.ts b/test/0.8.9/lib/triggerableWithdrawals/eip7002Mock.ts new file mode 100644 index 0000000000..5fd83ae17d --- /dev/null +++ b/test/0.8.9/lib/triggerableWithdrawals/eip7002Mock.ts @@ -0,0 +1,41 @@ +import { expect } from "chai"; +import { ContractTransactionReceipt } from "ethers"; +import { ContractTransactionResponse } from "ethers"; +import { ethers } from "hardhat"; + +import { findEventsWithInterfaces } from "lib"; + +const eip7002MockEventABI = ["event eip7002MockRequestAdded(bytes request, uint256 fee)"]; +const eip7002MockInterface = new ethers.Interface(eip7002MockEventABI); +type Eip7002MockTriggerableWithdrawalEvents = "eip7002MockRequestAdded"; + +export function findEip7002MockEvents( + receipt: ContractTransactionReceipt, + event: Eip7002MockTriggerableWithdrawalEvents, +) { + return findEventsWithInterfaces(receipt!, event, [eip7002MockInterface]); +} + +export function encodeEip7002Payload(pubkey: string, amount: bigint): string { + return `0x${pubkey}${amount.toString(16).padStart(16, "0")}`; +} + +export const testEip7002Mock = async ( + addTriggeranleWithdrawalRequests: () => Promise, + expectedPubkeys: string[], + expectedAmounts: bigint[], + expectedFee: bigint, +) => { + const tx = await addTriggeranleWithdrawalRequests(); + const receipt = await tx.wait(); + + const events = findEip7002MockEvents(receipt!, "eip7002MockRequestAdded"); + expect(events.length).to.equal(expectedPubkeys.length); + + for (let i = 0; i < expectedPubkeys.length; i++) { + expect(events[i].args[0]).to.equal(encodeEip7002Payload(expectedPubkeys[i], expectedAmounts[i])); + expect(events[i].args[1]).to.equal(expectedFee); + } + + return { tx, receipt }; +}; diff --git a/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts b/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts deleted file mode 100644 index 82047e8c16..0000000000 --- a/test/0.8.9/lib/triggerableWithdrawals/findEvents.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ContractTransactionReceipt } from "ethers"; -import { ethers } from "hardhat"; - -import { findEventsWithInterfaces } from "lib"; - -const withdrawalRequestEventABI = ["event WithdrawalRequestAdded(bytes pubkey, uint256 amount)"]; -const withdrawalRequestEventInterface = new ethers.Interface(withdrawalRequestEventABI); -type WithdrawalRequestEvents = "WithdrawalRequestAdded"; - -export function findEvents(receipt: ContractTransactionReceipt, event: WithdrawalRequestEvents) { - return findEventsWithInterfaces(receipt!, event, [withdrawalRequestEventInterface]); -} - -const eip7002TriggerableWithdrawalMockEventABI = ["event eip7002WithdrawalRequestAdded(bytes request, uint256 fee)"]; -const eip7002TriggerableWithdrawalMockInterface = new ethers.Interface(eip7002TriggerableWithdrawalMockEventABI); -type Eip7002WithdrawalEvents = "eip7002WithdrawalRequestAdded"; - -export function findEip7002TriggerableWithdrawalMockEvents( - receipt: ContractTransactionReceipt, - event: Eip7002WithdrawalEvents, -) { - return findEventsWithInterfaces(receipt!, event, [eip7002TriggerableWithdrawalMockInterface]); -} diff --git a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index af1325180a..5600a7e279 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -9,13 +9,15 @@ import { TriggerableWithdrawals_Harness, WithdrawalsPredeployed_Mock } from "typ import { Snapshot } from "test/suite"; -import { findEip7002TriggerableWithdrawalMockEvents, findEvents } from "./findEvents"; +import { findEip7002MockEvents, testEip7002Mock } from "./eip7002Mock"; import { deployWithdrawalsPredeployedMock, generateWithdrawalRequestPayload, withdrawalsPredeployedHardcodedAddress, } from "./utils"; +const EMPTY_PUBKEYS = "0x"; + describe("TriggerableWithdrawals.sol", () => { let actor: HardhatEthersSigner; @@ -83,96 +85,111 @@ describe("TriggerableWithdrawals.sol", () => { context("add triggerable withdrawal requests", () => { it("Should revert if empty arrays are provided", async function () { - await expect(triggerableWithdrawals.addFullWithdrawalRequests([], 1n)).to.be.revertedWithCustomError( + await expect(triggerableWithdrawals.addFullWithdrawalRequests(EMPTY_PUBKEYS, 1n)).to.be.revertedWithCustomError( triggerableWithdrawals, "NoWithdrawalRequests", ); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "NoWithdrawalRequests", - ); + await expect( + triggerableWithdrawals.addPartialWithdrawalRequests(EMPTY_PUBKEYS, [], 1n), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "NoWithdrawalRequests"); - await expect(triggerableWithdrawals.addWithdrawalRequests([], [], 1n)).to.be.revertedWithCustomError( + await expect(triggerableWithdrawals.addWithdrawalRequests(EMPTY_PUBKEYS, [], 1n)).to.be.revertedWithCustomError( triggerableWithdrawals, "NoWithdrawalRequests", ); }); it("Should revert if array lengths do not match", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(2); + const requestCount = 2; + const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); const amounts = [1n]; const fee = await getFee(); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee)) + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(pubkeys.length, amounts.length); + .withArgs(requestCount, amounts.length); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, [], fee)) + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, [], fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(pubkeys.length, 0); + .withArgs(requestCount, 0); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests([], amounts, fee)) + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(0, amounts.length); + .withArgs(requestCount, amounts.length); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)) + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, [], fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(pubkeys.length, amounts.length); - - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, [], fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(pubkeys.length, 0); - - await expect(triggerableWithdrawals.addWithdrawalRequests([], amounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(0, amounts.length); + .withArgs(requestCount, 0); }); it("Should revert if not enough fee is sent", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString } = generateWithdrawalRequestPayload(1); const amounts = [10n]; await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei // 2. Should revert if fee is less than required const insufficientFee = 2n; - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, insufficientFee)) + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, insufficientFee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") .withArgs(2n, 3n); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, insufficientFee)) + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, insufficientFee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") .withArgs(2n, 3n); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, insufficientFee)) + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, insufficientFee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") .withArgs(2n, 3n); }); - it("Should revert if any pubkey is not 48 bytes", async function () { + it("Should revert if pubkey is not 48 bytes", async function () { // Invalid pubkey (only 2 bytes) - const pubkeys = ["0x1234"]; + const invalidPubkeyHexString = "0x1234"; + const amounts = [10n]; + + const fee = await getFee(); + + await expect( + triggerableWithdrawals.addFullWithdrawalRequests(invalidPubkeyHexString, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + + await expect( + triggerableWithdrawals.addPartialWithdrawalRequests(invalidPubkeyHexString, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + + await expect( + triggerableWithdrawals.addWithdrawalRequests(invalidPubkeyHexString, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + }); + + it("Should revert if last pubkey not 48 bytes", async function () { + const validPubey = + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; + const invalidPubkey = "1234"; + const pubkeysHexString = `0x${validPubey}${invalidPubkey}`; + const amounts = [10n]; const fee = await getFee(); - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); + await expect( + triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); + await expect( + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); + await expect( + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); }); it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString } = generateWithdrawalRequestPayload(1); const amounts = [10n]; const fee = await getFee(); @@ -180,28 +197,26 @@ describe("TriggerableWithdrawals.sol", () => { // Set mock to fail on add await withdrawalsPredeployed.setFailOnAddRequest(true); - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "WithdrawalRequestAdditionFailed", - ); + await expect( + triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, fee)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "WithdrawalRequestAdditionFailed", - ); + await expect( + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); }); it("Should revert when a full withdrawal amount is included in 'addPartialWithdrawalRequests'", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(2); + const { pubkeysHexString } = generateWithdrawalRequestPayload(2); const amounts = [1n, 0n]; // Partial and Full withdrawal const fee = await getFee(); await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, fee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), ).to.be.revertedWithCustomError(triggerableWithdrawals, "PartialWithdrawalRequired"); }); @@ -211,20 +226,21 @@ describe("TriggerableWithdrawals.sol", () => { const balance = 19n; const expectedMinimalBalance = 20n; - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(keysCount); + const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(keysCount); await withdrawalsPredeployed.setFee(fee); await setBalance(await triggerableWithdrawals.getAddress(), balance); - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)) + await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, expectedMinimalBalance); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee)) + await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, expectedMinimalBalance); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)) + await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee)) .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") .withArgs(balance, expectedMinimalBalance); }); @@ -232,36 +248,87 @@ describe("TriggerableWithdrawals.sol", () => { it("Should revert when fee read fails", async function () { await withdrawalsPredeployed.setFailOnGetFee(true); - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(2); const fee = 10n; - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "WithdrawalRequestFeeReadFailed", - ); + await expect( + triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); }); - // ToDo: should accept when fee not defined + it("Should accept withdrawal requests with minimal possible fee when fee not provided", async function () { + const requestCount = 3; + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + const fee_not_provided = 0n; + await withdrawalsPredeployed.setFee(fee); + + await testEip7002Mock( + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee_not_provided), + pubkeys, + fullWithdrawalAmounts, + fee, + ); + + await testEip7002Mock( + () => + triggerableWithdrawals.addPartialWithdrawalRequests( + pubkeysHexString, + partialWithdrawalAmounts, + fee_not_provided, + ), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + + await testEip7002Mock( + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee_not_provided), + pubkeys, + mixedWithdrawalAmounts, + fee, + ); + }); it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(3n); + await withdrawalsPredeployed.setFee(fee); - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await testEip7002Mock( + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), + pubkeys, + fullWithdrawalAmounts, + fee, + ); + + await testEip7002Mock( + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + + await testEip7002Mock( + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), + pubkeys, + mixedWithdrawalAmounts, + fee, + ); // Check extremely high fee const highFee = ethers.parseEther("10"); @@ -269,35 +336,92 @@ describe("TriggerableWithdrawals.sol", () => { await triggerableWithdrawals.connect(actor).deposit({ value: highFee * BigInt(requestCount) * 3n }); - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, highFee); - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, highFee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, highFee); + await testEip7002Mock( + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, highFee), + pubkeys, + fullWithdrawalAmounts, + highFee, + ); + + await testEip7002Mock( + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, highFee), + pubkeys, + partialWithdrawalAmounts, + highFee, + ); + + await testEip7002Mock( + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, highFee), + pubkeys, + mixedWithdrawalAmounts, + highFee, + ); }); it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { const requestCount = 3; - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); await withdrawalsPredeployed.setFee(3n); - const fee = 4n; + const excessFee = 4n; + + await testEip7002Mock( + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, excessFee), + pubkeys, + fullWithdrawalAmounts, + excessFee, + ); - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee); - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await testEip7002Mock( + () => + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, excessFee), + pubkeys, + partialWithdrawalAmounts, + excessFee, + ); + + await testEip7002Mock( + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, excessFee), + pubkeys, + mixedWithdrawalAmounts, + excessFee, + ); // Check when the provided fee extremely exceeds the required amount - const largeFee = ethers.parseEther("10"); - await triggerableWithdrawals.connect(actor).deposit({ value: largeFee * BigInt(requestCount) * 3n }); + const extremelyHighFee = ethers.parseEther("10"); + await triggerableWithdrawals.connect(actor).deposit({ value: extremelyHighFee * BigInt(requestCount) * 3n }); + + await testEip7002Mock( + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, extremelyHighFee), + pubkeys, + fullWithdrawalAmounts, + extremelyHighFee, + ); + + await testEip7002Mock( + () => + triggerableWithdrawals.addPartialWithdrawalRequests( + pubkeysHexString, + partialWithdrawalAmounts, + extremelyHighFee, + ), + pubkeys, + partialWithdrawalAmounts, + extremelyHighFee, + ); - await triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, largeFee); - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, largeFee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, largeFee); + await testEip7002Mock( + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, extremelyHighFee), + pubkeys, + mixedWithdrawalAmounts, + extremelyHighFee, + ); }); it("Should correctly deduct the exact fee amount from the contract balance", async function () { const requestCount = 3; - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 4n; @@ -309,16 +433,18 @@ describe("TriggerableWithdrawals.sol", () => { expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); }; - await testFeeDeduction(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); + await testFeeDeduction(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)); + await testFeeDeduction(() => + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), + ); await testFeeDeduction(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), ); - await testFeeDeduction(() => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); }); it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; @@ -330,112 +456,39 @@ describe("TriggerableWithdrawals.sol", () => { expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); }; - await testFeeTransfer(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); + await testFeeTransfer(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)); await testFeeTransfer(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), + ); + await testFeeTransfer(() => + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), ); - await testFeeTransfer(() => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee)); }); it("Should accept full, partial, and mixed withdrawal requests via 'addWithdrawalRequests' function", async function () { - const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + const { pubkeysHexString, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(3); const fee = await getFee(); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, fullWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, fullWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee); + await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee); }); it("Should handle maximum uint64 withdrawal amount in partial withdrawal requests", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString } = generateWithdrawalRequestPayload(1); const amounts = [MAX_UINT64]; - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, 10n); - await triggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, 10n); - }); - - it("Should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { - const requestCount = 3; - const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - const fee = 10n; - - const testEventsEmit = async ( - addRequests: () => Promise, - expectedPubKeys: string[], - expectedAmounts: bigint[], - ) => { - const tx = await addRequests(); - - const receipt = await tx.wait(); - const events = findEvents(receipt!, "WithdrawalRequestAdded"); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[0]).to.equal(expectedPubKeys[i]); - expect(events[i].args[1]).to.equal(expectedAmounts[i]); - } - }; - - await testEventsEmit( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee), - pubkeys, - fullWithdrawalAmounts, - ); - await testEventsEmit( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), - pubkeys, - partialWithdrawalAmounts, - ); - await testEventsEmit( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), - pubkeys, - mixedWithdrawalAmounts, - ); - }); - - it("Should verify correct fee distribution among requests", async function () { - const requestCount = 5; - const { pubkeys, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const testFeeDistribution = async (fee: bigint) => { - const checkEip7002MockEvents = async (addRequests: () => Promise) => { - const tx = await addRequests(); - - const receipt = await tx.wait(); - const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[1]).to.equal(fee); - } - }; - - await checkEip7002MockEvents(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee)); - - await checkEip7002MockEvents(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), - ); - - await checkEip7002MockEvents(() => - triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), - ); - }; - - await testFeeDistribution(1n); - await testFeeDistribution(2n); - await testFeeDistribution(3n); + await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, 10n); + await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, 10n); }); it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { const requestCount = 16; - const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const fee = 333n; - const normalize = (hex: string) => (hex.startsWith("0x") ? hex.slice(2).toLowerCase() : hex.toLowerCase()); + const fee = 333n; const testEncoding = async ( addRequests: () => Promise, @@ -443,10 +496,9 @@ describe("TriggerableWithdrawals.sol", () => { expectedAmounts: bigint[], ) => { const tx = await addRequests(); - const receipt = await tx.wait(); - const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); + const events = findEip7002MockEvents(receipt!, "eip7002MockRequestAdded"); expect(events.length).to.equal(requestCount); for (let i = 0; i < requestCount; i++) { @@ -454,25 +506,27 @@ describe("TriggerableWithdrawals.sol", () => { // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters expect(encodedRequest.length).to.equal(114); - expect(normalize(encodedRequest.substring(0, 98))).to.equal(normalize(expectedPubKeys[i])); - expect(normalize(encodedRequest.substring(98, 114))).to.equal( - expectedAmounts[i].toString(16).padStart(16, "0"), - ); + expect(encodedRequest.slice(0, 2)).to.equal("0x"); + expect(encodedRequest.slice(2, 98)).to.equal(expectedPubKeys[i]); + expect(encodedRequest.slice(98, 114)).to.equal(expectedAmounts[i].toString(16).padStart(16, "0")); + + // double check the amount convertation + expect(BigInt("0x" + encodedRequest.slice(98, 114))).to.equal(expectedAmounts[i]); } }; await testEncoding( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee), + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), pubkeys, fullWithdrawalAmounts, ); await testEncoding( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), pubkeys, partialWithdrawalAmounts, ); await testEncoding( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), pubkeys, mixedWithdrawalAmounts, ); @@ -482,34 +536,14 @@ describe("TriggerableWithdrawals.sol", () => { addRequests: () => Promise, expectedPubkeys: string[], expectedAmounts: bigint[], + expectedFee: bigint, expectedTotalWithdrawalFee: bigint, ) { const initialBalance = await getWithdrawalCredentialsContractBalance(); - const tx = await addRequests(); + await testEip7002Mock(addRequests, expectedPubkeys, expectedAmounts, expectedFee); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); - - const receipt = await tx.wait(); - - const events = findEvents(receipt!, "WithdrawalRequestAdded"); - expect(events.length).to.equal(expectedPubkeys.length); - - for (let i = 0; i < expectedPubkeys.length; i++) { - expect(events[i].args[0]).to.equal(expectedPubkeys[i]); - expect(events[i].args[1]).to.equal(expectedAmounts[i]); - } - - const eip7002TriggerableWithdrawalMockEvents = findEip7002TriggerableWithdrawalMockEvents( - receipt!, - "eip7002WithdrawalRequestAdded", - ); - expect(eip7002TriggerableWithdrawalMockEvents.length).to.equal(expectedPubkeys.length); - for (let i = 0; i < expectedPubkeys.length; i++) { - expect(eip7002TriggerableWithdrawalMockEvents[i].args[0]).to.equal( - expectedPubkeys[i].concat(expectedAmounts[i].toString(16).padStart(16, "0")), - ); - } } const testCasesForWithdrawalRequests = [ @@ -525,31 +559,34 @@ describe("TriggerableWithdrawals.sol", () => { ]; testCasesForWithdrawalRequests.forEach(({ requestCount, fee }) => { - it(`Should successfully add ${requestCount} requests with fee ${fee} and emit events`, async () => { - const { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + it(`Should successfully add ${requestCount} requests with fee ${fee}`, async () => { + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const requestFee = fee == 0n ? await getFee() : fee; - const expectedTotalWithdrawalFee = requestFee * BigInt(requestCount); + const expectedFee = fee == 0n ? await getFee() : fee; + const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); await addWithdrawalRequests( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeys, fee), + () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), pubkeys, fullWithdrawalAmounts, + expectedFee, expectedTotalWithdrawalFee, ); await addWithdrawalRequests( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, partialWithdrawalAmounts, fee), + () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), pubkeys, partialWithdrawalAmounts, + expectedFee, expectedTotalWithdrawalFee, ); await addWithdrawalRequests( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeys, mixedWithdrawalAmounts, fee), + () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), pubkeys, mixedWithdrawalAmounts, + expectedFee, expectedTotalWithdrawalFee, ); }); diff --git a/test/0.8.9/lib/triggerableWithdrawals/utils.ts b/test/0.8.9/lib/triggerableWithdrawals/utils.ts index 105c23e474..676cd9ac80 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/utils.ts +++ b/test/0.8.9/lib/triggerableWithdrawals/utils.ts @@ -22,10 +22,10 @@ export async function deployWithdrawalsPredeployedMock( function toValidatorPubKey(num: number): string { if (num < 0 || num > 0xffff) { - throw new Error("Number is out of the 2-byte range (0x0000 - 0xFFFF)."); + throw new Error("Number is out of the 2-byte range (0x0000 - 0xffff)."); } - return `0x${num.toString(16).padStart(4, "0").repeat(24)}`; + return `${num.toString(16).padStart(4, "0").toLocaleLowerCase().repeat(24)}`; } const convertEthToGwei = (ethAmount: string | number): bigint => { @@ -47,5 +47,11 @@ export function generateWithdrawalRequestPayload(numberOfRequests: number) { mixedWithdrawalAmounts.push(i % 2 === 0 ? 0n : convertEthToGwei(i)); } - return { pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts }; + return { + pubkeysHexString: `0x${pubkeys.join("")}`, + pubkeys, + fullWithdrawalAmounts, + partialWithdrawalAmounts, + mixedWithdrawalAmounts, + }; } diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index d0bf1ab28f..e4bc64f172 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -17,7 +17,7 @@ import { MAX_UINT256, proxify, streccak } from "lib"; import { Snapshot } from "test/suite"; -import { findEip7002TriggerableWithdrawalMockEvents, findEvents } from "./lib/triggerableWithdrawals/findEvents"; +import { findEip7002MockEvents, testEip7002Mock } from "./lib/triggerableWithdrawals/eip7002Mock"; import { deployWithdrawalsPredeployedMock, generateWithdrawalRequestPayload, @@ -311,114 +311,178 @@ describe("WithdrawalVault.sol", () => { }); it("Should revert if the caller is not Validator Exit Bus", async () => { - await expect( - vault.connect(stranger).addFullWithdrawalRequests(["0x1234"]), - ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_FULL_WITHDRAWAL_REQUEST_ROLE); + await expect(vault.connect(stranger).addFullWithdrawalRequests("0x1234")).to.be.revertedWithOZAccessControlError( + stranger.address, + ADD_FULL_WITHDRAWAL_REQUEST_ROLE, + ); }); it("Should revert if empty arrays are provided", async function () { await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests([], { value: 1n }), + vault.connect(validatorsExitBus).addFullWithdrawalRequests("0x", { value: 1n }), ).to.be.revertedWithCustomError(vault, "NoWithdrawalRequests"); }); it("Should revert if not enough fee is sent", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString } = generateWithdrawalRequestPayload(1); await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent - await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys)) + await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString)) .to.be.revertedWithCustomError(vault, "InsufficientTriggerableWithdrawalFee") .withArgs(0, 3n, 1); // 2. Should revert if fee is less than required const insufficientFee = 2n; - await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: insufficientFee })) + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: insufficientFee }), + ) .to.be.revertedWithCustomError(vault, "InsufficientTriggerableWithdrawalFee") .withArgs(2n, 3n, 1); }); - it("Should revert if any pubkey is not 48 bytes", async function () { + it("Should revert if pubkey is not 48 bytes", async function () { // Invalid pubkey (only 2 bytes) - const pubkeys = ["0x1234"]; + const invalidPubkeyHexString = "0x1234"; const fee = await getFee(); - await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee })) - .to.be.revertedWithCustomError(vault, "InvalidPubkeyLength") - .withArgs(pubkeys[0]); + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(invalidPubkeyHexString, { value: fee }), + ).to.be.revertedWithCustomError(vault, "InvalidPublicKeyLength"); + }); + + it("Should revert if last pubkey not 48 bytes", async function () { + const validPubey = + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; + const invalidPubkey = "1234"; + const pubkeysHexString = `0x${validPubey}${invalidPubkey}`; + + const fee = await getFee(); + + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), + ).to.be.revertedWithCustomError(vault, "InvalidPublicKeyLength"); }); it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeys } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString } = generateWithdrawalRequestPayload(1); const fee = await getFee(); // Set mock to fail on add await withdrawalsPredeployed.setFailOnAddRequest(true); await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }), + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "WithdrawalRequestAdditionFailed"); }); it("Should revert when fee read fails", async function () { await withdrawalsPredeployed.setFailOnGetFee(true); - const { pubkeys } = generateWithdrawalRequestPayload(2); + const { pubkeysHexString } = generateWithdrawalRequestPayload(2); const fee = 10n; await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }), + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "WithdrawalRequestFeeReadFailed"); }); it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const fee = 3n; await withdrawalsPredeployed.setFee(3n); const expectedTotalWithdrawalFee = 9n; - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); + await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); // Check extremely high fee - await withdrawalsPredeployed.setFee(ethers.parseEther("10")); + const highFee = ethers.parseEther("10"); + await withdrawalsPredeployed.setFee(highFee); const expectedLargeTotalWithdrawalFee = ethers.parseEther("30"); - await vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeys, { value: expectedLargeTotalWithdrawalFee }); + await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedLargeTotalWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + highFee, + ); }); it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { const requestCount = 3; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - await withdrawalsPredeployed.setFee(3n); - const fee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei + const fee = 3n; + await withdrawalsPredeployed.setFee(fee); + const withdrawalFee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }); + await testEip7002Mock( + () => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: withdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); // Check when the provided fee extremely exceeds the required amount - const largeTotalWithdrawalFee = ethers.parseEther("10"); - - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: largeTotalWithdrawalFee }); + const largeWithdrawalFee = ethers.parseEther("10"); + + await testEip7002Mock( + () => + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: largeWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); }); it("Should not affect contract balance", async function () { const requestCount = 3; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - await withdrawalsPredeployed.setFee(3n); + const fee = 3n; + await withdrawalsPredeployed.setFee(fee); const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei const initialBalance = await getWithdrawalCredentialsContractBalance(); - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); + + await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); const excessTotalWithdrawalFee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: excessTotalWithdrawalFee }); + + await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: excessTotalWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); }); @@ -426,79 +490,53 @@ describe("WithdrawalVault.sol", () => { it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const fee = 3n; await withdrawalsPredeployed.setFee(3n); const expectedTotalWithdrawalFee = 9n; const excessTotalWithdrawalFee = 9n + 1n; let initialBalance = await getWithdrawalsPredeployedContractBalance(); - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); + + await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); initialBalance = await getWithdrawalsPredeployedContractBalance(); - await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: excessTotalWithdrawalFee }); + await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: excessTotalWithdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); // Only the expected fee should be transferred expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); }); - it("Should emit a 'WithdrawalRequestAdded' event when a new withdrawal request is added", async function () { - const requestCount = 3; - const { pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const fee = 10n; - - const tx = await vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeys, { value: fee }); - - const receipt = await tx.wait(); - const events = findEvents(receipt!, "WithdrawalRequestAdded"); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[0]).to.equal(pubkeys[i]); - expect(events[i].args[1]).to.equal(fullWithdrawalAmounts[i]); - } - }); - - it("Should verify correct fee distribution among requests", async function () { - const withdrawalFee = 2n; - await withdrawalsPredeployed.setFee(withdrawalFee); - - const requestCount = 5; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - - const testFeeDistribution = async (totalWithdrawalFee: bigint) => { - const tx = await vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); - - const receipt = await tx.wait(); - const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - expect(events[i].args[1]).to.equal(withdrawalFee); - } - }; - - await testFeeDistribution(10n); - await testFeeDistribution(11n); - await testFeeDistribution(14n); - }); - it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { const requestCount = 16; - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys } = generateWithdrawalRequestPayload(requestCount); const totalWithdrawalFee = 333n; - const normalize = (hex: string) => (hex.startsWith("0x") ? hex.slice(2).toLowerCase() : hex.toLowerCase()); - const tx = await vault .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeys, { value: totalWithdrawalFee }); + .addFullWithdrawalRequests(pubkeysHexString, { value: totalWithdrawalFee }); const receipt = await tx.wait(); - const events = findEip7002TriggerableWithdrawalMockEvents(receipt!, "eip7002WithdrawalRequestAdded"); + const events = findEip7002MockEvents(receipt!, "eip7002MockRequestAdded"); expect(events.length).to.equal(requestCount); for (let i = 0; i < requestCount; i++) { @@ -506,55 +544,40 @@ describe("WithdrawalVault.sol", () => { // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters expect(encodedRequest.length).to.equal(114); - expect(normalize(encodedRequest.substring(0, 98))).to.equal(normalize(pubkeys[i])); - expect(normalize(encodedRequest.substring(98, 114))).to.equal("0".repeat(16)); + expect(encodedRequest.slice(0, 2)).to.equal("0x"); + expect(encodedRequest.slice(2, 98)).to.equal(pubkeys[i]); + expect(encodedRequest.slice(98, 114)).to.equal("0".repeat(16)); // Amount is 0 } }); const testCasesForWithdrawalRequests = [ - { requestCount: 1, fee: 0n }, - { requestCount: 1, fee: 100n }, - { requestCount: 1, fee: 100_000_000_000n }, - { requestCount: 3, fee: 0n }, - { requestCount: 3, fee: 1n }, - { requestCount: 7, fee: 3n }, - { requestCount: 10, fee: 0n }, - { requestCount: 10, fee: 100_000_000_000n }, - { requestCount: 100, fee: 0n }, + { requestCount: 1, extraFee: 0n }, + { requestCount: 1, extraFee: 100n }, + { requestCount: 1, extraFee: 100_000_000_000n }, + { requestCount: 3, extraFee: 0n }, + { requestCount: 3, extraFee: 1n }, + { requestCount: 7, extraFee: 3n }, + { requestCount: 10, extraFee: 0n }, + { requestCount: 10, extraFee: 100_000_000_000n }, + { requestCount: 100, extraFee: 0n }, ]; - testCasesForWithdrawalRequests.forEach(({ requestCount, fee }) => { - it(`Should successfully add ${requestCount} requests with extra fee ${fee} and emit events`, async () => { - const { pubkeys } = generateWithdrawalRequestPayload(requestCount); - const requestFee = fee == 0n ? await getFee() : fee; - const expectedTotalWithdrawalFee = requestFee * BigInt(requestCount); + testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { + it(`Should successfully add ${requestCount} requests with extra fee ${extraFee}`, async () => { + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + const withdrawalFee = expectedFee * BigInt(requestCount) + extraFee; const initialBalance = await getWithdrawalCredentialsContractBalance(); - const tx = await vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeys, { value: expectedTotalWithdrawalFee }); + await testEip7002Mock( + () => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: withdrawalFee }), + pubkeys, + fullWithdrawalAmounts, + expectedFee, + ); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - - const receipt = await tx.wait(); - - const events = findEvents(receipt!, "WithdrawalRequestAdded"); - expect(events.length).to.equal(pubkeys.length); - - for (let i = 0; i < pubkeys.length; i++) { - expect(events[i].args[0]).to.equal(pubkeys[i]); - expect(events[i].args[1]).to.equal(0); - } - - const eip7002TriggerableWithdrawalMockEvents = findEip7002TriggerableWithdrawalMockEvents( - receipt!, - "eip7002WithdrawalRequestAdded", - ); - expect(eip7002TriggerableWithdrawalMockEvents.length).to.equal(pubkeys.length); - for (let i = 0; i < pubkeys.length; i++) { - expect(eip7002TriggerableWithdrawalMockEvents[i].args[0]).to.equal(pubkeys[i].concat("0".repeat(16))); - } }); }); }); From 0f37e515cb118dc14f1a6499411b341be1d4b98d Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 27 Jan 2025 11:23:41 +0100 Subject: [PATCH 020/405] refactor: format code --- contracts/0.8.9/WithdrawalVault.sol | 12 +++++++---- .../0.8.9/lib/TriggerableWithdrawals.sol | 21 +++++-------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index f1f02a2b02..c470119140 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -11,7 +11,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {TriggerableWithdrawals} from "./lib/TriggerableWithdrawals.sol"; -import { ILidoLocator } from "../common/interfaces/ILidoLocator.sol"; +import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; interface ILido { /** @@ -51,7 +51,11 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { error NotLido(); error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); - error InsufficientTriggerableWithdrawalFee(uint256 providedTotalFee, uint256 requiredTotalFee, uint256 requestCount); + error InsufficientTriggerableWithdrawalFee( + uint256 providedTotalFee, + uint256 requiredTotalFee, + uint256 requestCount + ); error TriggerableWithdrawalRefundFailed(); /** @@ -149,9 +153,9 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { uint256 prevBalance = address(this).balance - msg.value; uint256 minFeePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); - uint256 totalFee = pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH * minFeePerRequest; + uint256 totalFee = (pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH) * minFeePerRequest; - if(totalFee > msg.value) { + if (totalFee > msg.value) { revert InsufficientTriggerableWithdrawalFee( msg.value, totalFee, diff --git a/contracts/0.8.9/lib/TriggerableWithdrawals.sol b/contracts/0.8.9/lib/TriggerableWithdrawals.sol index a601a59304..3bd8425a4e 100644 --- a/contracts/0.8.9/lib/TriggerableWithdrawals.sol +++ b/contracts/0.8.9/lib/TriggerableWithdrawals.sol @@ -23,10 +23,7 @@ library TriggerableWithdrawals { * The validator will fully withdraw and exit its duties as a validator. * @param pubkeys An array of public keys for the validators requesting full withdrawals. */ - function addFullWithdrawalRequests( - bytes calldata pubkeys, - uint256 feePerRequest - ) internal { + function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) internal { uint256 keysCount = _validateAndCountPubkeys(pubkeys); feePerRequest = _validateAndAdjustFee(feePerRequest, keysCount); @@ -74,11 +71,7 @@ library TriggerableWithdrawals { * @param pubkeys An array of public keys for the validators requesting withdrawals. * @param amounts An array of corresponding withdrawal amounts for each public key. */ - function addWithdrawalRequests( - bytes calldata pubkeys, - uint64[] calldata amounts, - uint256 feePerRequest - ) internal { + function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest) internal { uint256 keysCount = _validateAndCountPubkeys(pubkeys); if (keysCount != amounts.length) { @@ -116,11 +109,7 @@ library TriggerableWithdrawals { function _copyPubkeyToMemory(bytes calldata pubkeys, bytes memory target, uint256 keyIndex) private pure { assembly { - calldatacopy( - add(target, 32), - add(pubkeys.offset, mul(keyIndex, PUBLIC_KEY_LENGTH)), - PUBLIC_KEY_LENGTH - ) + calldatacopy(add(target, 32), add(pubkeys.offset, mul(keyIndex, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) } } @@ -131,7 +120,7 @@ library TriggerableWithdrawals { } function _validateAndCountPubkeys(bytes calldata pubkeys) private pure returns (uint256) { - if(pubkeys.length % PUBLIC_KEY_LENGTH != 0) { + if (pubkeys.length % PUBLIC_KEY_LENGTH != 0) { revert InvalidPublicKeyLength(); } @@ -154,7 +143,7 @@ library TriggerableWithdrawals { revert InsufficientRequestFee(feePerRequest, minFeePerRequest); } - if(address(this).balance < feePerRequest * keysCount) { + if (address(this).balance < feePerRequest * keysCount) { revert InsufficientBalance(address(this).balance, feePerRequest * keysCount); } From 6f303e572d12c0b138ffce6d0e683ae85f362f3b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 27 Jan 2025 18:29:47 +0100 Subject: [PATCH 021/405] refactor: move TriggerableWithdrawals lib from 0.8.9 to common --- contracts/0.8.9/WithdrawalVault.sol | 4 ++-- .../lib/TriggerableWithdrawals.sol | 6 ++++-- test/0.8.9/withdrawalVault.test.ts | 8 ++++---- .../EIP7002WithdrawalRequest_Mock.sol} | 13 ++++++------- .../TriggerableWithdrawals_Harness.sol | 19 +++++++++---------- .../lib/triggerableWithdrawals/eip7002Mock.ts | 0 .../triggerableWithdrawals.test.ts | 4 ++-- .../lib/triggerableWithdrawals/utils.ts | 8 ++++---- 8 files changed, 31 insertions(+), 31 deletions(-) rename contracts/{0.8.9 => common}/lib/TriggerableWithdrawals.sol (97%) rename test/{0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol => common/contracts/EIP7002WithdrawalRequest_Mock.sol} (81%) rename test/{0.8.9 => common}/contracts/TriggerableWithdrawals_Harness.sol (65%) rename test/{0.8.9 => common}/lib/triggerableWithdrawals/eip7002Mock.ts (100%) rename test/{0.8.9 => common}/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts (99%) rename test/{0.8.9 => common}/lib/triggerableWithdrawals/utils.ts (83%) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index c470119140..5ef5ee8ab7 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 /* See contracts/COMPILERS.md */ @@ -10,7 +10,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; -import {TriggerableWithdrawals} from "./lib/TriggerableWithdrawals.sol"; +import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; interface ILido { diff --git a/contracts/0.8.9/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol similarity index 97% rename from contracts/0.8.9/lib/TriggerableWithdrawals.sol rename to contracts/common/lib/TriggerableWithdrawals.sol index 3bd8425a4e..3c1ce0a518 100644 --- a/contracts/0.8.9/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -1,7 +1,9 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.9; +/* See contracts/COMPILERS.md */ +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.8.9 <0.9.0; library TriggerableWithdrawals { address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index e4bc64f172..92eb532c4e 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -6,10 +6,10 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { + EIP7002WithdrawalRequest_Mock, ERC20__Harness, ERC721__Harness, Lido__MockForWithdrawalVault, - WithdrawalsPredeployed_Mock, WithdrawalVault__Harness, } from "typechain-types"; @@ -17,12 +17,12 @@ import { MAX_UINT256, proxify, streccak } from "lib"; import { Snapshot } from "test/suite"; -import { findEip7002MockEvents, testEip7002Mock } from "./lib/triggerableWithdrawals/eip7002Mock"; +import { findEip7002MockEvents, testEip7002Mock } from "../common/lib/triggerableWithdrawals/eip7002Mock"; import { deployWithdrawalsPredeployedMock, generateWithdrawalRequestPayload, withdrawalsPredeployedHardcodedAddress, -} from "./lib/triggerableWithdrawals/utils"; +} from "../common/lib/triggerableWithdrawals/utils"; const PETRIFIED_VERSION = MAX_UINT256; @@ -39,7 +39,7 @@ describe("WithdrawalVault.sol", () => { let lido: Lido__MockForWithdrawalVault; let lidoAddress: string; - let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; + let withdrawalsPredeployed: EIP7002WithdrawalRequest_Mock; let impl: WithdrawalVault__Harness; let vault: WithdrawalVault__Harness; diff --git a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol b/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol similarity index 81% rename from test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol rename to test/common/contracts/EIP7002WithdrawalRequest_Mock.sol index 25581ff798..8ea01a81d1 100644 --- a/test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol +++ b/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: UNLICENSED +// for testing purposes only + pragma solidity 0.8.9; /** - * @notice This is an mock of EIP-7002's pre-deploy contract. + * @notice This is a mock of EIP-7002's pre-deploy contract. */ -contract WithdrawalsPredeployed_Mock { +contract EIP7002WithdrawalRequest_Mock { uint256 public fee; bool public failOnAddRequest; bool public failOnGetFee; @@ -24,7 +26,7 @@ contract WithdrawalsPredeployed_Mock { fee = _fee; } - fallback(bytes calldata input) external payable returns (bytes memory output){ + fallback(bytes calldata input) external payable returns (bytes memory output) { if (input.length == 0) { require(!failOnGetFee, "fail on get fee"); @@ -36,9 +38,6 @@ contract WithdrawalsPredeployed_Mock { require(input.length == 56, "Invalid callData length"); - emit eip7002MockRequestAdded( - input, - msg.value - ); + emit eip7002MockRequestAdded(input, msg.value); } } diff --git a/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol b/test/common/contracts/TriggerableWithdrawals_Harness.sol similarity index 65% rename from test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol rename to test/common/contracts/TriggerableWithdrawals_Harness.sol index 1ea18a48bf..a29db8a05c 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawals_Harness.sol +++ b/test/common/contracts/TriggerableWithdrawals_Harness.sol @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + pragma solidity 0.8.9; -import {TriggerableWithdrawals} from "contracts/0.8.9/lib/TriggerableWithdrawals.sol"; +import {TriggerableWithdrawals} from "contracts/common/lib/TriggerableWithdrawals.sol"; +/** + * @notice This is a harness of TriggerableWithdrawals library. + */ contract TriggerableWithdrawals_Harness { - function addFullWithdrawalRequests( - bytes calldata pubkeys, - uint256 feePerRequest - ) external { + function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) external { TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); } @@ -18,11 +21,7 @@ contract TriggerableWithdrawals_Harness { TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, feePerRequest); } - function addWithdrawalRequests( - bytes calldata pubkeys, - uint64[] calldata amounts, - uint256 feePerRequest - ) external { + function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest) external { TriggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, feePerRequest); } diff --git a/test/0.8.9/lib/triggerableWithdrawals/eip7002Mock.ts b/test/common/lib/triggerableWithdrawals/eip7002Mock.ts similarity index 100% rename from test/0.8.9/lib/triggerableWithdrawals/eip7002Mock.ts rename to test/common/lib/triggerableWithdrawals/eip7002Mock.ts diff --git a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts similarity index 99% rename from test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts rename to test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index 5600a7e279..07f7214e65 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -5,7 +5,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { TriggerableWithdrawals_Harness, WithdrawalsPredeployed_Mock } from "typechain-types"; +import { EIP7002WithdrawalRequest_Mock, TriggerableWithdrawals_Harness } from "typechain-types"; import { Snapshot } from "test/suite"; @@ -21,7 +21,7 @@ const EMPTY_PUBKEYS = "0x"; describe("TriggerableWithdrawals.sol", () => { let actor: HardhatEthersSigner; - let withdrawalsPredeployed: WithdrawalsPredeployed_Mock; + let withdrawalsPredeployed: EIP7002WithdrawalRequest_Mock; let triggerableWithdrawals: TriggerableWithdrawals_Harness; let originalState: string; diff --git a/test/0.8.9/lib/triggerableWithdrawals/utils.ts b/test/common/lib/triggerableWithdrawals/utils.ts similarity index 83% rename from test/0.8.9/lib/triggerableWithdrawals/utils.ts rename to test/common/lib/triggerableWithdrawals/utils.ts index 676cd9ac80..d98b8a9870 100644 --- a/test/0.8.9/lib/triggerableWithdrawals/utils.ts +++ b/test/common/lib/triggerableWithdrawals/utils.ts @@ -1,13 +1,13 @@ import { ethers } from "hardhat"; -import { WithdrawalsPredeployed_Mock } from "typechain-types"; +import { EIP7002WithdrawalRequest_Mock } from "typechain-types"; export const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; export async function deployWithdrawalsPredeployedMock( defaultRequestFee: bigint, -): Promise { - const withdrawalsPredeployed = await ethers.deployContract("WithdrawalsPredeployed_Mock"); +): Promise { + const withdrawalsPredeployed = await ethers.deployContract("EIP7002WithdrawalRequest_Mock"); const withdrawalsPredeployedAddress = await withdrawalsPredeployed.getAddress(); await ethers.provider.send("hardhat_setCode", [ @@ -15,7 +15,7 @@ export async function deployWithdrawalsPredeployedMock( await ethers.provider.getCode(withdrawalsPredeployedAddress), ]); - const contract = await ethers.getContractAt("WithdrawalsPredeployed_Mock", withdrawalsPredeployedHardcodedAddress); + const contract = await ethers.getContractAt("EIP7002WithdrawalRequest_Mock", withdrawalsPredeployedHardcodedAddress); await contract.setFee(defaultRequestFee); return contract; } From ade67a7704147877d8e0acdf852fcb24b3877e18 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 28 Jan 2025 09:41:44 +0100 Subject: [PATCH 022/405] refactor: improve naming for address validation utility --- contracts/0.8.9/WithdrawalVault.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 5ef5ee8ab7..8aa5d5a09c 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -63,8 +63,8 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) */ constructor(address _lido, address _treasury) { - _requireNonZero(_lido); - _requireNonZero(_treasury); + _onlyNonZeroAddress(_lido); + _onlyNonZeroAddress(_treasury); LIDO = ILido(_lido); TREASURY = _treasury; @@ -181,12 +181,12 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { return TriggerableWithdrawals.getWithdrawalRequestFee(); } - function _requireNonZero(address _address) internal pure { + function _onlyNonZeroAddress(address _address) internal pure { if (_address == address(0)) revert ZeroAddress(); } function _initialize_v2(address _admin) internal { - _requireNonZero(_admin); + _onlyNonZeroAddress(_admin); _setupRole(DEFAULT_ADMIN_ROLE, _admin); } } From 89d583aa37e993cf188c876c0bc17d0a8d0e5f7d Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 28 Jan 2025 13:09:59 +0100 Subject: [PATCH 023/405] test: add unit tests for Withdrawal Vault excess fee refund behavior --- test/0.8.9/contracts/RefundFailureTester.sol | 31 +++++++++ test/0.8.9/withdrawalVault.test.ts | 68 +++++++++++++++++-- .../lib/triggerableWithdrawals/eip7002Mock.ts | 9 ++- 3 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 test/0.8.9/contracts/RefundFailureTester.sol diff --git a/test/0.8.9/contracts/RefundFailureTester.sol b/test/0.8.9/contracts/RefundFailureTester.sol new file mode 100644 index 0000000000..0363e87cf5 --- /dev/null +++ b/test/0.8.9/contracts/RefundFailureTester.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +interface IWithdrawalVault { + function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; + function getWithdrawalRequestFee() external view returns (uint256); +} + +/** + * @notice This is a contract for testing refund failure in WithdrawalVault contract + */ +contract RefundFailureTester { + IWithdrawalVault private immutable withdrawalVault; + + constructor(address _withdrawalVault) { + withdrawalVault = IWithdrawalVault(_withdrawalVault); + } + + receive() external payable { + revert("Refund failed intentionally"); + } + + function addFullWithdrawalRequests(bytes calldata pubkeys) external payable { + require(msg.value > withdrawalVault.getWithdrawalRequestFee(), "Not enough eth for Refund"); + + // withdrawal vault should fail to refund + withdrawalVault.addFullWithdrawalRequests{value: msg.value}(pubkeys); + } +} diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 92eb532c4e..dea0118c87 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -10,6 +10,7 @@ import { ERC20__Harness, ERC721__Harness, Lido__MockForWithdrawalVault, + RefundFailureTester, WithdrawalVault__Harness, } from "typechain-types"; @@ -389,6 +390,34 @@ describe("WithdrawalVault.sol", () => { ).to.be.revertedWithCustomError(vault, "WithdrawalRequestFeeReadFailed"); }); + it("should revert if refund failed", async function () { + const refundFailureTester: RefundFailureTester = await ethers.deployContract("RefundFailureTester", [ + vaultAddress, + ]); + const refundFailureTesterAddress = await refundFailureTester.getAddress(); + + await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, refundFailureTesterAddress); + + const requestCount = 3; + const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.setFee(fee); + const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei + + await expect( + refundFailureTester + .connect(stranger) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + 1n }), + ).to.be.revertedWithCustomError(vault, "TriggerableWithdrawalRefundFailed"); + + await expect( + refundFailureTester + .connect(stranger) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + ethers.parseEther("1") }), + ).to.be.revertedWithCustomError(vault, "TriggerableWithdrawalRefundFailed"); + }); + it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); @@ -486,7 +515,31 @@ describe("WithdrawalVault.sol", () => { expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); }); - // ToDo: should return back the excess fee + it("Should refund excess fee", async function () { + const requestCount = 3; + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.setFee(fee); + const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei + const excessFee = 1n; + + const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + + const { receipt } = await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + excessFee }), + pubkeys, + fullWithdrawalAmounts, + fee, + ); + + expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( + vebInitialBalance - expectedTotalWithdrawalFee - receipt.gasUsed * receipt.gasPrice, + ); + }); it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; @@ -566,18 +619,25 @@ describe("WithdrawalVault.sol", () => { it(`Should successfully add ${requestCount} requests with extra fee ${extraFee}`, async () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); - const withdrawalFee = expectedFee * BigInt(requestCount) + extraFee; + const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); const initialBalance = await getWithdrawalCredentialsContractBalance(); + const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - await testEip7002Mock( - () => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: withdrawalFee }), + const { receipt } = await testEip7002Mock( + () => + vault + .connect(validatorsExitBus) + .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + extraFee }), pubkeys, fullWithdrawalAmounts, expectedFee, ); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( + vebInitialBalance - expectedTotalWithdrawalFee - receipt.gasUsed * receipt.gasPrice, + ); }); }); }); diff --git a/test/common/lib/triggerableWithdrawals/eip7002Mock.ts b/test/common/lib/triggerableWithdrawals/eip7002Mock.ts index 5fd83ae17d..a23d7c89ec 100644 --- a/test/common/lib/triggerableWithdrawals/eip7002Mock.ts +++ b/test/common/lib/triggerableWithdrawals/eip7002Mock.ts @@ -1,6 +1,5 @@ import { expect } from "chai"; -import { ContractTransactionReceipt } from "ethers"; -import { ContractTransactionResponse } from "ethers"; +import { ContractTransactionReceipt, ContractTransactionResponse } from "ethers"; import { ethers } from "hardhat"; import { findEventsWithInterfaces } from "lib"; @@ -25,7 +24,7 @@ export const testEip7002Mock = async ( expectedPubkeys: string[], expectedAmounts: bigint[], expectedFee: bigint, -) => { +): Promise<{ tx: ContractTransactionResponse; receipt: ContractTransactionReceipt }> => { const tx = await addTriggeranleWithdrawalRequests(); const receipt = await tx.wait(); @@ -37,5 +36,9 @@ export const testEip7002Mock = async ( expect(events[i].args[1]).to.equal(expectedFee); } + if (!receipt) { + throw new Error("No receipt"); + } + return { tx, receipt }; }; From cfadfb437c40c7740aaf0538c0247320d529ac03 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 28 Jan 2025 15:44:07 +0100 Subject: [PATCH 024/405] refactor: improve TriggerableWithdrawals lib methods description --- .../common/lib/TriggerableWithdrawals.sol | 80 +++++++++++++++---- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol index 3c1ce0a518..a5e265f5f8 100644 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -4,6 +4,11 @@ /* See contracts/COMPILERS.md */ // solhint-disable-next-line lido/fixed-compiler-version pragma solidity >=0.8.9 <0.9.0; + +/** + * @title A lib for EIP-7002: Execution layer triggerable withdrawals. + * Allow validators to trigger withdrawals and exits from their execution layer (0x01) withdrawal credentials. + */ library TriggerableWithdrawals { address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; @@ -21,9 +26,20 @@ library TriggerableWithdrawals { error InvalidPublicKeyLength(); /** - * @dev Adds full withdrawal requests for the provided public keys. - * The validator will fully withdraw and exit its duties as a validator. - * @param pubkeys An array of public keys for the validators requesting full withdrawals. + * @dev Send EIP-7002 full withdrawal requests for the specified public keys. + * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @param feePerRequest The withdrawal fee for each withdrawal request. + * - Must be greater than or equal to the current minimal withdrawal fee. + * - If set to zero, the current minimal withdrawal fee will be used automatically. + * + * @notice Reverts if: + * - Validation of the public keys fails. + * - The provided fee per request is insufficient. + * - The contract has an insufficient balance to cover the total fees. */ function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) internal { uint256 keysCount = _validateAndCountPubkeys(pubkeys); @@ -43,13 +59,27 @@ library TriggerableWithdrawals { } /** - * @dev Adds partial withdrawal requests for the provided public keys with corresponding amounts. - * A partial withdrawal is any withdrawal where the amount is greater than zero. - * A full withdrawal is any withdrawal where the amount is zero. - * This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn). - * However, the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. - * @param pubkeys An array of public keys for the validators requesting withdrawals. - * @param amounts An array of corresponding withdrawal amounts for each public key. + * @dev Send EIP-7002 partial withdrawal requests for the specified public keys with corresponding amounts. + * Each request instructs a validator to partially withdraw its stake. + * A partial withdrawal is any withdrawal where the amount is greater than zero, + * allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn), + * the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @param amounts An array of corresponding partial withdrawal amounts for each public key. + * + * @param feePerRequest The withdrawal fee for each withdrawal request. + * - Must be greater than or equal to the current minimal withdrawal fee. + * - If set to zero, the current minimal withdrawal fee will be used automatically. + * + * @notice Reverts if: + * - Validation of the public keys fails. + * - The pubkeys and amounts length mismatch. + * - Full withdrawal requested for any pubkeys (withdrawal amount = 0). + * - The provided fee per request is insufficient. + * - The contract has an insufficient balance to cover the total fees. */ function addPartialWithdrawalRequests( bytes calldata pubkeys, @@ -66,12 +96,30 @@ library TriggerableWithdrawals { } /** - * @dev Adds partial or full withdrawal requests for the provided public keys with corresponding amounts. - * A partial withdrawal is any withdrawal where the amount is greater than zero. - * This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn). - * However, the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. - * @param pubkeys An array of public keys for the validators requesting withdrawals. - * @param amounts An array of corresponding withdrawal amounts for each public key. + * @dev Send EIP-7002 partial or full withdrawal requests for the specified public keys with corresponding amounts. + * Each request instructs a validator to partially or fully withdraw its stake. + + * 1. A partial withdrawal is any withdrawal where the amount is greater than zero, + * allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn), + * the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. + * + * 2. A full withdrawal is a withdrawal where the amount is equal to zero, + * allows to fully withdraw validator stake and exit its duties as a validator. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @param amounts An array of corresponding partial withdrawal amounts for each public key. + * + * @param feePerRequest The withdrawal fee for each withdrawal request. + * - Must be greater than or equal to the current minimal withdrawal fee. + * - If set to zero, the current minimal withdrawal fee will be used automatically. + * + * @notice Reverts if: + * - Validation of the public keys fails. + * - The pubkeys and amounts length mismatch. + * - The provided fee per request is insufficient. + * - The contract has an insufficient balance to cover the total fees. */ function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest) internal { uint256 keysCount = _validateAndCountPubkeys(pubkeys); From 9f268cf5a3982cb565d71525fbe04e5cfbc64a81 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 28 Jan 2025 16:46:15 +0100 Subject: [PATCH 025/405] refactor: triggerable withdrawals lib rename errors for clarity --- .../common/lib/TriggerableWithdrawals.sol | 24 +++++++------- test/0.8.9/withdrawalVault.test.ts | 11 +++---- .../triggerableWithdrawals.test.ts | 32 +++++++++---------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol index a5e265f5f8..cba6198969 100644 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -11,19 +11,21 @@ pragma solidity >=0.8.9 <0.9.0; */ library TriggerableWithdrawals { address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; - uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; + uint256 internal constant PUBLIC_KEY_LENGTH = 48; uint256 internal constant WITHDRAWAL_AMOUNT_LENGTH = 8; + uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; - error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); - error InsufficientBalance(uint256 balance, uint256 totalWithdrawalFee); - error InsufficientRequestFee(uint256 feePerRequest, uint256 minFeePerRequest); - - error WithdrawalRequestFeeReadFailed(); + error WithdrawalFeeReadFailed(); error WithdrawalRequestAdditionFailed(bytes callData); + + error InsufficientWithdrawalFee(uint256 feePerRequest, uint256 minFeePerRequest); + error TotalWithdrawalFeeExceededBalance(uint256 balance, uint256 totalWithdrawalFee); + error NoWithdrawalRequests(); + error MalformedPubkeysArray(); error PartialWithdrawalRequired(uint256 index); - error InvalidPublicKeyLength(); + error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); /** * @dev Send EIP-7002 full withdrawal requests for the specified public keys. @@ -151,7 +153,7 @@ library TriggerableWithdrawals { (bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall(""); if (!success) { - revert WithdrawalRequestFeeReadFailed(); + revert WithdrawalFeeReadFailed(); } return abi.decode(feeData, (uint256)); @@ -171,7 +173,7 @@ library TriggerableWithdrawals { function _validateAndCountPubkeys(bytes calldata pubkeys) private pure returns (uint256) { if (pubkeys.length % PUBLIC_KEY_LENGTH != 0) { - revert InvalidPublicKeyLength(); + revert MalformedPubkeysArray(); } uint256 keysCount = pubkeys.length / PUBLIC_KEY_LENGTH; @@ -190,11 +192,11 @@ library TriggerableWithdrawals { } if (feePerRequest < minFeePerRequest) { - revert InsufficientRequestFee(feePerRequest, minFeePerRequest); + revert InsufficientWithdrawalFee(feePerRequest, minFeePerRequest); } if (address(this).balance < feePerRequest * keysCount) { - revert InsufficientBalance(address(this).balance, feePerRequest * keysCount); + revert TotalWithdrawalFeeExceededBalance(address(this).balance, feePerRequest * keysCount); } return feePerRequest; diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index dea0118c87..bfe3e97d2c 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -282,10 +282,7 @@ describe("WithdrawalVault.sol", () => { it("Should revert if fee read fails", async function () { await withdrawalsPredeployed.setFailOnGetFee(true); - await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError( - vault, - "WithdrawalRequestFeeReadFailed", - ); + await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); }); }); @@ -351,7 +348,7 @@ describe("WithdrawalVault.sol", () => { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(invalidPubkeyHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); }); it("Should revert if last pubkey not 48 bytes", async function () { @@ -364,7 +361,7 @@ describe("WithdrawalVault.sol", () => { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); }); it("Should revert if addition fails at the withdrawal request contract", async function () { @@ -387,7 +384,7 @@ describe("WithdrawalVault.sol", () => { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalRequestFeeReadFailed"); + ).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); }); it("should revert if refund failed", async function () { diff --git a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index 07f7214e65..39b69836e1 100644 --- a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -78,7 +78,7 @@ describe("TriggerableWithdrawals.sol", () => { await withdrawalsPredeployed.setFailOnGetFee(true); await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( triggerableWithdrawals, - "WithdrawalRequestFeeReadFailed", + "WithdrawalFeeReadFailed", ); }); }); @@ -133,15 +133,15 @@ describe("TriggerableWithdrawals.sol", () => { // 2. Should revert if fee is less than required const insufficientFee = 2n; await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, insufficientFee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientWithdrawalFee") .withArgs(2n, 3n); await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, insufficientFee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientWithdrawalFee") .withArgs(2n, 3n); await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, insufficientFee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientRequestFee") + .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientWithdrawalFee") .withArgs(2n, 3n); }); @@ -154,15 +154,15 @@ describe("TriggerableWithdrawals.sol", () => { await expect( triggerableWithdrawals.addFullWithdrawalRequests(invalidPubkeyHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); await expect( triggerableWithdrawals.addPartialWithdrawalRequests(invalidPubkeyHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); await expect( triggerableWithdrawals.addWithdrawalRequests(invalidPubkeyHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); }); it("Should revert if last pubkey not 48 bytes", async function () { @@ -177,15 +177,15 @@ describe("TriggerableWithdrawals.sol", () => { await expect( triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); await expect( triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); await expect( triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "InvalidPublicKeyLength"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); }); it("Should revert if addition fails at the withdrawal request contract", async function () { @@ -233,15 +233,15 @@ describe("TriggerableWithdrawals.sol", () => { await setBalance(await triggerableWithdrawals.getAddress(), balance); await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") + .to.be.revertedWithCustomError(triggerableWithdrawals, "TotalWithdrawalFeeExceededBalance") .withArgs(balance, expectedMinimalBalance); await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") + .to.be.revertedWithCustomError(triggerableWithdrawals, "TotalWithdrawalFeeExceededBalance") .withArgs(balance, expectedMinimalBalance); await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientBalance") + .to.be.revertedWithCustomError(triggerableWithdrawals, "TotalWithdrawalFeeExceededBalance") .withArgs(balance, expectedMinimalBalance); }); @@ -254,15 +254,15 @@ describe("TriggerableWithdrawals.sol", () => { await expect( triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); await expect( triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); await expect( triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestFeeReadFailed"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); }); it("Should accept withdrawal requests with minimal possible fee when fee not provided", async function () { From 811fdf814ee7fb9b68b60a1e2194777e7db88206 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 28 Jan 2025 17:15:33 +0100 Subject: [PATCH 026/405] refactor: describe full withdrawal method in withdrawal vault --- contracts/0.8.9/WithdrawalVault.sol | 20 ++++++++++++++++--- .../common/lib/TriggerableWithdrawals.sol | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 8aa5d5a09c..9df5e186f0 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -143,9 +143,19 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { } /** - * @dev Adds full withdrawal requests for the provided public keys. - * The validator will fully withdraw and exit its duties as a validator. - * @param pubkeys An array of public keys for the validators requesting full withdrawals. + * @dev Submits EIP-7002 full withdrawal requests for the specified public keys. + * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. + * Refunds any excess fee to the caller after deducting the total fees, + * which are calculated based on the number of public keys and the current minimum fee per withdrawal request. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @notice Reverts if: + * - The caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE`. + * - Validation of any of the provided public keys fails. + * - The provided total withdrawal fee is insufficient to cover all requests. + * - Refund of the excess fee fails. */ function addFullWithdrawalRequests( bytes calldata pubkeys @@ -177,6 +187,10 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { assert(address(this).balance == prevBalance); } + /** + * @dev Retrieves the current EIP-7002 withdrawal fee. + * @return The minimum fee required per withdrawal request. + */ function getWithdrawalRequestFee() external view returns (uint256) { return TriggerableWithdrawals.getWithdrawalRequestFee(); } diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol index cba6198969..30b94fdfe4 100644 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -146,7 +146,7 @@ library TriggerableWithdrawals { } /** - * @dev Retrieves the current withdrawal request fee. + * @dev Retrieves the current EIP-7002 withdrawal fee. * @return The minimum fee required per withdrawal request. */ function getWithdrawalRequestFee() internal view returns (uint256) { From 6da1d6f7f4fbf2d112e24e1b38798cc61d33e935 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Sat, 1 Feb 2025 11:47:49 +0100 Subject: [PATCH 027/405] feat: grant withdrawal request role to ValidatorsExitBusOracle contract during scratch deploy Grant ADD_FULL_WITHDRAWAL_REQUEST_ROLE to ValidatorsExitBusOracle contract --- scripts/scratch/steps/0130-grant-roles.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index 2ef6f4f5ea..f332bc8403 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -1,6 +1,12 @@ import { ethers } from "hardhat"; -import { Burner, StakingRouter, ValidatorsExitBusOracle, WithdrawalQueueERC721 } from "typechain-types"; +import { + Burner, + StakingRouter, + ValidatorsExitBusOracle, + WithdrawalQueueERC721, + WithdrawalVault, +} from "typechain-types"; import { loadContract } from "lib/contract"; import { makeTx } from "lib/deploy"; @@ -19,6 +25,7 @@ export async function main() { const burnerAddress = state[Sk.burner].address; const stakingRouterAddress = state[Sk.stakingRouter].proxy.address; const withdrawalQueueAddress = state[Sk.withdrawalQueueERC721].proxy.address; + const withdrawalVaultAddress = state[Sk.withdrawalVault].proxy.address; const accountingOracleAddress = state[Sk.accountingOracle].proxy.address; const validatorsExitBusOracleAddress = state[Sk.validatorsExitBusOracle].proxy.address; const depositSecurityModuleAddress = state[Sk.depositSecurityModule].address; @@ -77,6 +84,18 @@ export async function main() { from: deployer, }); + // WithdrawalVault + const withdrawalVault = await loadContract("WithdrawalVault", withdrawalVaultAddress); + + await makeTx( + withdrawalVault, + "grantRole", + [await withdrawalVault.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(), validatorsExitBusOracleAddress], + { + from: deployer, + }, + ); + // Burner const burner = await loadContract("Burner", burnerAddress); // NB: REQUEST_BURN_SHARES_ROLE is already granted to Lido in Burner constructor From 1af1d3a24170acad301fc53c4d328f9229b13f1e Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 4 Feb 2025 14:38:57 +0100 Subject: [PATCH 028/405] feat: validate withdrawal fee response --- .../common/lib/TriggerableWithdrawals.sol | 5 +++ test/0.8.9/withdrawalVault.test.ts | 21 ++++++++++++ .../EIP7002WithdrawalRequest_Mock.sol | 13 +++++--- .../triggerableWithdrawals.test.ts | 33 +++++++++++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol index 30b94fdfe4..79916b1a6b 100644 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -17,6 +17,7 @@ library TriggerableWithdrawals { uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; error WithdrawalFeeReadFailed(); + error WithdrawalFeeInvalidData(); error WithdrawalRequestAdditionFailed(bytes callData); error InsufficientWithdrawalFee(uint256 feePerRequest, uint256 minFeePerRequest); @@ -156,6 +157,10 @@ library TriggerableWithdrawals { revert WithdrawalFeeReadFailed(); } + if (feeData.length != 32) { + revert WithdrawalFeeInvalidData(); + } + return abi.decode(feeData, (uint256)); } diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index bfe3e97d2c..a584e896fb 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -284,6 +284,14 @@ describe("WithdrawalVault.sol", () => { await withdrawalsPredeployed.setFailOnGetFee(true); await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); }); + + ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { + it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + + await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); + }); + }); }); async function getFee(): Promise { @@ -387,6 +395,19 @@ describe("WithdrawalVault.sol", () => { ).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); }); + ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { + it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + + const { pubkeysHexString } = generateWithdrawalRequestPayload(2); + const fee = 10n; + + await expect( + vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), + ).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); + }); + }); + it("should revert if refund failed", async function () { const refundFailureTester: RefundFailureTester = await ethers.deployContract("RefundFailureTester", [ vaultAddress, diff --git a/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol b/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol index 8ea01a81d1..4ed8060243 100644 --- a/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol +++ b/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol @@ -7,7 +7,7 @@ pragma solidity 0.8.9; * @notice This is a mock of EIP-7002's pre-deploy contract. */ contract EIP7002WithdrawalRequest_Mock { - uint256 public fee; + bytes public fee; bool public failOnAddRequest; bool public failOnGetFee; @@ -23,15 +23,18 @@ contract EIP7002WithdrawalRequest_Mock { function setFee(uint256 _fee) external { require(_fee > 0, "fee must be greater than 0"); - fee = _fee; + fee = abi.encode(_fee); } - fallback(bytes calldata input) external payable returns (bytes memory output) { + function setFeeRaw(bytes calldata _rawFeeBytes) external { + fee = _rawFeeBytes; + } + + fallback(bytes calldata input) external payable returns (bytes memory) { if (input.length == 0) { require(!failOnGetFee, "fail on get fee"); - output = abi.encode(fee); - return output; + return fee; } require(!failOnAddRequest, "fail on add request"); diff --git a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index 39b69836e1..d3f271d81d 100644 --- a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -81,6 +81,17 @@ describe("TriggerableWithdrawals.sol", () => { "WithdrawalFeeReadFailed", ); }); + + ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { + it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + + await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( + triggerableWithdrawals, + "WithdrawalFeeInvalidData", + ); + }); + }); }); context("add triggerable withdrawal requests", () => { @@ -265,6 +276,28 @@ describe("TriggerableWithdrawals.sol", () => { ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); }); + ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { + it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + + const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = + generateWithdrawalRequestPayload(2); + const fee = 10n; + + await expect( + triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeInvalidData"); + + await expect( + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeInvalidData"); + + await expect( + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeInvalidData"); + }); + }); + it("Should accept withdrawal requests with minimal possible fee when fee not provided", async function () { const requestCount = 3; const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = From c27de348951788abcc4f29c7cafa24c58fd633e9 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 4 Feb 2025 14:40:00 +0100 Subject: [PATCH 029/405] feat: update eip-7002 contract address --- contracts/common/lib/TriggerableWithdrawals.sol | 2 +- test/common/lib/triggerableWithdrawals/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol index 79916b1a6b..0547065e8e 100644 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -10,7 +10,7 @@ pragma solidity >=0.8.9 <0.9.0; * Allow validators to trigger withdrawals and exits from their execution layer (0x01) withdrawal credentials. */ library TriggerableWithdrawals { - address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA; + address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; uint256 internal constant PUBLIC_KEY_LENGTH = 48; uint256 internal constant WITHDRAWAL_AMOUNT_LENGTH = 8; diff --git a/test/common/lib/triggerableWithdrawals/utils.ts b/test/common/lib/triggerableWithdrawals/utils.ts index d98b8a9870..678a4a9fb5 100644 --- a/test/common/lib/triggerableWithdrawals/utils.ts +++ b/test/common/lib/triggerableWithdrawals/utils.ts @@ -2,7 +2,7 @@ import { ethers } from "hardhat"; import { EIP7002WithdrawalRequest_Mock } from "typechain-types"; -export const withdrawalsPredeployedHardcodedAddress = "0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA"; +export const withdrawalsPredeployedHardcodedAddress = "0x00000961Ef480Eb55e80D19ad83579A64c007002"; export async function deployWithdrawalsPredeployedMock( defaultRequestFee: bigint, From 2e1a0d122780d1034f7acb3344d0fcac4ed349b7 Mon Sep 17 00:00:00 2001 From: F4ever Date: Mon, 10 Feb 2025 10:18:05 +0100 Subject: [PATCH 030/405] feat: add tw deploy script --- lib/state-file.ts | 2 + scripts/triggerable-withdrawals/.env.sample | 9 ++ scripts/triggerable-withdrawals/tw-deploy.ts | 142 +++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 scripts/triggerable-withdrawals/.env.sample create mode 100644 scripts/triggerable-withdrawals/tw-deploy.ts diff --git a/lib/state-file.ts b/lib/state-file.ts index 646448751b..71332794f3 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -86,6 +86,7 @@ export enum Sk { chainSpec = "chainSpec", scratchDeployGasUsed = "scratchDeployGasUsed", minFirstAllocationStrategy = "minFirstAllocationStrategy", + triggerableWithdrawals = "triggerableWithdrawals", } export function getAddress(contractKey: Sk, state: DeploymentState): string { @@ -108,6 +109,7 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.withdrawalQueueERC721: case Sk.withdrawalVault: return state[contractKey].proxy.address; + case Sk.triggerableWithdrawals: case Sk.apmRegistryFactory: case Sk.burner: case Sk.callsScript: diff --git a/scripts/triggerable-withdrawals/.env.sample b/scripts/triggerable-withdrawals/.env.sample new file mode 100644 index 0000000000..8157a2159d --- /dev/null +++ b/scripts/triggerable-withdrawals/.env.sample @@ -0,0 +1,9 @@ +# Deployer +DEPLOYER= +DEPLOYER_PRIVATE_KEY= +# Chain config +RPC_URL= +NETWORK= +# Deploy transactions gas +GAS_PRIORITY_FEE= +GAS_MAX_FEE= diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts new file mode 100644 index 0000000000..1ba83a3ae3 --- /dev/null +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -0,0 +1,142 @@ +import * as dotenv from "dotenv"; +import { ethers, run } from "hardhat"; +import { join } from "path"; +import readline from "readline"; + +import { LidoLocator } from "typechain-types"; + +import { + cy, + deployImplementation, + deployWithoutProxy, + loadContract, + log, + persistNetworkState, + readNetworkState, + Sk, +} from "lib"; + +dotenv.config({ path: join(__dirname, "../../.env") }); + +function getEnvVariable(name: string, defaultValue?: string) { + const value = process.env[name]; + if (value === undefined) { + if (defaultValue === undefined) { + throw new Error(`Env variable ${name} must be set`); + } + return defaultValue; + } else { + log(`Using env variable ${name}=${value}`); + return value; + } +} + +/* Accounting Oracle args */ + +// Must comply with the specification +// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 +const SECONDS_PER_SLOT = 12; + +// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis +// and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb +const GENESIS_TIME = 1606824023; +const LOCATOR_ADDRESS = "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"; + +async function main() { + const deployer = ethers.getAddress(getEnvVariable("DEPLOYER")); + const chainId = (await ethers.provider.getNetwork()).chainId; + + log(cy(`Deploy of contracts on chain ${chainId}`)); + + const state = readNetworkState(); + persistNetworkState(state); + + // Read contracts addresses from config + const locator = await loadContract("LidoLocator", LOCATOR_ADDRESS); + + const LIDO_PROXY = await locator.lido(); + const TREASURY_PROXY = await locator.treasury(); + + // Deploy ValidatorExitBusOracle + // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator + const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, GENESIS_TIME, locator]; + + const validatorsExitBusOracle = ( + await deployImplementation( + Sk.validatorsExitBusOracle, + "ValidatorsExitBusOracle", + deployer, + validatorsExitBusOracleArgs, + ) + ).address; + log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle}`); + log.emptyLine(); + + // Deploy Triggerable Withdrawal Library + const triggerableWithdrawals = ( + await deployWithoutProxy(Sk.triggerableWithdrawals, "triggerableWithdrawals", deployer) + ).address; + log.success(`TriggerableWithdrawal address: ${triggerableWithdrawals}`); + log.emptyLine(); + + const libraries = { triggerableWithdrawals: triggerableWithdrawals }; + + // Deploy WithdrawalVault + const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; + + const withdrawalVault = ( + await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, withdrawalVaultArgs, { libraries }) + ).address; + log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); + log.emptyLine(); + + // Deploy AO + // const accountingOracleArgs = [LOCATOR, LIDO, LEGACY_ORACLE, SECONDS_PER_SLOT, GENESIS_TIME]; + // const accountingOracleAddress = ( + // await deployImplementation(Sk.accountingOracle, "AccountingOracle", deployer, accountingOracleArgs) + // ).address; + // log.success(`AO implementation address: ${accountingOracleAddress}`); + // log.emptyLine(); + + await waitForPressButton(); + + log(cy("Continuing...")); + + await run("verify:verify", { + address: triggerableWithdrawals, + constructorArguments: [], + contract: "contracts/common/lib/TriggerableWithdrawals.sol:TriggerableWithdrawals", + }); + + await run("verify:verify", { + address: withdrawalVault, + constructorArguments: withdrawalVaultArgs, + libraries: libraries, + contract: "contracts/0.8.9/WithdrawalVault.sol:WithdrawalVault", + }); + + await run("verify:verify", { + address: validatorsExitBusOracle, + constructorArguments: [], + contract: "contracts/0.8.9/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", + }); +} + +async function waitForPressButton(): Promise { + return new Promise((resolve) => { + log(cy("When contracts will be ready for verification step, press Enter to continue...")); + const rl = readline.createInterface({ input: process.stdin }); + + rl.on("line", () => { + rl.close(); + resolve(); + }); + }); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + log.error(error); + process.exit(1); + }); From 4d8e5f35d196ec56bb3cec117e8d003d8711d77c Mon Sep 17 00:00:00 2001 From: F4ever Date: Tue, 11 Feb 2025 13:24:42 +0100 Subject: [PATCH 031/405] feat: update tw script --- scripts/triggerable-withdrawals/tw-deploy.ts | 34 ++------------------ 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 1ba83a3ae3..2041179161 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -5,16 +5,7 @@ import readline from "readline"; import { LidoLocator } from "typechain-types"; -import { - cy, - deployImplementation, - deployWithoutProxy, - loadContract, - log, - persistNetworkState, - readNetworkState, - Sk, -} from "lib"; +import { cy, deployImplementation, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; dotenv.config({ path: join(__dirname, "../../.env") }); @@ -31,8 +22,6 @@ function getEnvVariable(name: string, defaultValue?: string) { } } -/* Accounting Oracle args */ - // Must comply with the specification // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 const SECONDS_PER_SLOT = 12; @@ -40,7 +29,6 @@ const SECONDS_PER_SLOT = 12; // Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis // and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb const GENESIS_TIME = 1606824023; -const LOCATOR_ADDRESS = "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"; async function main() { const deployer = ethers.getAddress(getEnvVariable("DEPLOYER")); @@ -52,7 +40,7 @@ async function main() { persistNetworkState(state); // Read contracts addresses from config - const locator = await loadContract("LidoLocator", LOCATOR_ADDRESS); + const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); const LIDO_PROXY = await locator.lido(); const TREASURY_PROXY = await locator.treasury(); @@ -72,20 +60,11 @@ async function main() { log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle}`); log.emptyLine(); - // Deploy Triggerable Withdrawal Library - const triggerableWithdrawals = ( - await deployWithoutProxy(Sk.triggerableWithdrawals, "triggerableWithdrawals", deployer) - ).address; - log.success(`TriggerableWithdrawal address: ${triggerableWithdrawals}`); - log.emptyLine(); - - const libraries = { triggerableWithdrawals: triggerableWithdrawals }; - // Deploy WithdrawalVault const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; const withdrawalVault = ( - await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, withdrawalVaultArgs, { libraries }) + await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, withdrawalVaultArgs) ).address; log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); log.emptyLine(); @@ -102,16 +81,9 @@ async function main() { log(cy("Continuing...")); - await run("verify:verify", { - address: triggerableWithdrawals, - constructorArguments: [], - contract: "contracts/common/lib/TriggerableWithdrawals.sol:TriggerableWithdrawals", - }); - await run("verify:verify", { address: withdrawalVault, constructorArguments: withdrawalVaultArgs, - libraries: libraries, contract: "contracts/0.8.9/WithdrawalVault.sol:WithdrawalVault", }); From 5cff88ecec6e93dc977a4e5d38d599595e42ad60 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 12 Feb 2025 18:19:52 +0100 Subject: [PATCH 032/405] fix: read GENESIS_TIME from env --- scripts/triggerable-withdrawals/tw-deploy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 2041179161..78c805a672 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -28,7 +28,7 @@ const SECONDS_PER_SLOT = 12; // Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis // and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb -const GENESIS_TIME = 1606824023; +const genesisTime = parseInt(getEnvVariable("GENESIS_TIME")); async function main() { const deployer = ethers.getAddress(getEnvVariable("DEPLOYER")); @@ -47,7 +47,7 @@ async function main() { // Deploy ValidatorExitBusOracle // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator - const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, GENESIS_TIME, locator]; + const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator]; const validatorsExitBusOracle = ( await deployImplementation( @@ -77,7 +77,7 @@ async function main() { // log.success(`AO implementation address: ${accountingOracleAddress}`); // log.emptyLine(); - await waitForPressButton(); + // await waitForPressButton(); log(cy("Continuing...")); From 5c3647d629f900b478f3a528e8ea05b0dd6856ba Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 12 Feb 2025 19:38:01 +0100 Subject: [PATCH 033/405] fix: deploy tw script --- scripts/triggerable-withdrawals/tw-deploy.ts | 32 +++----------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 78c805a672..b49380f50c 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -1,7 +1,6 @@ import * as dotenv from "dotenv"; import { ethers, run } from "hardhat"; import { join } from "path"; -import readline from "readline"; import { LidoLocator } from "typechain-types"; @@ -47,7 +46,7 @@ async function main() { // Deploy ValidatorExitBusOracle // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator - const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator]; + const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; const validatorsExitBusOracle = ( await deployImplementation( @@ -60,7 +59,6 @@ async function main() { log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle}`); log.emptyLine(); - // Deploy WithdrawalVault const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; const withdrawalVault = ( @@ -69,18 +67,10 @@ async function main() { log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); log.emptyLine(); - // Deploy AO - // const accountingOracleArgs = [LOCATOR, LIDO, LEGACY_ORACLE, SECONDS_PER_SLOT, GENESIS_TIME]; - // const accountingOracleAddress = ( - // await deployImplementation(Sk.accountingOracle, "AccountingOracle", deployer, accountingOracleArgs) - // ).address; - // log.success(`AO implementation address: ${accountingOracleAddress}`); - // log.emptyLine(); - - // await waitForPressButton(); - log(cy("Continuing...")); + await new Promise((res) => setTimeout(res, 5000)); + await run("verify:verify", { address: withdrawalVault, constructorArguments: withdrawalVaultArgs, @@ -89,20 +79,8 @@ async function main() { await run("verify:verify", { address: validatorsExitBusOracle, - constructorArguments: [], - contract: "contracts/0.8.9/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", - }); -} - -async function waitForPressButton(): Promise { - return new Promise((resolve) => { - log(cy("When contracts will be ready for verification step, press Enter to continue...")); - const rl = readline.createInterface({ input: process.stdin }); - - rl.on("line", () => { - rl.close(); - resolve(); - }); + constructorArguments: validatorsExitBusOracleArgs, + contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", }); } From 7feab5d8925d7efd009b2c77fc706b8746d384f4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 13 Feb 2025 17:08:51 +0100 Subject: [PATCH 034/405] feat: tw deploy --- scripts/triggerable-withdrawals/tw-deploy.ts | 19 +----- scripts/triggerable-withdrawals/tw-verify.ts | 68 ++++++++++++++++++++ 2 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 scripts/triggerable-withdrawals/tw-verify.ts diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index b49380f50c..36fd82db55 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -1,5 +1,5 @@ import * as dotenv from "dotenv"; -import { ethers, run } from "hardhat"; +import { ethers } from "hardhat"; import { join } from "path"; import { LidoLocator } from "typechain-types"; @@ -65,23 +65,6 @@ async function main() { await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, withdrawalVaultArgs) ).address; log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); - log.emptyLine(); - - log(cy("Continuing...")); - - await new Promise((res) => setTimeout(res, 5000)); - - await run("verify:verify", { - address: withdrawalVault, - constructorArguments: withdrawalVaultArgs, - contract: "contracts/0.8.9/WithdrawalVault.sol:WithdrawalVault", - }); - - await run("verify:verify", { - address: validatorsExitBusOracle, - constructorArguments: validatorsExitBusOracleArgs, - contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", - }); } main() diff --git a/scripts/triggerable-withdrawals/tw-verify.ts b/scripts/triggerable-withdrawals/tw-verify.ts new file mode 100644 index 0000000000..87e4c03d45 --- /dev/null +++ b/scripts/triggerable-withdrawals/tw-verify.ts @@ -0,0 +1,68 @@ +import * as dotenv from "dotenv"; +import { ethers, run } from "hardhat"; +import { join } from "path"; + +import { LidoLocator } from "typechain-types"; + +import { cy, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; + +dotenv.config({ path: join(__dirname, "../../.env") }); + +function getEnvVariable(name: string, defaultValue?: string) { + const value = process.env[name]; + if (value === undefined) { + if (defaultValue === undefined) { + throw new Error(`Env variable ${name} must be set`); + } + return defaultValue; + } else { + log(`Using env variable ${name}=${value}`); + return value; + } +} + +// Must comply with the specification +// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 +const SECONDS_PER_SLOT = 12; + +// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis +// and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb +const genesisTime = parseInt(getEnvVariable("GENESIS_TIME")); + +async function main() { + const chainId = (await ethers.provider.getNetwork()).chainId; + + log(cy(`Deploy of contracts on chain ${chainId}`)); + + const state = readNetworkState(); + persistNetworkState(state); + + // Read contracts addresses from config + const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); + + const LIDO_PROXY = await locator.lido(); + const TREASURY_PROXY = await locator.treasury(); + + const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; + + const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; + + await run("verify:verify", { + address: state[Sk.withdrawalVault].implementation.address, + constructorArguments: withdrawalVaultArgs, + contract: "contracts/0.8.9/WithdrawalVault.sol:WithdrawalVault", + }); + + await run("verify:verify", { + address: state[Sk.validatorsExitBusOracle].implementation.address, + constructorArguments: validatorsExitBusOracleArgs, + contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", + }); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + log.error(error); + process.exit(1); + }); From 46b3c40bdcd9065f63b7f7dd9a67390555264ea5 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 17 Feb 2025 12:20:38 +0400 Subject: [PATCH 035/405] fix: vebo tw unit tests --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 29 ++++++--------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 1 - .../contracts/WithdrawalValut_MockForVebo.sol | 4 +- ...t-bus-oracle.triggerExitHashVerify.test.ts | 37 +++++++++++++++---- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index bc3d49d547..7bf117c1ee 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -7,7 +7,7 @@ import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; interface IWithdrawalVault { - function addFullWithdrawalRequests(bytes[] calldata pubkeys) external payable; + function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; function getWithdrawalRequestFee() external view returns (uint256); } @@ -76,6 +76,8 @@ contract ValidatorsExitBus is AccessControlEnumerable { /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; + uint256 internal constant PUBLIC_KEY_LENGTH = 48; + /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); @@ -98,42 +100,33 @@ contract ValidatorsExitBus is AccessControlEnumerable { address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); - uint256 fee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - uint requestsFee = keyIndexes.length * fee; + uint256 minFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); + uint requestsFee = keyIndexes.length * minFee; if (msg.value < requestsFee) { - revert FeeNotEnough(fee, keyIndexes.length, msg.value); + revert FeeNotEnough(minFee, keyIndexes.length, msg.value); } uint256 refund = msg.value - requestsFee; uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; - uint256 offset; bytes calldata data = exitRequestData.data; - bytes[] memory pubkeys = new bytes[](keyIndexes.length); - - assembly { - offset := data.offset - } + bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] > lastDeliveredKeyIndex) { revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex); } - uint256 requestOffset = offset + keyIndexes[i] * 64; + uint256 requestOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16; - bytes calldata pubkey; + for (uint256 j = 0; j < PUBLIC_KEY_LENGTH; j++) { + pubkeys[i * PUBLIC_KEY_LENGTH + j] = data[requestOffset + j]; - assembly { - pubkey.offset := add(requestOffset, 16) - pubkey.length := 48 } - pubkeys[i] = pubkey; - } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests(pubkeys); + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: requestsFee}(pubkeys); if (refund > 0) { (bool success, ) = msg.sender.call{value: refund}(""); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index b6f15cb776..18424175ea 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -166,7 +166,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus /// /// Requests data /// - ExitRequestData exitRequestData; } diff --git a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol index 97ca6e6011..f9810b2314 100644 --- a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol +++ b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.9; contract WithdrawalVault__MockForVebo { - event AddFullWithdrawalRequestsCalled(bytes[] pubkeys); + event AddFullWithdrawalRequestsCalled(bytes pubkeys); - function addFullWithdrawalRequests(bytes[] calldata pubkeys) external { + function addFullWithdrawalRequests(bytes calldata pubkeys) external payable { emit AddFullWithdrawalRequestsCalled(pubkeys); } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index d9dc51c8a9..c86498c6ff 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -150,6 +150,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, ]; reportFields = { @@ -220,24 +221,46 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("last requested validator indices are updated", async () => { - const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n]); - const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n]); + const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n, 3n]); + const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n, 3n]); - expect([...indices1]).to.have.ordered.members([2n, -1n, -1n]); - expect([...indices2]).to.have.ordered.members([1n, -1n, -1n]); + expect([...indices1]).to.have.ordered.members([2n, -1n, -1n, -1n]); + expect([...indices2]).to.have.ordered.members([3n, -1n, -1n, -1n]); }); it("someone submitted exit report data and triggered exit", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 2], { value: 3 }); + const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 2, 3], { value: 4 }); + const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[2], PUBKEYS[3]]; + const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); await expect(tx) .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") - .withArgs([PUBKEYS[0], PUBKEYS[1], PUBKEYS[2]]); + .withArgs("0x" + concatenatedPubKeys); }); it("someone submitted exit report data and triggered exit again", async () => { const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1], { value: 2 }); - await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs([PUBKEYS[0], PUBKEYS[1]]); + const pubkeys = [PUBKEYS[0], PUBKEYS[1]]; + const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); + await expect(tx) + .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") + .withArgs("0x" + concatenatedPubKeys); + }); + + it("someone triggered exit on unpacked key", async () => { + await expect(oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 2, 4], { value: 3 })) + .to.be.revertedWithCustomError(oracle, "KeyWasNotUnpacked") + .withArgs(4, 3); + }); + + it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { + const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 3], { value: 3 }); + + const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; + const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); + await expect(tx) + .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") + .withArgs("0x" + concatenatedPubKeys); }); }); From 228550217e8286df3069991cab2cbb95d4943c2b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 20 Feb 2025 14:28:11 +0400 Subject: [PATCH 036/405] fix: hash ReportData.data separatly --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 40 +--------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 74 ++++++++++++++----- ...ator-exit-bus-oracle.accessControl.test.ts | 35 +++------ .../validator-exit-bus-oracle.gas.test.ts | 33 +++------ ...alidator-exit-bus-oracle.happyPath.test.ts | 39 ++++------ ...r-exit-bus-oracle.submitReportData.test.ts | 66 +++++++---------- ...t-bus-oracle.triggerExitHashVerify.test.ts | 42 ++++------- 7 files changed, 139 insertions(+), 190 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 7bf117c1ee..866cf93ffc 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -20,21 +20,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); error ZeroAddress(); error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); - - /// Part of report data - struct ExitRequestData { - /// @dev Total number of validator exit requests in this report. Must not be greater - /// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport. - uint256 requestsCount; - - /// @dev Format of the validator exit requests data. Currently, only the - /// DATA_FORMAT_LIST=1 is supported. - uint256 dataFormat; - - /// @dev Validator exit requests data. Can differ based on the data format, - /// see the constant defining a specific data format below for more info. - bytes data; - } + error TriggerableWithdrawalRefundFailed(); // TODO: make type optimization struct DeliveryHistory { @@ -55,24 +41,6 @@ contract ValidatorsExitBus is AccessControlEnumerable { DeliveryHistory[] deliverHistory; } - /// @notice The list format of the validator exit requests data. Used when all - /// requests fit into a single transaction. - /// - /// Each validator exit request is described by the following 64-byte array: - /// - /// MSB <------------------------------------------------------- LSB - /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | - /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | - /// - /// All requests are tightly packed into a byte array where requests follow - /// one another without any separator or padding, and passed to the `data` - /// field of the report structure. - /// - /// Requests must be sorted in the ascending order by the following compound - /// key: (moduleId, nodeOpId, validatorIndex). - /// - uint256 public constant DATA_FORMAT_LIST = 1; - /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; @@ -94,8 +62,8 @@ contract ValidatorsExitBus is AccessControlEnumerable { LOCATOR_CONTRACT_POSITION.setStorageAddress(addr); } - function triggerExitHashVerify(ExitRequestData calldata exitRequestData, uint256[] calldata keyIndexes) external payable { - bytes32 dataHash = keccak256(abi.encode(exitRequestData)); + function triggerExitHashVerify(bytes calldata data, uint256[] calldata keyIndexes) external payable { + bytes32 dataHash = keccak256(abi.encode(data)); RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash]; address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); @@ -111,7 +79,6 @@ contract ValidatorsExitBus is AccessControlEnumerable { uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; - bytes calldata data = exitRequestData.data; bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); for (uint256 i = 0; i < keyIndexes.length; i++) { @@ -131,6 +98,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { if (refund > 0) { (bool success, ) = msg.sender.call{value: refund}(""); require(success, "Refund failed"); + revert TriggerableWithdrawalRefundFailed(); } } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 18424175ea..ffcb63a602 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -166,9 +166,38 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus /// /// Requests data /// - ExitRequestData exitRequestData; + + /// @dev Total number of validator exit requests in this report. Must not be greater + /// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport. + uint256 requestsCount; + + /// @dev Format of the validator exit requests data. Currently, only the + /// DATA_FORMAT_LIST=1 is supported. + uint256 dataFormat; + + /// @dev Validator exit requests data. Can differ based on the data format, + /// see the constant defining a specific data format below for more info. + bytes data; } + /// @notice The list format of the validator exit requests data. Used when all + /// requests fit into a single transaction. + /// + /// Each validator exit request is described by the following 64-byte array: + /// + /// MSB <------------------------------------------------------- LSB + /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | + /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | + /// + /// All requests are tightly packed into a byte array where requests follow + /// one another without any separator or padding, and passed to the `data` + /// field of the report structure. + /// + /// Requests must be sorted in the ascending order by the following compound + /// key: (moduleId, nodeOpId, validatorIndex). + /// + uint256 public constant DATA_FORMAT_LIST = 1; + /// @notice Submits report data for processing. /// /// @param data The data. See the `ReportData` structure's docs for details. @@ -190,12 +219,21 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus { _checkMsgSenderIsAllowedToSubmitData(); _checkContractVersion(contractVersion); - bytes32 exitRequestDataHash = keccak256(abi.encode(data.exitRequestData)); + bytes32 dataHash = keccak256(abi.encode(data.data)); // it's a waste of gas to copy the whole calldata into mem but seems there's no way around - _checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data.consensusVersion, data.refSlot, exitRequestDataHash))); + bytes32 reportDataHash = keccak256( + abi.encode( + data.consensusVersion, + data.refSlot, + data.requestsCount, + data.dataFormat, + dataHash + ) + ); + _checkConsensusData(data.refSlot, data.consensusVersion, reportDataHash); _startProcessing(); _handleConsensusReportData(data); - _storeOracleExitRequestHash(exitRequestDataHash, data, contractVersion); + _storeOracleExitRequestHash(dataHash, data, contractVersion); } /// @notice Returns the total number of validator exit requests ever processed @@ -304,37 +342,37 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus } function _handleConsensusReportData(ReportData calldata data) internal { - if (data.exitRequestData.dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(data.exitRequestData.dataFormat); + if (data.dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(data.dataFormat); } - if (data.exitRequestData.data.length % PACKED_REQUEST_LENGTH != 0) { + if (data.data.length % PACKED_REQUEST_LENGTH != 0) { revert InvalidRequestsDataLength(); } // TODO: next iteration will check ref slot deliveredReportAmount IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) - .checkExitBusOracleReport(data.exitRequestData.requestsCount); + .checkExitBusOracleReport(data.requestsCount); - if (data.exitRequestData.data.length / PACKED_REQUEST_LENGTH != data.exitRequestData.requestsCount) { + if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); } - _processExitRequestsList(data.exitRequestData.data); + _processExitRequestsList(data.data); _storageDataProcessingState().value = DataProcessingState({ refSlot: data.refSlot.toUint64(), - requestsCount: data.exitRequestData.requestsCount.toUint64(), - requestsProcessed: data.exitRequestData.requestsCount.toUint64(), + requestsCount: data.requestsCount.toUint64(), + requestsProcessed: data.requestsCount.toUint64(), dataFormat: uint16(DATA_FORMAT_LIST) }); - if (data.exitRequestData.requestsCount == 0) { + if (data.requestsCount == 0) { return; } TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256( - TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.exitRequestData.requestsCount + TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.requestsCount ); } @@ -417,17 +455,17 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus } function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { - if (report.exitRequestData.requestsCount == 0) { + if (report.requestsCount == 0) { return; } mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); RequestStatus storage request = hashes[exitRequestHash]; - request.totalItemsCount = report.exitRequestData.requestsCount; - request.deliveredItemsCount = report.exitRequestData.requestsCount; + request.totalItemsCount = report.requestsCount; + request.deliveredItemsCount = report.requestsCount; request.contractVersion = contractVersion; - request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.exitRequestData.requestsCount - 1})); + request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.requestsCount - 1})); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index 8498ac4c21..465bb4768c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -42,29 +42,22 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { valIndex: number; valPubkey: string; } - interface ExitRequestData { - requestsCount: number; - dataFormat: number; - data: string; - } interface ReportFields { consensusVersion: bigint; refSlot: bigint; - exitRequestData: ExitRequestData; + requestsCount: number; + dataFormat: number; + data: string; } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const exitRequestItems = [ - items.exitRequestData.requestsCount, - items.exitRequestData.dataFormat, - items.exitRequestData.data, - ]; - const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); - const dataHash = ethers.keccak256(exitRequestData); - const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); - return ethers.keccak256(data); + const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportDataHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ); + return reportDataHash; }; const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { @@ -101,16 +94,12 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, refSlot: refSlot, - exitRequestData: { - dataFormat: DATA_FORMAT_LIST, - requestsCount: exitRequests.length, - data: encodeExitRequestsDataList(exitRequests), - }, + dataFormat: DATA_FORMAT_LIST, + requestsCount: exitRequests.length, + data: encodeExitRequestsDataList(exitRequests), }; - // reportItems = getValidatorsExitBusReportDataItems(reportFields); reportHash = calcValidatorsExitBusReportDataHash(reportFields); - await consensus.connect(member1).submitReport(refSlot, reportHash, CONSENSUS_VERSION); await consensus.connect(member3).submitReport(refSlot, reportHash, CONSENSUS_VERSION); }; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index 07545aff09..09f0e136f5 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -49,29 +49,22 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { valIndex: number; valPubkey: string; } - interface ExitRequestData { - requestsCount: number; - dataFormat: number; - data: string; - } interface ReportFields { consensusVersion: bigint; refSlot: bigint; - exitRequestData: ExitRequestData; + requestsCount: number; + dataFormat: number; + data: string; } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const exitRequestItems = [ - items.exitRequestData.requestsCount, - items.exitRequestData.dataFormat, - items.exitRequestData.data, - ]; - const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); - const dataHash = ethers.keccak256(exitRequestData); - const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); - return ethers.keccak256(data); + const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportDataHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ); + return reportDataHash; }; const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { @@ -177,11 +170,9 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, refSlot: refSlot, - exitRequestData: { - requestsCount: exitRequests.requests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests.requests), - }, + requestsCount: exitRequests.requests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests.requests), }; reportHash = calcValidatorsExitBusReportDataHash(reportFields); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 8cab0dac2a..1fd79f1b49 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -50,29 +50,21 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { valPubkey: string; } - interface ExitRequestData { - requestsCount: number; - dataFormat: number; - data: string; - } - interface ReportFields { consensusVersion: bigint; refSlot: bigint; - exitRequestData: ExitRequestData; + requestsCount: number; + dataFormat: number; + data: string; } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const exitRequestItems = [ - items.exitRequestData.requestsCount, - items.exitRequestData.dataFormat, - items.exitRequestData.data, - ]; - const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); - const dataHash = ethers.keccak256(exitRequestData); - const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); - return ethers.keccak256(data); + const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportDataHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ); + return reportDataHash; }; const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { @@ -156,11 +148,9 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, refSlot: refSlot, - exitRequestData: { - requestsCount: exitRequests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }, + requestsCount: exitRequests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), }; reportHash = calcValidatorsExitBusReportDataHash(reportFields); @@ -215,10 +205,7 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { it("a data not matching the consensus hash cannot be submitted", async () => { const invalidReport = { ...reportFields, - exitRequestData: { - ...reportFields.exitRequestData, - requestsCount: reportFields.exitRequestData.requestsCount + 1, - }, + requestsCount: reportFields.requestsCount + 1, }; const invalidReportHash = calcValidatorsExitBusReportDataHash(invalidReport); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 53d45f301e..f150ace4a2 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -47,29 +47,22 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { valIndex: number; valPubkey: string; } - interface ExitRequestData { - requestsCount: number; - dataFormat: number; - data: string; - } interface ReportFields { consensusVersion: bigint; refSlot: bigint; - exitRequestData: ExitRequestData; + requestsCount: number; + dataFormat: number; + data: string; } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const exitRequestItems = [ - items.exitRequestData.requestsCount, - items.exitRequestData.dataFormat, - items.exitRequestData.data, - ]; - const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); - const dataHash = ethers.keccak256(exitRequestData); - const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); - return ethers.keccak256(data); + const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportDataHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ); + return reportDataHash; }; const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { @@ -91,20 +84,16 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const prepareReportAndSubmitHash = async ( requests = [{ moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }], - options?: Partial> & { - exitRequestData?: Partial; - }, + options = { reportFields: {} }, ) => { const { refSlot } = await consensus.getCurrentFrame(); const reportData = { - consensusVersion: options?.consensusVersion || CONSENSUS_VERSION, - refSlot: options?.refSlot || refSlot, - exitRequestData: { - requestsCount: requests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(requests), - ...options?.exitRequestData, - }, + consensusVersion: CONSENSUS_VERSION, + dataFormat: DATA_FORMAT_LIST, + refSlot, + requestsCount: requests.length, + data: encodeExitRequestsDataList(requests), + ...options.reportFields, }; const reportHash = calcValidatorsExitBusReportDataHash(reportData); @@ -199,7 +188,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const dataFormatUnsupported = 0; const { reportData } = await prepareReportAndSubmitHash( [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], - { exitRequestData: { dataFormat: dataFormatUnsupported } }, + { reportFields: { dataFormat: dataFormatUnsupported } }, ); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) @@ -211,7 +200,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const dataFormatUnsupported = 2; const { reportData } = await prepareReportAndSubmitHash( [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], - { exitRequestData: { dataFormat: dataFormatUnsupported } }, + { reportFields: { dataFormat: dataFormatUnsupported } }, ); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) @@ -232,8 +221,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const { refSlot } = await consensus.getCurrentFrame(); const exitRequests = [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }]; const { reportData } = await prepareReportAndSubmitHash(exitRequests, { - exitRequestData: { data: encodeExitRequestsDataList(exitRequests) + "aaaaaaaaaaaaaaaaaa" }, - refSlot, + reportFields: { refSlot, data: encodeExitRequestsDataList(exitRequests) + "aaaaaaaaaaaaaaaaaa" }, }); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( @@ -248,10 +236,10 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const data = encodeExitRequestsDataList(exitRequests); const { reportData } = await prepareReportAndSubmitHash(exitRequests, { - exitRequestData: { + reportFields: { data: data.slice(0, data.length - 18), + refSlot, }, - refSlot, }); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( @@ -301,7 +289,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { it("reverts if requestsCount does not match with encoded data size", async () => { const { reportData } = await prepareReportAndSubmitHash( [{ moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }], - { exitRequestData: { requestsCount: 2 } }, + { reportFields: { requestsCount: 2 } }, ); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( @@ -519,7 +507,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); it("SUBMIT_DATA_ROLE is allowed", async () => { - oracle.grantRole(await oracle.SUBMIT_DATA_ROLE(), stranger, { from: admin }); + await oracle.grantRole(await oracle.SUBMIT_DATA_ROLE(), stranger, { from: admin }); await consensus.advanceTimeToNextFrameStart(); const { reportData } = await prepareReportAndSubmitHash(); await oracle.connect(stranger).submitReportData(reportData, oracleVersion); @@ -572,11 +560,9 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const reportData = { consensusVersion: CONSENSUS_VERSION, refSlot, - exitRequestData: { - requestsCount: requests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(newRequests), - }, + requestsCount: requests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(newRequests), }; const changedReportHash = calcValidatorsExitBusReportDataHash(reportData); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index c86498c6ff..f8790c5ab7 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -49,29 +49,21 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { valPubkey: string; } - interface ExitRequestData { - requestsCount: number; - dataFormat: number; - data: string; - } - interface ReportFields { consensusVersion: bigint; refSlot: bigint; - exitRequestData: ExitRequestData; + requestsCount: number; + dataFormat: number; + data: string; } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const exitRequestItems = [ - items.exitRequestData.requestsCount, - items.exitRequestData.dataFormat, - items.exitRequestData.data, - ]; - const exitRequestData = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes)"], [exitRequestItems]); - const dataHash = ethers.keccak256(exitRequestData); - const oracleReportItems = [items.consensusVersion, items.refSlot, dataHash]; - const data = ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,bytes32)"], [oracleReportItems]); - return ethers.keccak256(data); + const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportDataHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ); + return reportDataHash; }; const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { @@ -156,11 +148,9 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { reportFields = { consensusVersion: CONSENSUS_VERSION, refSlot: refSlot, - exitRequestData: { - requestsCount: exitRequests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }, + requestsCount: exitRequests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), }; reportHash = calcValidatorsExitBusReportDataHash(reportFields); @@ -229,7 +219,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("someone submitted exit report data and triggered exit", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 2, 3], { value: 4 }); + const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1, 2, 3], { value: 4 }); const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[2], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); @@ -239,7 +229,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("someone submitted exit report data and triggered exit again", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1], { value: 2 }); + const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1], { value: 2 }); const pubkeys = [PUBKEYS[0], PUBKEYS[1]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); @@ -249,13 +239,13 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("someone triggered exit on unpacked key", async () => { - await expect(oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 2, 4], { value: 3 })) + await expect(oracle.triggerExitHashVerify(reportFields.data, [0, 2, 4], { value: 3 })) .to.be.revertedWithCustomError(oracle, "KeyWasNotUnpacked") .withArgs(4, 3); }); it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.exitRequestData, [0, 1, 3], { value: 3 }); + const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1, 3], { value: 3 }); const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); From ed8d9fd3f34053621cfad804ac1f3df6d2be6ba8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 21 Feb 2025 00:03:13 +0400 Subject: [PATCH 037/405] fix: add constructor to VEB --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 5 +++-- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 866cf93ffc..d5046b514d 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -38,6 +38,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { uint256 deliveredItemsCount; // Vebo contract version at the time of hash submittion uint256 contractVersion; + DeliveryHistory[] deliverHistory; } @@ -52,7 +53,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { bytes32 private constant LOCATOR_CONTRACT_POSITION = keccak256("lido.ValidatorsExitBus.locatorContract"); - function _initialize_v2(address locatorAddr) internal { + constructor(address locatorAddr) { _setLocatorAddress(locatorAddr); } @@ -69,7 +70,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); uint256 minFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - uint requestsFee = keyIndexes.length * minFee; + uint256 requestsFee = keyIndexes.length * minFee; if (msg.value < requestsFee) { revert FeeNotEnough(minFee, keyIndexes.length, msg.value); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index ffcb63a602..8fa11514a1 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -93,6 +93,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus constructor(uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator) BaseOracle(secondsPerSlot, genesisTime) + ValidatorsExitBus(lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); } @@ -112,7 +113,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus function finalizeUpgrade_v2() external { _updateContractVersion(2); - _initialize_v2(address(LOCATOR)); } /// @notice Resume accepting validator exit requests From cd9523ef24b0a54426d4081145550abd9bfe52d6 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 21 Feb 2025 15:51:39 +0400 Subject: [PATCH 038/405] fix: separate tw hash from oracle consensus hash --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 6 ++++-- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 10 +--------- .../validator-exit-bus-oracle.accessControl.test.ts | 5 ++--- .../oracle/validator-exit-bus-oracle.gas.test.ts | 5 ++--- .../validator-exit-bus-oracle.happyPath.test.ts | 5 ++--- ...validator-exit-bus-oracle.submitReportData.test.ts | 5 ++--- ...ator-exit-bus-oracle.triggerExitHashVerify.test.ts | 11 ++++++++--- 7 files changed, 21 insertions(+), 26 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d5046b514d..c1159e12b8 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -98,8 +98,10 @@ contract ValidatorsExitBus is AccessControlEnumerable { if (refund > 0) { (bool success, ) = msg.sender.call{value: refund}(""); - require(success, "Refund failed"); - revert TriggerableWithdrawalRefundFailed(); + + if (!success) { + revert TriggerableWithdrawalRefundFailed(); + } } } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 8fa11514a1..2c16f33939 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -221,15 +221,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus _checkContractVersion(contractVersion); bytes32 dataHash = keccak256(abi.encode(data.data)); // it's a waste of gas to copy the whole calldata into mem but seems there's no way around - bytes32 reportDataHash = keccak256( - abi.encode( - data.consensusVersion, - data.refSlot, - data.requestsCount, - data.dataFormat, - dataHash - ) - ); + bytes32 reportDataHash = keccak256(abi.encode(data)); _checkConsensusData(data.refSlot, data.consensusVersion, reportDataHash); _startProcessing(); _handleConsensusReportData(data); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index 465bb4768c..507ea6ba4e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -52,10 +52,9 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); - const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), ); return reportDataHash; }; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index 09f0e136f5..8a2b4cbb6e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -59,10 +59,9 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); - const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), ); return reportDataHash; }; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 1fd79f1b49..7e51d5d920 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -59,10 +59,9 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); - const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), ); return reportDataHash; }; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index f150ace4a2..d3b0c1bdbf 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -57,10 +57,9 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); - const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), ); return reportDataHash; }; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index f8790c5ab7..a0a9cb0418 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -58,10 +58,9 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { } const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const dataHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes"], [items.data])); - const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, dataHash]; + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes32)"], [reportData]), + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), ); return reportDataHash; }; @@ -253,4 +252,10 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") .withArgs("0x" + concatenatedPubKeys); }); + + it("Not enough fee", async () => { + await expect(oracle.triggerExitHashVerify(reportFields.data, [0, 1], { value: 1 })) + .to.be.revertedWithCustomError(oracle, "FeeNotEnough") + .withArgs(1, 2, 1); + }); }); From 710e0c3348fc15234a51f9007774d59fa757362b Mon Sep 17 00:00:00 2001 From: Eddort Date: Sat, 22 Feb 2025 10:19:31 +0100 Subject: [PATCH 039/405] feat: config for holesky-fork --- hardhat.config.ts | 3 +++ scripts/deploy-tw.sh | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100755 scripts/deploy-tw.sh diff --git a/hardhat.config.ts b/hardhat.config.ts index df23c8a245..3db7e4ca29 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -33,6 +33,9 @@ const config: HardhatUserConfig = { url: process.env.LOCAL_RPC_URL || RPC_URL, accounts: [process.env.LOCAL_DEVNET_PK || ZERO_PK], }, + "holesky": { + url: process.env.LOCAL_RPC_URL || RPC_URL, + }, "mainnet-fork": { url: process.env.MAINNET_RPC_URL || RPC_URL, timeout: 20 * 60 * 1000, // 20 minutes diff --git a/scripts/deploy-tw.sh b/scripts/deploy-tw.sh new file mode 100755 index 0000000000..6887510bd7 --- /dev/null +++ b/scripts/deploy-tw.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e +u +set -o pipefail + +export NETWORK=holesky +export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise +export SLOTS_PER_EPOCH=32 +export GENESIS_TIME=1639659600 # just some time +# export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" +# export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" + +export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of default mnemonic "test test ..." +export GAS_PRIORITY_FEE=1 +export GAS_MAX_FEE=100 + +export NETWORK_STATE_FILE="deployed-holesky.json" +# export NETWORK_STATE_DEFAULTS_FILE="scripts/scratch/deployed-testnet-defaults.json" + + +# Need this to get sure the last transactions are mined +yarn hardhat --network $NETWORK run scripts/triggerable-withdrawals/tw-deploy.ts + From e76e0d87e79345f4529983334c86f45d573f3e7b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 23 Feb 2025 23:16:16 +0400 Subject: [PATCH 040/405] fix: added event for hash store, check key exist --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 22 +++++++++++-- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 8 ++++- ...r-exit-bus-oracle.submitReportData.test.ts | 5 +++ ...t-bus-oracle.triggerExitHashVerify.test.ts | 32 +++++++++---------- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index c1159e12b8..b19c11dcaf 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -21,6 +21,14 @@ contract ValidatorsExitBus is AccessControlEnumerable { error ZeroAddress(); error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); error TriggerableWithdrawalRefundFailed(); + error ExitHashWasNotSubmitted(); + error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + + /// @dev Events + event MadeRefund( + address sender, + uint256 refundValue + ); // TODO: make type optimization struct DeliveryHistory { @@ -64,9 +72,13 @@ contract ValidatorsExitBus is AccessControlEnumerable { } function triggerExitHashVerify(bytes calldata data, uint256[] calldata keyIndexes) external payable { - bytes32 dataHash = keccak256(abi.encode(data)); + bytes32 dataHash = keccak256(data); RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash]; + if (requestStatus.contractVersion == 0) { + revert ExitHashWasNotSubmitted(); + } + address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); uint256 minFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); @@ -83,14 +95,18 @@ contract ValidatorsExitBus is AccessControlEnumerable { bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); for (uint256 i = 0; i < keyIndexes.length; i++) { + if (keyIndexes[i] >= requestStatus.totalItemsCount) { + revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); + } + if (keyIndexes[i] > lastDeliveredKeyIndex) { revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex); } + uint256 requestOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16; for (uint256 j = 0; j < PUBLIC_KEY_LENGTH; j++) { pubkeys[i * PUBLIC_KEY_LENGTH + j] = data[requestOffset + j]; - } } @@ -102,6 +118,8 @@ contract ValidatorsExitBus is AccessControlEnumerable { if (!success) { revert TriggerableWithdrawalRefundFailed(); } + + emit MadeRefund(msg.sender, refund); } } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 2c16f33939..a4cafec148 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -51,6 +51,10 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus uint256 requestsCount ); + event StoredOracleTWExitRequestHash( + bytes32 exitRequestHash + ); + struct DataProcessingState { uint64 refSlot; uint64 requestsCount; @@ -219,7 +223,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus { _checkMsgSenderIsAllowedToSubmitData(); _checkContractVersion(contractVersion); - bytes32 dataHash = keccak256(abi.encode(data.data)); + bytes32 dataHash = keccak256(data.data); // it's a waste of gas to copy the whole calldata into mem but seems there's no way around bytes32 reportDataHash = keccak256(abi.encode(data)); _checkConsensusData(data.refSlot, data.consensusVersion, reportDataHash); @@ -458,6 +462,8 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus request.deliveredItemsCount = report.requestsCount; request.contractVersion = contractVersion; request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.requestsCount - 1})); + + emit StoredOracleTWExitRequestHash(exitRequestHash); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index d3b0c1bdbf..ce7a9e8730 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -325,6 +325,11 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { await expect(tx) .to.emit(oracle, "ValidatorExitRequest") .withArgs(requests[1].moduleId, requests[1].nodeOpId, requests[1].valIndex, requests[1].valPubkey, timestamp); + + const data = encodeExitRequestsDataList(requests); + const reportDataHash = ethers.keccak256(data); + + await expect(tx).to.emit(oracle, "StoredOracleTWExitRequestHash").withArgs(reportDataHash); }); it("updates processing state", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index a0a9cb0418..bf069db187 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -227,22 +227,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { .withArgs("0x" + concatenatedPubKeys); }); - it("someone submitted exit report data and triggered exit again", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1], { value: 2 }); - - const pubkeys = [PUBKEYS[0], PUBKEYS[1]]; - const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); - await expect(tx) - .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") - .withArgs("0x" + concatenatedPubKeys); - }); - - it("someone triggered exit on unpacked key", async () => { - await expect(oracle.triggerExitHashVerify(reportFields.data, [0, 2, 4], { value: 3 })) - .to.be.revertedWithCustomError(oracle, "KeyWasNotUnpacked") - .withArgs(4, 3); - }); - it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1, 3], { value: 3 }); @@ -258,4 +242,20 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { .to.be.revertedWithCustomError(oracle, "FeeNotEnough") .withArgs(1, 2, 1); }); + + it("Should trigger withdrawals only for validators that were requested for voluntary exit by trusted entities earlier", async () => { + await expect( + oracle.triggerExitHashVerify( + "0x0000030000000000000000000000005a894d712b61ee6d5da473f87d9c8175c4022fd05a8255b6713dc75388b099a85514ceca78a52b9122d09aecda9010c047", + [0], + { value: 2 }, + ), + ).to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted"); + }); + + it("Requested index out of range", async () => { + await expect(oracle.triggerExitHashVerify(reportFields.data, [5], { value: 2 })) + .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") + .withArgs(5, 4); + }); }); From 65c89e63de5b4fae01d9f1bc5714fe80732e0970 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 24 Feb 2025 17:14:49 +0400 Subject: [PATCH 041/405] fix: add event for debug --- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index a4cafec148..fb23025ea0 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -55,6 +55,10 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus bytes32 exitRequestHash ); + event StoreOracleExitRequestHashStart( + bytes32 exitRequestHash + ); + struct DataProcessingState { uint64 refSlot; uint64 requestsCount; @@ -451,6 +455,8 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus } function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { + emit StoreOracleExitRequestHashStart(exitRequestHash); + if (report.requestsCount == 0) { return; } From ad2158dd35825197112937bd5dfdd7ac6550272a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 24 Feb 2025 19:49:03 +0400 Subject: [PATCH 042/405] fix: use slice --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index b19c11dcaf..cfe00610d7 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -92,7 +92,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; - bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); + bytes memory pubkeys; for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { @@ -104,10 +104,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { } uint256 requestOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16; - - for (uint256 j = 0; j < PUBLIC_KEY_LENGTH; j++) { - pubkeys[i * PUBLIC_KEY_LENGTH + j] = data[requestOffset + j]; - } + pubkeys = bytes.concat(pubkeys, data[requestOffset:requestOffset + PUBLIC_KEY_LENGTH]); } IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: requestsFee}(pubkeys); From 1fd1b703d4501d9a5bab1541fad1dd482f6562e0 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 25 Feb 2025 14:54:27 +0400 Subject: [PATCH 043/405] fix: set locator address in finalizeUpgrade_v2 --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 ---- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index cfe00610d7..25516bec1f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -61,10 +61,6 @@ contract ValidatorsExitBus is AccessControlEnumerable { bytes32 private constant LOCATOR_CONTRACT_POSITION = keccak256("lido.ValidatorsExitBus.locatorContract"); - constructor(address locatorAddr) { - _setLocatorAddress(locatorAddr); - } - function _setLocatorAddress(address addr) internal { if (addr == address(0)) revert ZeroAddress(); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index fb23025ea0..fb47778736 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -101,7 +101,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus constructor(uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator) BaseOracle(secondsPerSlot, genesisTime) - ValidatorsExitBus(lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); } @@ -121,6 +120,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus function finalizeUpgrade_v2() external { _updateContractVersion(2); + _setLocatorAddress(address(LOCATOR)); } /// @notice Resume accepting validator exit requests From af497f368b858cd1ef2c08856b047b8331486446 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 25 Feb 2025 15:12:52 +0400 Subject: [PATCH 044/405] fix: removed debug event --- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index fb47778736..3185870e02 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -455,8 +455,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus } function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { - emit StoreOracleExitRequestHashStart(exitRequestHash); - if (report.requestsCount == 0) { return; } From 7163ee256eaf8c02218601055a89dea42411e1b8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 3 Mar 2025 13:00:13 +0400 Subject: [PATCH 045/405] fix: add data_format in hash for tw --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 90 ++++++++++++++----- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 23 ++--- ...untingOracle.submitReportExtraData.test.ts | 2 +- ...r-exit-bus-oracle.submitReportData.test.ts | 6 +- ...t-bus-oracle.triggerExitHashVerify.test.ts | 34 +++++-- 5 files changed, 105 insertions(+), 50 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 25516bec1f..49567b3963 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -17,9 +17,9 @@ contract ValidatorsExitBus is AccessControlEnumerable { using UnstructuredStorage for bytes32; /// @dev Errors - error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); + error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); error ZeroAddress(); - error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue); + error InsufficientPayment(uint256 withdrawalFeePerRequest, uint256 requestCount, uint256 msgValue); error TriggerableWithdrawalRefundFailed(); error ExitHashWasNotSubmitted(); error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); @@ -29,18 +29,18 @@ contract ValidatorsExitBus is AccessControlEnumerable { address sender, uint256 refundValue ); + event StoredExitRequestHash( + bytes32 exitRequestHash + ); - // TODO: make type optimization struct DeliveryHistory { - uint256 blockNumber; /// @dev Key index in exit request array uint256 lastDeliveredKeyIndex; - - // TODO: timestamp + /// @dev Block timestamp + uint256 timestamp; } - // TODO: make type optimization struct RequestStatus { - // Total items count in report (by default type(uint32).max, update on first report unpack) + // Total items count in report (by default type(uint32).max, update on first report delivery) uint256 totalItemsCount; // Total processed items in report (by default 0) uint256 deliveredItemsCount; @@ -49,6 +49,10 @@ contract ValidatorsExitBus is AccessControlEnumerable { DeliveryHistory[] deliverHistory; } + struct ExitRequestData { + bytes data; + uint256 dataFormat; + } /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; @@ -67,9 +71,12 @@ contract ValidatorsExitBus is AccessControlEnumerable { LOCATOR_CONTRACT_POSITION.setStorageAddress(addr); } - function triggerExitHashVerify(bytes calldata data, uint256[] calldata keyIndexes) external payable { - bytes32 dataHash = keccak256(data); - RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash]; + /// @notice Triggers exits on the EL via the Withdrawal Vault contract after + /// @dev This function verifies that the hash of the provided exit request data exists in storage + // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. + function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable { + RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(request.data, request.dataFormat))]; + bytes calldata data = request.data; if (requestStatus.contractVersion == 0) { revert ExitHashWasNotSubmitted(); @@ -77,18 +84,17 @@ contract ValidatorsExitBus is AccessControlEnumerable { address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); - uint256 minFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - uint256 requestsFee = keyIndexes.length * minFee; + uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - if (msg.value < requestsFee) { - revert FeeNotEnough(minFee, keyIndexes.length, msg.value); + if (msg.value < keyIndexes.length * withdrawalFee ) { + revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); } - uint256 refund = msg.value - requestsFee; + uint256 prevBalance = address(this).balance - msg.value; uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; - bytes memory pubkeys; + bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { @@ -96,14 +102,29 @@ contract ValidatorsExitBus is AccessControlEnumerable { } if (keyIndexes[i] > lastDeliveredKeyIndex) { - revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex); + revert KeyWasNotDelivered(keyIndexes[i], lastDeliveredKeyIndex); } - uint256 requestOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16; - pubkeys = bytes.concat(pubkeys, data[requestOffset:requestOffset + PUBLIC_KEY_LENGTH]); + /// + /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | + /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | + /// 16 bytes - part without pubkey + uint256 requestPublicKeyOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16; + uint256 destOffset = i * PUBLIC_KEY_LENGTH; + + assembly { + let dest := add(pubkeys, add(32, destOffset)) + calldatacopy( + dest, + add(data.offset, requestPublicKeyOffset), + PUBLIC_KEY_LENGTH + ) + } } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: requestsFee}(pubkeys); + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}(pubkeys); + + uint256 refund = msg.value - keyIndexes.length * withdrawalFee; if (refund > 0) { (bool success, ) = msg.sender.call{value: refund}(""); @@ -115,6 +136,33 @@ contract ValidatorsExitBus is AccessControlEnumerable { emit MadeRefund(msg.sender, refund); } + assert(address(this).balance == prevBalance); + } + + function _storeExitRequestHash( + bytes32 exitRequestHash, + uint256 totalItemsCount, + uint256 deliveredItemsCount, + uint256 contractVersion, + uint256 lastDeliveredKeyIndex + ) internal { + if (deliveredItemsCount == 0) { + return; + } + + mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); + + RequestStatus storage request = hashes[exitRequestHash]; + + request.totalItemsCount = totalItemsCount; + request.deliveredItemsCount = deliveredItemsCount; + request.contractVersion = contractVersion; + request.deliverHistory.push(DeliveryHistory({ + timestamp: block.timestamp, + lastDeliveredKeyIndex: lastDeliveredKeyIndex + })); + + emit StoredExitRequestHash(exitRequestHash); } /// Storage helpers diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 3185870e02..d1a2f07951 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -51,10 +51,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus uint256 requestsCount ); - event StoredOracleTWExitRequestHash( - bytes32 exitRequestHash - ); - event StoreOracleExitRequestHashStart( bytes32 exitRequestHash ); @@ -227,13 +223,13 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus { _checkMsgSenderIsAllowedToSubmitData(); _checkContractVersion(contractVersion); - bytes32 dataHash = keccak256(data.data); + bytes32 dataHash = keccak256(abi.encode(data.data, data.dataFormat)); // it's a waste of gas to copy the whole calldata into mem but seems there's no way around bytes32 reportDataHash = keccak256(abi.encode(data)); _checkConsensusData(data.refSlot, data.consensusVersion, reportDataHash); _startProcessing(); _handleConsensusReportData(data); - _storeOracleExitRequestHash(dataHash, data, contractVersion); + _storeOracleExitRequestHash(dataHash, data.requestsCount, contractVersion); } /// @notice Returns the total number of validator exit requests ever processed @@ -454,20 +450,11 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus return (moduleId << 40) | nodeOpId; } - function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal { - if (report.requestsCount == 0) { + function _storeOracleExitRequestHash(bytes32 exitRequestHash, uint256 requestsCount, uint256 contractVersion) internal { + if (requestsCount == 0) { return; } - - mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); - - RequestStatus storage request = hashes[exitRequestHash]; - request.totalItemsCount = report.requestsCount; - request.deliveredItemsCount = report.requestsCount; - request.contractVersion = contractVersion; - request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.requestsCount - 1})); - - emit StoredOracleTWExitRequestHash(exitRequestHash); + _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, requestsCount - 1); } /// diff --git a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts index 89351ca035..e034a73e64 100644 --- a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts +++ b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts @@ -488,7 +488,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { .withArgs(extraDataChunkHashes[1], extraDataChunkHashes[0]); const tx2 = await oracleMemberSubmitExtraData(extraDataChunks[1]); - await expect(tx2).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, anyValue, anyValue); + await expect(tx2).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, , anyValue); }); it("pass successfully if data hash matches", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index ce7a9e8730..5051af732e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -327,9 +327,11 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { .withArgs(requests[1].moduleId, requests[1].nodeOpId, requests[1].valIndex, requests[1].valPubkey, timestamp); const data = encodeExitRequestsDataList(requests); - const reportDataHash = ethers.keccak256(data); - await expect(tx).to.emit(oracle, "StoredOracleTWExitRequestHash").withArgs(reportDataHash); + const encodedData = ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [data, reportData.dataFormat]); + const reportDataHash = ethers.keccak256(encodedData); + + await expect(tx).to.emit(oracle, "StoredExitRequestHash").withArgs(reportDataHash); }); it("updates processing state", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index bf069db187..39b8e95b0d 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { ZeroHash } from "ethers"; import { ethers } from "hardhat"; +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; @@ -25,7 +26,7 @@ const PUBKEYS = [ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ]; -describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { +describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; @@ -218,7 +219,11 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("someone submitted exit report data and triggered exit", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1, 2, 3], { value: 4 }); + const tx = await oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 2, 3], + { value: 4 }, + ); const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[2], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); @@ -228,25 +233,36 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { - const tx = await oracle.triggerExitHashVerify(reportFields.data, [0, 1, 3], { value: 3 }); + const tx = await oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { + value: 10, + }); const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); await expect(tx) .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") .withArgs("0x" + concatenatedPubKeys); + + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 7); }); it("Not enough fee", async () => { - await expect(oracle.triggerExitHashVerify(reportFields.data, [0, 1], { value: 1 })) - .to.be.revertedWithCustomError(oracle, "FeeNotEnough") + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1], { + value: 1, + }), + ) + .to.be.revertedWithCustomError(oracle, "InsufficientPayment") .withArgs(1, 2, 1); }); it("Should trigger withdrawals only for validators that were requested for voluntary exit by trusted entities earlier", async () => { await expect( - oracle.triggerExitHashVerify( - "0x0000030000000000000000000000005a894d712b61ee6d5da473f87d9c8175c4022fd05a8255b6713dc75388b099a85514ceca78a52b9122d09aecda9010c047", + oracle.triggerExits( + { + data: "0x0000030000000000000000000000005a894d712b61ee6d5da473f87d9c8175c4022fd05a8255b6713dc75388b099a85514ceca78a52b9122d09aecda9010c047", + dataFormat: reportFields.dataFormat, + }, [0], { value: 2 }, ), @@ -254,7 +270,9 @@ describe("ValidatorsExitBusOracle.sol:triggerExitHashVerify", () => { }); it("Requested index out of range", async () => { - await expect(oracle.triggerExitHashVerify(reportFields.data, [5], { value: 2 })) + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], { value: 2 }), + ) .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") .withArgs(5, 4); }); From 1ba7dc60db5a07374980c949b41800ea506906e4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 3 Mar 2025 13:17:46 +0400 Subject: [PATCH 046/405] fix: unit tests --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 ++-- .../oracle/accountingOracle.submitReportExtraData.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 49567b3963..a84e5f0afd 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -90,7 +90,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); } - uint256 prevBalance = address(this).balance - msg.value; + // uint256 prevBalance = address(this).balance - msg.value; uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; @@ -136,7 +136,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { emit MadeRefund(msg.sender, refund); } - assert(address(this).balance == prevBalance); + // assert(address(this).balance == prevBalance); } function _storeExitRequestHash( diff --git a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts index e034a73e64..89351ca035 100644 --- a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts +++ b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts @@ -488,7 +488,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { .withArgs(extraDataChunkHashes[1], extraDataChunkHashes[0]); const tx2 = await oracleMemberSubmitExtraData(extraDataChunks[1]); - await expect(tx2).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, , anyValue); + await expect(tx2).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, anyValue, anyValue); }); it("pass successfully if data hash matches", async () => { From 3e732bff39e80899ee77a04520c48639042e345b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 3 Mar 2025 13:41:24 +0400 Subject: [PATCH 047/405] fix: vebo balance check --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index a84e5f0afd..5804ba32d9 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -12,7 +12,6 @@ interface IWithdrawalVault { function getWithdrawalRequestFee() external view returns (uint256); } - contract ValidatorsExitBus is AccessControlEnumerable { using UnstructuredStorage for bytes32; @@ -75,6 +74,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { /// @dev This function verifies that the hash of the provided exit request data exists in storage // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable { + uint256 prevBalance = address(this).balance - msg.value; RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(request.data, request.dataFormat))]; bytes calldata data = request.data; @@ -90,8 +90,6 @@ contract ValidatorsExitBus is AccessControlEnumerable { revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); } - // uint256 prevBalance = address(this).balance - msg.value; - uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); @@ -136,7 +134,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { emit MadeRefund(msg.sender, refund); } - // assert(address(this).balance == prevBalance); + assert(address(this).balance == prevBalance); } function _storeExitRequestHash( From 8894a7664017373f84189324bdfb9ec1223b858f Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 19 Mar 2025 13:04:11 +0100 Subject: [PATCH 048/405] feat: ssz porting from csm --- contracts/0.8.25/lib/BeaconTypes.sol | 22 ++ contracts/0.8.25/lib/GIndex.sol | 122 +++++++++ contracts/0.8.25/lib/SSZ.sol | 267 +++++++++++++++++++ test/0.8.25/contracts/Utilities.sol | 93 +++++++ test/0.8.25/lib/GIndex.t.sol | 352 +++++++++++++++++++++++++ test/0.8.25/lib/SSZ.t.sol | 369 +++++++++++++++++++++++++++ 6 files changed, 1225 insertions(+) create mode 100644 contracts/0.8.25/lib/BeaconTypes.sol create mode 100644 contracts/0.8.25/lib/GIndex.sol create mode 100644 contracts/0.8.25/lib/SSZ.sol create mode 100644 test/0.8.25/contracts/Utilities.sol create mode 100644 test/0.8.25/lib/GIndex.t.sol create mode 100644 test/0.8.25/lib/SSZ.t.sol diff --git a/contracts/0.8.25/lib/BeaconTypes.sol b/contracts/0.8.25/lib/BeaconTypes.sol new file mode 100644 index 0000000000..cb48f54b41 --- /dev/null +++ b/contracts/0.8.25/lib/BeaconTypes.sol @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +struct Validator { + bytes pubkey; + bytes32 withdrawalCredentials; + uint64 effectiveBalance; + bool slashed; + uint64 activationEligibilityEpoch; + uint64 activationEpoch; + uint64 exitEpoch; + uint64 withdrawableEpoch; +} +struct BeaconBlockHeader { + uint64 slot; + uint64 proposerIndex; + bytes32 parentRoot; + bytes32 stateRoot; + bytes32 bodyRoot; +} diff --git a/contracts/0.8.25/lib/GIndex.sol b/contracts/0.8.25/lib/GIndex.sol new file mode 100644 index 0000000000..996bb053bc --- /dev/null +++ b/contracts/0.8.25/lib/GIndex.sol @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* + GIndex library from CSM + original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/GIndex.sol +*/ + +pragma solidity 0.8.25; + +type GIndex is bytes32; + +using {isRoot, isParentOf, index, width, shr, shl, concat, unwrap, pow} for GIndex global; + +error IndexOutOfRange(); + +/// @param gI Is a generalized index of a node in a tree. +/// @param p Is a power of a tree level the node belongs to. +/// @return GIndex +function pack(uint256 gI, uint8 p) pure returns (GIndex) { + if (gI > type(uint248).max) { + revert IndexOutOfRange(); + } + + // NOTE: We can consider adding additional metadata like a fork version. + return GIndex.wrap(bytes32((gI << 8) | p)); +} + +function unwrap(GIndex self) pure returns (bytes32) { + return GIndex.unwrap(self); +} + +function isRoot(GIndex self) pure returns (bool) { + return index(self) == 1; +} + +function index(GIndex self) pure returns (uint256) { + return uint256(unwrap(self)) >> 8; +} + +function width(GIndex self) pure returns (uint256) { + return 1 << pow(self); +} + +function pow(GIndex self) pure returns (uint8) { + return uint8(uint256(unwrap(self))); +} + +/// @return Generalized index of the nth neighbor of the node to the right. +function shr(GIndex self, uint256 n) pure returns (GIndex) { + uint256 i = index(self); + uint256 w = width(self); + + if ((i % w) + n >= w) { + revert IndexOutOfRange(); + } + + return pack(i + n, pow(self)); +} + +/// @return Generalized index of the nth neighbor of the node to the left. +function shl(GIndex self, uint256 n) pure returns (GIndex) { + uint256 i = index(self); + uint256 w = width(self); + + if (i % w < n) { + revert IndexOutOfRange(); + } + + return pack(i - n, pow(self)); +} + +// See https://github.com/protolambda/remerkleable/blob/91ed092d08ef0ba5ab076f0a34b0b371623db728/remerkleable/tree.py#L46 +function concat(GIndex lhs, GIndex rhs) pure returns (GIndex) { + uint256 lhsMSbIndex = fls(index(lhs)); + uint256 rhsMSbIndex = fls(index(rhs)); + + if (lhsMSbIndex + 1 + rhsMSbIndex > 248) { + revert IndexOutOfRange(); + } + + return pack((index(lhs) << rhsMSbIndex) | (index(rhs) ^ (1 << rhsMSbIndex)), pow(rhs)); +} + +function isParentOf(GIndex self, GIndex child) pure returns (bool) { + uint256 parentIndex = index(self); + uint256 childIndex = index(child); + + if (parentIndex >= childIndex) { + return false; + } + + while (childIndex > 0) { + if (childIndex == parentIndex) { + return true; + } + + childIndex = childIndex >> 1; + } + + return false; +} + +/// @dev From Solady LibBit, see https://github.com/Vectorized/solady/blob/main/src/utils/LibBit.sol. +/// @dev Find last set. +/// Returns the index of the most significant bit of `x`, +/// counting from the least significant bit position. +/// If `x` is zero, returns 256. +function fls(uint256 x) pure returns (uint256 r) { + /// @solidity memory-safe-assembly + assembly { + // prettier-ignore + r := or(shl(8, iszero(x)), shl(7, lt(0xffffffffffffffffffffffffffffffff, x))) + r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) + r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) + r := or(r, shl(4, lt(0xffff, shr(r, x)))) + r := or(r, shl(3, lt(0xff, shr(r, x)))) + // prettier-ignore + r := or(r, byte(and(0x1f, shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be)), + 0x0706060506020504060203020504030106050205030304010505030400000000)) + } +} diff --git a/contracts/0.8.25/lib/SSZ.sol b/contracts/0.8.25/lib/SSZ.sol new file mode 100644 index 0000000000..48e637b1b3 --- /dev/null +++ b/contracts/0.8.25/lib/SSZ.sol @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* + SSZ library from CSM + original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol +*/ + +pragma solidity 0.8.25; + +import {BeaconBlockHeader, Validator} from "./BeaconTypes.sol"; +import {GIndex} from "./GIndex.sol"; + +library SSZ { + error BranchHasMissingItem(); + error BranchHasExtraItem(); + error InvalidProof(); + + function hashTreeRoot(BeaconBlockHeader memory header) internal view returns (bytes32 root) { + bytes32[8] memory nodes = [ + toLittleEndian(header.slot), + toLittleEndian(header.proposerIndex), + header.parentRoot, + header.stateRoot, + header.bodyRoot, + bytes32(0), + bytes32(0), + bytes32(0) + ]; + + /// @solidity memory-safe-assembly + assembly { + // Count of nodes to hash + let count := 8 + + // Loop over levels + // prettier-ignore + for { } 1 { } { + // Loop over nodes at the given depth + + // Initialize `offset` to the offset of `proof` elements in memory. + let target := nodes + let source := nodes + let end := add(source, shl(5, count)) + + // prettier-ignore + for { } 1 { } { + // Read next two hashes to hash + mcopy(0x00, source, 0x40) + + // Call sha256 precompile + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + // Store the resulting hash at the target location + mstore(target, mload(0x00)) + + // Advance the pointers + target := add(target, 0x20) + source := add(source, 0x40) + + if iszero(lt(source, end)) { + break + } + } + + count := shr(1, count) + if eq(count, 1) { + root := mload(0x00) + break + } + } + } + } + + function hashTreeRoot(Validator memory validator) internal view returns (bytes32 root) { + bytes32 pubkeyRoot; + + assembly { + // Dynamic data types such as bytes are stored at the specified offset. + let offset := mload(validator) + // Copy the pubkey to the scratch space. + mcopy(0x00, add(offset, 32), 48) + // Clear the last 16 bytes. + mcopy(48, 0x60, 16) + // Call sha256 precompile. + let result := staticcall(gas(), 0x02, 0x00, 0x40, 0x00, 0x20) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + pubkeyRoot := mload(0x00) + } + + bytes32[8] memory nodes = [ + pubkeyRoot, + validator.withdrawalCredentials, + toLittleEndian(validator.effectiveBalance), + toLittleEndian(validator.slashed), + toLittleEndian(validator.activationEligibilityEpoch), + toLittleEndian(validator.activationEpoch), + toLittleEndian(validator.exitEpoch), + toLittleEndian(validator.withdrawableEpoch) + ]; + + /// @solidity memory-safe-assembly + assembly { + // Count of nodes to hash + let count := 8 + + // Loop over levels + // prettier-ignore + for { } 1 { } { + // Loop over nodes at the given depth + + // Initialize `offset` to the offset of `proof` elements in memory. + let target := nodes + let source := nodes + let end := add(source, shl(5, count)) + + // prettier-ignore + for { } 1 { } { + // Read next two hashes to hash + mcopy(0x00, source, 0x40) + + // Call sha256 precompile + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompiles returns no data on OutOfGas error. + revert(0, 0) + } + + // Store the resulting hash at the target location + mstore(target, mload(0x00)) + + // Advance the pointers + target := add(target, 0x20) + source := add(source, 0x40) + + if iszero(lt(source, end)) { + break + } + } + + count := shr(1, count) + if eq(count, 1) { + root := mload(0x00) + break + } + } + } + } + + /// @notice Modified version of `verify` from Solady `MerkleProofLib` to support generalized indices and sha256 precompile. + /// @dev Reverts if `leaf` doesn't exist in the Merkle tree with `root`, given `proof`. + function verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, GIndex gI) internal view { + uint256 index = gI.index(); + + /// @solidity memory-safe-assembly + assembly { + // Check if `proof` is empty. + if iszero(proof.length) { + // revert InvalidProof() + mstore(0x00, 0x09bde339) + revert(0x1c, 0x04) + } + // Left shift by 5 is equivalent to multiplying by 0x20. + let end := add(proof.offset, shl(5, proof.length)) + // Initialize `offset` to the offset of `proof` in the calldata. + let offset := proof.offset + // Iterate over proof elements to compute root hash. + // prettier-ignore + for { } 1 { } { + // Slot of `leaf` in scratch space. + // If the condition is true: 0x20, otherwise: 0x00. + let scratch := shl(5, and(index, 1)) + index := shr(1, index) + if iszero(index) { + // revert BranchHasExtraItem() + mstore(0x00, 0x5849603f) + // 0x1c = 28 => offset in 32-byte word of a slot 0x00 + revert(0x1c, 0x04) + } + // Store elements to hash contiguously in scratch space. + // Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes. + mstore(scratch, leaf) + mstore(xor(scratch, 0x20), calldataload(offset)) + // Call sha256 precompile. + let result := staticcall( + gas(), + 0x02, + 0x00, + 0x40, + 0x00, + 0x20 + ) + + if iszero(result) { + // Precompile returns no data on OutOfGas error. + revert(0, 0) + } + + // Reuse `leaf` to store the hash to reduce stack operations. + leaf := mload(0x00) + offset := add(offset, 0x20) + if iszero(lt(offset, end)) { + break + } + } + + if iszero(eq(index, 1)) { + // revert BranchHasMissingItem() + mstore(0x00, 0x1b6661c3) + revert(0x1c, 0x04) + } + + if iszero(eq(leaf, root)) { + // revert InvalidProof() + mstore(0x00, 0x09bde339) + revert(0x1c, 0x04) + } + } + } + + // See https://github.com/succinctlabs/telepathy-contracts/blob/5aa4bb7/src/libraries/SimpleSerialize.sol#L17-L28 + function toLittleEndian(uint256 v) internal pure returns (bytes32) { + v = + ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + v = + ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + v = + ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + v = + ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) | + ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + v = (v >> 128) | (v << 128); + return bytes32(v); + } + + function toLittleEndian(bool v) internal pure returns (bytes32) { + return bytes32(v ? 1 << 248 : 0); + } +} diff --git a/test/0.8.25/contracts/Utilities.sol b/test/0.8.25/contracts/Utilities.sol new file mode 100644 index 0000000000..8f0c2c091a --- /dev/null +++ b/test/0.8.25/contracts/Utilities.sol @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +contract Utilities { + error FreeMemoryPointerOverflowed(); + error ZeroSlotIsNotZero(); + + /// See https://github.com/Vectorized/solady - MIT licensed. + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + function _brutalizeMemory() private view { + // To prevent a solidity 0.8.13 bug. + // See: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug + // Basically, we need to access a solidity variable from the assembly to + // tell the compiler that this assembly block is not in isolation. + uint256 zero; + /// @solidity memory-safe-assembly + assembly { + let offset := mload(0x40) // Start the offset at the free memory pointer. + calldatacopy(offset, zero, calldatasize()) + + // Fill the 64 bytes of scratch space with garbage. + mstore(zero, add(caller(), gas())) + mstore(0x20, keccak256(offset, calldatasize())) + mstore(zero, keccak256(zero, 0x40)) + + let r0 := mload(zero) + let r1 := mload(0x20) + + let cSize := add(codesize(), iszero(codesize())) + if iszero(lt(cSize, 32)) { + cSize := sub(cSize, and(mload(0x02), 0x1f)) + } + let start := mod(mload(0x10), cSize) + let size := mul(sub(cSize, start), gt(cSize, start)) + let times := div(0x7ffff, cSize) + if iszero(lt(times, 128)) { + times := 128 + } + + // Occasionally offset the offset by a pseudorandom large amount. + // Can't be too large, or we will easily get out-of-gas errors. + offset := add(offset, mul(iszero(and(r1, 0xf)), and(r0, 0xfffff))) + + // Fill the free memory with garbage. + // prettier-ignore + for { let w := not(0) } 1 {} { + mstore(offset, r0) + mstore(add(offset, 0x20), r1) + offset := add(offset, 0x40) + // We use codecopy instead of the identity precompile + // to avoid polluting the `forge test -vvvv` output with tons of junk. + codecopy(offset, start, size) + codecopy(add(offset, size), 0, start) + offset := add(offset, cSize) + times := add(times, w) // `sub(times, 1)`. + if iszero(times) { break } + } + } + } + + /// See https://github.com/Vectorized/solady - MIT licensed. + /// @dev Check if the free memory pointer and the zero slot are not contaminated. + /// Useful for cases where these slots are used for temporary storage. + function _checkMemory() internal pure { + bool zeroSlotIsNotZero; + bool freeMemoryPointerOverflowed; + /// @solidity memory-safe-assembly + assembly { + // Write ones to the free memory, to make subsequent checks fail if + // insufficient memory is allocated. + mstore(mload(0x40), not(0)) + // Test at a lower, but reasonable limit for more safety room. + if gt(mload(0x40), 0xffffffff) { + freeMemoryPointerOverflowed := 1 + } + // Check the value of the zero slot. + zeroSlotIsNotZero := mload(0x60) + } + if (freeMemoryPointerOverflowed) revert FreeMemoryPointerOverflowed(); + if (zeroSlotIsNotZero) revert ZeroSlotIsNotZero(); + } + + /// See https://github.com/Vectorized/solady - MIT licensed. + /// @dev Fills the memory with junk, for more robust testing of inline assembly + /// which reads/write to the memory. + modifier brutalizeMemory() { + _brutalizeMemory(); + _; + _checkMemory(); + } +} diff --git a/test/0.8.25/lib/GIndex.t.sol b/test/0.8.25/lib/GIndex.t.sol new file mode 100644 index 0000000000..4da9a01da1 --- /dev/null +++ b/test/0.8.25/lib/GIndex.t.sol @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {GIndex, pack, IndexOutOfRange, fls} from "../../../contracts/0.8.25/lib/GIndex.sol"; +import {SSZ} from "../../../contracts/0.8.25/lib/SSZ.sol"; + +// Wrap the library internal methods to make an actual call to them. +// Supposed to be used with `expectRevert` cheatcode. +contract Library { + function concat(GIndex lhs, GIndex rhs) public pure returns (GIndex) { + return lhs.concat(rhs); + } + + function shr(GIndex self, uint256 n) public pure returns (GIndex) { + return self.shr(n); + } + + function shl(GIndex self, uint256 n) public pure returns (GIndex) { + return self.shl(n); + } +} + +contract GIndexTest is Test { + GIndex internal ZERO = GIndex.wrap(bytes32(0)); + GIndex internal ROOT = GIndex.wrap(0x0000000000000000000000000000000000000000000000000000000000000100); + GIndex internal MAX = GIndex.wrap(bytes32(type(uint256).max)); + + Library internal lib; + + error Log2Undefined(); + + function setUp() public { + lib = new Library(); + } + + function test_pack() public { + GIndex gI; + + gI = pack(0x7b426f79504c6a8e9d31415b722f696e705c8a3d9f41, 42); + assertEq( + gI.unwrap(), + 0x0000000000000000007b426f79504c6a8e9d31415b722f696e705c8a3d9f412a, + "Invalid gindex encoded" + ); + + assertEq(MAX.unwrap(), bytes32(type(uint256).max), "Invalid gindex encoded"); + } + + function test_isRootTrue() public { + assertTrue(ROOT.isRoot(), "ROOT is not root gindex"); + } + + function test_isRootFalse() public { + GIndex gI; + + gI = pack(0, 0); + assertFalse(gI.isRoot(), "Expected [0,0].isRoot() to be false"); + + gI = pack(42, 0); + assertFalse(gI.isRoot(), "Expected [42,0].isRoot() to be false"); + + gI = pack(42, 4); + assertFalse(gI.isRoot(), "Expected [42,4].isRoot() to be false"); + + gI = pack(2048, 4); + assertFalse(gI.isRoot(), "Expected [2048,4].isRoot() to be false"); + + gI = pack(type(uint248).max, type(uint8).max); + assertFalse(gI.isRoot(), "Expected [uint248.max,uint8.max].isRoot() to be false"); + } + + function test_isParentOf_Truthy() public { + assertTrue(pack(1024, 0).isParentOf(pack(2048, 0))); + assertTrue(pack(1024, 0).isParentOf(pack(2049, 0))); + assertTrue(pack(1024, 9).isParentOf(pack(2048, 0))); + assertTrue(pack(1024, 9).isParentOf(pack(2049, 0))); + assertTrue(pack(1024, 0).isParentOf(pack(2048, 9))); + assertTrue(pack(1024, 0).isParentOf(pack(2049, 9))); + assertTrue(pack(1023, 0).isParentOf(pack(4094, 0))); + assertTrue(pack(1024, 0).isParentOf(pack(4098, 0))); + } + + function testFuzz_ROOT_isParentOfAnyChild(GIndex rhs) public { + vm.assume(rhs.index() > 1); + assertTrue(ROOT.isParentOf(rhs)); + } + + function testFuzz_isParentOf_LessThanAnchor(GIndex lhs, GIndex rhs) public { + vm.assume(rhs.index() < lhs.index()); + assertFalse(lhs.isParentOf(rhs)); + } + + function test_isParentOf_OffTheBranch() public { + assertFalse(pack(1024, 0).isParentOf(pack(2050, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2051, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2047, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2046, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2050, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2051, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2047, 0))); + assertFalse(pack(1024, 9).isParentOf(pack(2046, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(2050, 9))); + assertFalse(pack(1024, 0).isParentOf(pack(2051, 9))); + assertFalse(pack(1024, 0).isParentOf(pack(2047, 9))); + assertFalse(pack(1024, 0).isParentOf(pack(2046, 9))); + assertFalse(pack(1023, 0).isParentOf(pack(2048, 0))); + assertFalse(pack(1023, 0).isParentOf(pack(2049, 0))); + assertFalse(pack(1023, 9).isParentOf(pack(2048, 0))); + assertFalse(pack(1023, 9).isParentOf(pack(2049, 0))); + assertFalse(pack(1023, 0).isParentOf(pack(4098, 0))); + assertFalse(pack(1024, 0).isParentOf(pack(4094, 0))); + } + + function test_concat() public { + assertEq(pack(2, 99).concat(pack(3, 99)).unwrap(), pack(5, 99).unwrap()); + assertEq(pack(31, 99).concat(pack(3, 99)).unwrap(), pack(63, 99).unwrap()); + assertEq(pack(31, 99).concat(pack(6, 99)).unwrap(), pack(126, 99).unwrap()); + assertEq(ROOT.concat(pack(2, 1)).concat(pack(5, 1)).concat(pack(9, 1)).unwrap(), pack(73, 1).unwrap()); + assertEq(ROOT.concat(pack(2, 9)).concat(pack(5, 1)).concat(pack(9, 4)).unwrap(), pack(73, 4).unwrap()); + + assertEq(ROOT.concat(MAX).unwrap(), MAX.unwrap()); + } + + function test_concat_RevertsIfZeroGIndex() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(ZERO, pack(1024, 1)); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(1024, 1), ZERO); + } + + function test_concat_BigIndicesBorderCases() public view { + lib.concat(pack(2 ** 9, 0), pack(2 ** 238, 0)); + lib.concat(pack(2 ** 47, 0), pack(2 ** 200, 0)); + lib.concat(pack(2 ** 199, 0), pack(2 ** 48, 0)); + } + + function test_concat_RevertsIfTooBigIndices() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(MAX, MAX); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(2 ** 48, 0), pack(2 ** 200, 0)); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(2 ** 200, 0), pack(2 ** 48, 0)); + } + + function testFuzz_concat_WithRoot(GIndex rhs) public { + vm.assume(rhs.index() > 0); + assertEq(ROOT.concat(rhs).unwrap(), rhs.unwrap(), "`concat` with a root should return right-hand side value"); + } + + function testFuzz_concat_isParentOf(GIndex lhs, GIndex rhs) public { + // Left-hand side value can be a root. + vm.assume(lhs.index() > 0); + // But root.concat(root) will result in a root value again, and root is not a parent for itself. + vm.assume(rhs.index() > 1); + // Overflow check. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + + assertTrue(lhs.isParentOf(lhs.concat(rhs)), "Left-hand side value should be a parent of `concat` result"); + assertFalse(lhs.concat(rhs).isParentOf(lhs), "`concat` result can't be a parent for the left-hand side value"); + assertFalse(lhs.concat(rhs).isParentOf(rhs), "`concat` result can't be a parent for the right-hand side value"); + } + + function testFuzz_unpack(uint248 index, uint8 pow) public { + GIndex gI = pack(index, pow); + assertEq(gI.index(), index); + assertEq(gI.width(), 2 ** pow); + } + + function test_shr() public { + GIndex gI; + + gI = pack(1024, 4); + assertEq(gI.shr(0).unwrap(), pack(1024, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(1025, 4).unwrap()); + assertEq(gI.shr(15).unwrap(), pack(1039, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gI.shr(0).unwrap(), pack(1031, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(1032, 4).unwrap()); + assertEq(gI.shr(8).unwrap(), pack(1039, 4).unwrap()); + + gI = pack(2049, 4); + assertEq(gI.shr(0).unwrap(), pack(2049, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(2050, 4).unwrap()); + assertEq(gI.shr(14).unwrap(), pack(2063, 4).unwrap()); + } + + function test_shr_AfterConcat() public { + GIndex gI; + GIndex gIParent = pack(5, 4); + + gI = pack(1024, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5120, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5121, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(15).unwrap(), pack(5135, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5127, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5128, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(8).unwrap(), pack(5135, 4).unwrap()); + + gI = pack(2049, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(10241, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(10242, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(14).unwrap(), pack(10255, 4).unwrap()); + } + + function test_shr_OffTheWidth() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(ROOT, 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1024, 4), 16); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1031, 4), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1023, 4), 1); + } + + function test_shr_OffTheWidth_AfterConcat() public { + GIndex gIParent = pack(154, 4); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(ROOT), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1024, 4)), 16); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1031, 4)), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1023, 4)), 1); + } + + function testFuzz_shr_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { + // Indices concatenation overflow protection. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + vm.assume(rhs.index() >= rhs.width()); + unchecked { + vm.assume(rhs.width() + shift > rhs.width()); + vm.assume(lhs.concat(rhs).index() + shift > lhs.concat(rhs).index()); + } + + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(lhs.concat(rhs), rhs.width() + shift); + } + + function test_shl() public { + GIndex gI; + + gI = pack(1023, 4); + assertEq(gI.shl(0).unwrap(), pack(1023, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(1022, 4).unwrap()); + assertEq(gI.shl(15).unwrap(), pack(1008, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gI.shl(0).unwrap(), pack(1031, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(1030, 4).unwrap()); + assertEq(gI.shl(7).unwrap(), pack(1024, 4).unwrap()); + + gI = pack(2063, 4); + assertEq(gI.shl(0).unwrap(), pack(2063, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(2062, 4).unwrap()); + assertEq(gI.shl(15).unwrap(), pack(2048, 4).unwrap()); + } + + function test_shl_AfterConcat() public { + GIndex gI; + GIndex gIParent = pack(5, 4); + + gI = pack(1023, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(3071, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(3070, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(3056, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(5127, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(5126, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(7).unwrap(), pack(5120, 4).unwrap()); + + gI = pack(2063, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(10255, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(10254, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(10240, 4).unwrap()); + } + + function test_shl_OffTheWidth() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(ROOT, 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1024, 4), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1031, 4), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1023, 4), 16); + } + + function test_shl_OffTheWidth_AfterConcat() public { + GIndex gIParent = pack(154, 4); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(ROOT), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1024, 4)), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1031, 4)), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1023, 4)), 16); + } + + function testFuzz_shl_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { + // Indices concatenation overflow protection. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + vm.assume(rhs.index() >= rhs.width()); + vm.assume(shift > rhs.index() % rhs.width()); + + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(lhs.concat(rhs), shift); + } + + function testFuzz_shl_shr_Idempotent(GIndex gI, uint256 shift) public { + vm.assume(gI.index() > 0); + vm.assume(gI.index() >= gI.width()); + vm.assume(shift < gI.index() % gI.width()); + + assertEq(lib.shr(lib.shl(gI, shift), shift).unwrap(), gI.unwrap()); + } + + function testFuzz_shr_shl_Idempotent(GIndex gI, uint256 shift) public { + vm.assume(gI.index() > 0); + vm.assume(gI.index() >= gI.width()); + vm.assume(shift < gI.width() - (gI.index() % gI.width())); + + assertEq(lib.shl(lib.shr(gI, shift), shift).unwrap(), gI.unwrap()); + } + + function test_fls() public { + for (uint256 i = 1; i < 255; i++) { + assertEq(fls((1 << i) - 1), i - 1); + assertEq(fls((1 << i)), i); + assertEq(fls((1 << i) + 1), i); + } + + assertEq(fls(3), 1); // 0011 + assertEq(fls(7), 2); // 0101 + assertEq(fls(10), 3); // 1010 + assertEq(fls(300), 8); // 0001 0010 1100 + assertEq(fls(0), 256); + } +} diff --git a/test/0.8.25/lib/SSZ.t.sol b/test/0.8.25/lib/SSZ.t.sol new file mode 100644 index 0000000000..647eee0c04 --- /dev/null +++ b/test/0.8.25/lib/SSZ.t.sol @@ -0,0 +1,369 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {BeaconBlockHeader, Validator} from "../../../contracts/0.8.25/lib/BeaconTypes.sol"; +import {GIndex, pack} from "../../../contracts/0.8.25/lib/GIndex.sol"; +import {Utilities} from "../contracts/Utilities.sol"; +import {SSZ} from "../../../contracts/0.8.25/lib/SSZ.sol"; + +// Wrap the library internal methods to make an actual call to them. +// Supposed to be used with `expectRevert` cheatcode and to pass +// calldata arguments. +contract Library { + function verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf, GIndex gI) external view { + SSZ.verifyProof(proof, root, leaf, gI); + } +} + +contract SSZTest is Utilities, Test { + Library internal lib; + + function setUp() public { + lib = new Library(); + } + + function test_toLittleEndianUint() public pure { + uint256 v = 0x1234567890ABCDEF; + bytes32 expected = bytes32(bytes.concat(hex"EFCDAB9078563412", bytes24(0))); + bytes32 actual = SSZ.toLittleEndian(v); + assertEq(actual, expected); + } + + function test_toLittleEndianUintZero() public pure { + bytes32 actual = SSZ.toLittleEndian(0); + assertEq(actual, bytes32(0)); + } + + function test_toLittleEndianFalse() public pure { + bool v = false; + bytes32 expected = 0x0000000000000000000000000000000000000000000000000000000000000000; + bytes32 actual = SSZ.toLittleEndian(v); + assertEq(actual, expected); + } + + function test_toLittleEndianTrue() public pure { + bool v = true; + bytes32 expected = 0x0100000000000000000000000000000000000000000000000000000000000000; + bytes32 actual = SSZ.toLittleEndian(v); + assertEq(actual, expected); + } + + function testFuzz_toLittleEndian_Idempotent(uint256 v) public pure { + uint256 n = v; + n = uint256(SSZ.toLittleEndian(n)); + n = uint256(SSZ.toLittleEndian(n)); + assertEq(n, v); + } + + function test_ValidatorRootExitedSlashed() public view { + Validator memory v = Validator({ + pubkey: hex"91760f8a17729cfcb68bfc621438e5d9dfa831cd648e7b2b7d33540a7cbfda1257e4405e67cd8d3260351ab3ff71b213", + withdrawalCredentials: 0x01000000000000000000000006676e8584342cc8b6052cfdf381c3a281f00ac8, + effectiveBalance: 30000000000, + slashed: true, + activationEligibilityEpoch: 242529, + activationEpoch: 242551, + exitEpoch: 242556, + withdrawableEpoch: 250743 + }); + + bytes32 expected = 0xe4674dc5c27e7d3049fcd298745c00d3e314f03d33c877f64bf071d3b77eb942; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRootActive() public view { + Validator memory v = Validator({ + pubkey: hex"8fb78536e82bcec34e98fff85c907f0a8e6f4b1ccdbf1e8ace26b59eb5a06d16f34e50837f6c490e2ad6a255db8d543b", + withdrawalCredentials: 0x0023b9d00bf66e7f8071208a85afde59b3148dea046ee3db5d79244880734881, + effectiveBalance: 32000000000, + slashed: false, + activationEligibilityEpoch: 2593, + activationEpoch: 5890, + exitEpoch: type(uint64).max, + withdrawableEpoch: type(uint64).max + }); + + bytes32 expected = 0x60fb91184416404ddfc62bef6df9e9a52c910751daddd47ea426aabaf19dfa09; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRootExtraBytesInPubkey() public view { + Validator memory v = Validator({ + pubkey: hex"8fb78536e82bcec34e98fff85c907f0a8e6f4b1ccdbf1e8ace26b59eb5a06d16f34e50837f6c490e2ad6a255db8d543bDEADBEEFDEADBEEFDEADBEEFDEADBEEF", + withdrawalCredentials: 0x0023b9d00bf66e7f8071208a85afde59b3148dea046ee3db5d79244880734881, + effectiveBalance: 32000000000, + slashed: false, + activationEligibilityEpoch: 2593, + activationEpoch: 5890, + exitEpoch: type(uint64).max, + withdrawableEpoch: type(uint64).max + }); + + bytes32 expected = 0x60fb91184416404ddfc62bef6df9e9a52c910751daddd47ea426aabaf19dfa09; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRoot_AllZeroes() public view { + Validator memory v = Validator({ + pubkey: hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + withdrawalCredentials: 0x0000000000000000000000000000000000000000000000000000000000000000, + effectiveBalance: 0, + slashed: false, + activationEligibilityEpoch: 0, + activationEpoch: 0, + exitEpoch: 0, + withdrawableEpoch: 0 + }); + + bytes32 expected = 0xfa324a462bcb0f10c24c9e17c326a4e0ebad204feced523eccaf346c686f06ee; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function test_ValidatorRoot_AllOnes() public view { + Validator memory v = Validator({ + pubkey: hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + withdrawalCredentials: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + effectiveBalance: type(uint64).max, + slashed: true, + activationEligibilityEpoch: type(uint64).max, + activationEpoch: type(uint64).max, + exitEpoch: type(uint64).max, + withdrawableEpoch: type(uint64).max + }); + + bytes32 expected = 0x29c03a7cc9a8047ff05619a04bb6e60440a791e6ac3fe7d72e6fe9037dd3696f; + bytes32 actual = SSZ.hashTreeRoot(v); + assertEq(actual, expected); + } + + function testFuzz_validatorRoot_memory(Validator memory v) public view brutalizeMemory { + SSZ.hashTreeRoot(v); + } + + function test_BeaconBlockHeaderRoot() public view { + // Can be obtained via /eth/v1/beacon/headers/{block_id}. + BeaconBlockHeader memory h = BeaconBlockHeader({ + slot: 7472518, + proposerIndex: 152834, + parentRoot: 0x4916af1ff31b06f1b27125d2d20cd26e123c425a4b34ebd414e5f0120537e78d, + stateRoot: 0x76ca64f3732754bc02c7966271fb6356a9464fe5fce85be8e7abc403c8c7b56b, + bodyRoot: 0x6d858c959f1c95f411dba526c4ae9ab8b2690f8b1e59ed1b79ad963ab798b01a + }); + + bytes32 expected = 0x26631ee28ab4dd44a39c3756e03714d6a35a256560de5e2885caef9c3efd5516; + bytes32 actual = SSZ.hashTreeRoot(h); + assertEq(actual, expected); + } + + function test_BeaconBlockHeaderRoot_AllZeroes() public view { + BeaconBlockHeader memory h = BeaconBlockHeader({ + slot: 0, + proposerIndex: 0, + parentRoot: 0x0000000000000000000000000000000000000000000000000000000000000000, + stateRoot: 0x0000000000000000000000000000000000000000000000000000000000000000, + bodyRoot: 0x0000000000000000000000000000000000000000000000000000000000000000 + }); + + bytes32 expected = 0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c; + bytes32 actual = SSZ.hashTreeRoot(h); + assertEq(actual, expected); + } + + function test_BeaconBlockHeaderRoot_AllOnes() public view { + BeaconBlockHeader memory h = BeaconBlockHeader({ + slot: type(uint64).max, + proposerIndex: type(uint64).max, + parentRoot: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + stateRoot: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + bodyRoot: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + }); + + bytes32 expected = 0x5ebe9f2b0267944bd80dd5cde20317a91d07225ff12e9cd5ba1e834c05cc2b05; + bytes32 actual = SSZ.hashTreeRoot(h); + assertEq(actual, expected); + } + + function testFuzz_BeaconBlockHeaderRoot_memory(BeaconBlockHeader memory h) public view brutalizeMemory { + SSZ.hashTreeRoot(h); + } + + // For the tests below, assume there's the following tree from the bottom up: + // -- + // 0x0000000000000000000000000000000000000000000000000000000000000000 + // 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5 + // 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30 + // 0x21ddb9a356815c3fac1026b6dec5df3124afbadb485c9ba5a3e3398a04b7ba85 + // -- + // 0x0a4b105f69a6f41c3b3efc9bb5ac525b5b557a524039a13c657a916d8eb04451 + // 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124 + // -- + // 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c + + function test_verifyProof_HappyPath() public view { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0x0000000000000000000000000000000000000000000000000000000000000000, + pack(4, 0) + ); + + // prettier-ignore + { + proof[0] = 0x0000000000000000000000000000000000000000000000000000000000000000; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(5, 0) + ); + } + + function test_verifyProof_OneItem() public view brutalizeMemory { + bytes32[] memory proof = new bytes32[](1); + + // prettier-ignore + proof[0] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0x0a4b105f69a6f41c3b3efc9bb5ac525b5b557a524039a13c657a916d8eb04451, + pack(2, 0) + ); + } + + function test_verifyProof_RevertWhen_NoProof() public brutalizeMemory { + vm.expectRevert(SSZ.InvalidProof.selector); + + // bytes32(0) is a valid proof for the inputs. + lib.verifyProof( + new bytes32[](0), + 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, + 0x0000000000000000000000000000000000000000000000000000000000000000, + pack(2, 0) + ); + } + + function test_verifyProof_RevertWhen_ProvingRoot() public brutalizeMemory { + vm.expectRevert(SSZ.InvalidProof.selector); + + lib.verifyProof( + new bytes32[](0), + 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, + 0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b, + pack(1, 0) + ); + } + + function test_verifyProof_RevertWhen_InvalidProof() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.InvalidProof.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(4, 0) + ); + } + + function test_verifyProof_RevertWhen_WrongGIndex() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.InvalidProof.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0x0000000000000000000000000000000000000000000000000000000000000000, + pack(5, 0) + ); + } + + function test_verifyProof_RevertWhen_BranchHasExtraItem() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.BranchHasExtraItem.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(2, 0) + ); + } + + function test_verifyProof_RevertWhen_BranchHasMissingItem() public brutalizeMemory { + bytes32[] memory proof = new bytes32[](2); + + // prettier-ignore + { + proof[0] = 0xb4c11951957c6f8f642c4af61cd6b24640fec6dc7fc607ee8206a99e92410d30; + proof[1] = 0xf4551dd23f47858f0e66957db62a0bced8cfd5e9cbd63f2fd73672ed0db7c124; + } + + vm.expectRevert(SSZ.BranchHasMissingItem.selector); + + lib.verifyProof( + proof, + 0xda1c902c54a4386439ce622d7e527dc11decace28ebb902379cba91c4a116b1c, + 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5, + pack(8, 0) + ); + } + + function testFuzz_verifyProof_MemorySafe( + bytes32[] calldata proof, + bytes32 root, + bytes32 leaf, + GIndex gI + ) public view { + try this.verifyProofCallJunkMemory(proof, root, leaf, gI) {} catch {} + } + + function verifyProofCallJunkMemory( + bytes32[] calldata proof, + bytes32 root, + bytes32 leaf, + GIndex gI + ) external view brutalizeMemory { + SSZ.verifyProof(proof, root, leaf, gI); + } +} From 2decce28f2355535ba83b5dbbb81eeaf95b559e3 Mon Sep 17 00:00:00 2001 From: Yuri Tkachenko Date: Mon, 24 Mar 2025 12:59:24 +0000 Subject: [PATCH 049/405] feat: reuse eip7002 mock in integration tests --- lib/eips/eip7002.ts | 32 +++++++ lib/{ => eips}/eip712.ts | 0 lib/eips/index.ts | 2 + lib/index.ts | 2 +- lib/protocol/provision.ts | 5 ++ test/0.8.9/withdrawalVault.test.ts | 69 +++++++-------- .../EIP7002WithdrawalRequest_Mock.sol | 46 ---------- .../EIP7002WithdrawalRequest__Mock.sol | 54 ++++++++++++ ...ol => TriggerableWithdrawals__Harness.sol} | 2 +- .../lib/triggerableWithdrawals/eip7002Mock.ts | 31 +++---- .../triggerableWithdrawals.test.ts | 84 +++++++++---------- .../lib/triggerableWithdrawals/utils.ts | 20 ----- 12 files changed, 179 insertions(+), 168 deletions(-) create mode 100644 lib/eips/eip7002.ts rename lib/{ => eips}/eip712.ts (100%) create mode 100644 lib/eips/index.ts delete mode 100644 test/common/contracts/EIP7002WithdrawalRequest_Mock.sol create mode 100644 test/common/contracts/EIP7002WithdrawalRequest__Mock.sol rename test/common/contracts/{TriggerableWithdrawals_Harness.sol => TriggerableWithdrawals__Harness.sol} (96%) diff --git a/lib/eips/eip7002.ts b/lib/eips/eip7002.ts new file mode 100644 index 0000000000..18179bca15 --- /dev/null +++ b/lib/eips/eip7002.ts @@ -0,0 +1,32 @@ +import { ethers } from "hardhat"; + +import { EIP7002WithdrawalRequest__Mock } from "typechain-types"; + +import { log } from "lib"; + +// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7002.md#configuration +export const EIP7002_ADDRESS = "0x00000961Ef480Eb55e80D19ad83579A64c007002"; +export const EIP7002_MIN_WITHDRAWAL_REQUEST_FEE = 1n; + +export const deployEIP7002WithdrawalRequestContract = async (fee: bigint): Promise => { + const eip7002Mock = await ethers.deployContract("EIP7002WithdrawalRequest__Mock"); + const eip7002MockAddress = await eip7002Mock.getAddress(); + + await ethers.provider.send("hardhat_setCode", [EIP7002_ADDRESS, await ethers.provider.getCode(eip7002MockAddress)]); + + const contract = await ethers.getContractAt("EIP7002WithdrawalRequest__Mock", EIP7002_ADDRESS); + await contract.mock__setFee(fee); + + return contract; +}; + +export const ensureEIP7002WithdrawalRequestContractPresent = async (): Promise => { + const code = await ethers.provider.getCode(EIP7002_ADDRESS); + + if (code === "0x") { + log.warning(`EIP7002 withdrawal request contract not found at ${EIP7002_ADDRESS}`); + + await deployEIP7002WithdrawalRequestContract(EIP7002_MIN_WITHDRAWAL_REQUEST_FEE); + log.success("EIP7002 withdrawal request contract is present"); + } +}; diff --git a/lib/eip712.ts b/lib/eips/eip712.ts similarity index 100% rename from lib/eip712.ts rename to lib/eips/eip712.ts diff --git a/lib/eips/index.ts b/lib/eips/index.ts new file mode 100644 index 0000000000..a4aec2e3aa --- /dev/null +++ b/lib/eips/index.ts @@ -0,0 +1,2 @@ +export * from "./eip712"; +export * from "./eip7002"; diff --git a/lib/index.ts b/lib/index.ts index a2dde748d8..b66cdef958 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -6,7 +6,7 @@ export * from "./contract"; export * from "./deploy"; export * from "./dsm"; export * from "./ec"; -export * from "./eip712"; +export * from "./eips"; export * from "./ens"; export * from "./event"; export * from "./keccak"; diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 9457ba39a1..8d2f656fa0 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -1,4 +1,5 @@ import { log } from "lib"; +import { ensureEIP7002WithdrawalRequestContractPresent } from "lib/eips"; import { ensureHashConsensusInitialEpoch, @@ -23,6 +24,10 @@ export const provision = async (ctx: ProtocolContext) => { return; } + // Ensure necessary precompiled contracts are present + await ensureEIP7002WithdrawalRequestContractPresent(); + + // Ensure protocol is fully operational await ensureHashConsensusInitialEpoch(ctx); await ensureOracleCommitteeMembers(ctx, 5n); diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index a584e896fb..438ff8ed10 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; import { - EIP7002WithdrawalRequest_Mock, + EIP7002WithdrawalRequest__Mock, ERC20__Harness, ERC721__Harness, Lido__MockForWithdrawalVault, @@ -14,17 +14,12 @@ import { WithdrawalVault__Harness, } from "typechain-types"; -import { MAX_UINT256, proxify, streccak } from "lib"; +import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, proxify, streccak } from "lib"; +import { findEIP7002MockEvents, testEIP7002Mock } from "test/common/lib/triggerableWithdrawals/eip7002Mock"; +import { generateWithdrawalRequestPayload } from "test/common/lib/triggerableWithdrawals/utils"; import { Snapshot } from "test/suite"; -import { findEip7002MockEvents, testEip7002Mock } from "../common/lib/triggerableWithdrawals/eip7002Mock"; -import { - deployWithdrawalsPredeployedMock, - generateWithdrawalRequestPayload, - withdrawalsPredeployedHardcodedAddress, -} from "../common/lib/triggerableWithdrawals/utils"; - const PETRIFIED_VERSION = MAX_UINT256; const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); @@ -40,7 +35,7 @@ describe("WithdrawalVault.sol", () => { let lido: Lido__MockForWithdrawalVault; let lidoAddress: string; - let withdrawalsPredeployed: EIP7002WithdrawalRequest_Mock; + let withdrawalsPredeployed: EIP7002WithdrawalRequest__Mock; let impl: WithdrawalVault__Harness; let vault: WithdrawalVault__Harness; @@ -49,9 +44,9 @@ describe("WithdrawalVault.sol", () => { before(async () => { [owner, treasury, validatorsExitBus, stranger] = await ethers.getSigners(); - withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(1n); + withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); - expect(await withdrawalsPredeployed.getAddress()).to.equal(withdrawalsPredeployedHardcodedAddress); + expect(await withdrawalsPredeployed.getAddress()).to.equal(EIP7002_ADDRESS); lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); lidoAddress = await lido.getAddress(); @@ -273,7 +268,7 @@ describe("WithdrawalVault.sol", () => { context("get triggerable withdrawal request fee", () => { it("Should get fee from the EIP 7002 contract", async function () { - await withdrawalsPredeployed.setFee(333n); + await withdrawalsPredeployed.mock__setFee(333n); expect( (await vault.getWithdrawalRequestFee()) == 333n, "withdrawal request should use fee from the EIP 7002 contract", @@ -281,13 +276,13 @@ describe("WithdrawalVault.sol", () => { }); it("Should revert if fee read fails", async function () { - await withdrawalsPredeployed.setFailOnGetFee(true); + await withdrawalsPredeployed.mock__setFailOnGetFee(true); await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); }); ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); }); @@ -332,7 +327,7 @@ describe("WithdrawalVault.sol", () => { it("Should revert if not enough fee is sent", async function () { const { pubkeysHexString } = generateWithdrawalRequestPayload(1); - await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei + await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString)) @@ -377,7 +372,7 @@ describe("WithdrawalVault.sol", () => { const fee = await getFee(); // Set mock to fail on add - await withdrawalsPredeployed.setFailOnAddRequest(true); + await withdrawalsPredeployed.mock__setFailOnAddRequest(true); await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), @@ -385,7 +380,7 @@ describe("WithdrawalVault.sol", () => { }); it("Should revert when fee read fails", async function () { - await withdrawalsPredeployed.setFailOnGetFee(true); + await withdrawalsPredeployed.mock__setFailOnGetFee(true); const { pubkeysHexString } = generateWithdrawalRequestPayload(2); const fee = 10n; @@ -397,7 +392,7 @@ describe("WithdrawalVault.sol", () => { ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); const { pubkeysHexString } = generateWithdrawalRequestPayload(2); const fee = 10n; @@ -420,7 +415,7 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei await expect( @@ -441,10 +436,10 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(3n); + await withdrawalsPredeployed.mock__setFee(3n); const expectedTotalWithdrawalFee = 9n; - await testEip7002Mock( + await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -456,10 +451,10 @@ describe("WithdrawalVault.sol", () => { // Check extremely high fee const highFee = ethers.parseEther("10"); - await withdrawalsPredeployed.setFee(highFee); + await withdrawalsPredeployed.mock__setFee(highFee); const expectedLargeTotalWithdrawalFee = ethers.parseEther("30"); - await testEip7002Mock( + await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -475,10 +470,10 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); const withdrawalFee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei - await testEip7002Mock( + await testEIP7002Mock( () => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: withdrawalFee }), pubkeys, fullWithdrawalAmounts, @@ -488,7 +483,7 @@ describe("WithdrawalVault.sol", () => { // Check when the provided fee extremely exceeds the required amount const largeWithdrawalFee = ethers.parseEther("10"); - await testEip7002Mock( + await testEIP7002Mock( () => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: largeWithdrawalFee }), pubkeys, @@ -502,12 +497,12 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei const initialBalance = await getWithdrawalCredentialsContractBalance(); - await testEip7002Mock( + await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -520,7 +515,7 @@ describe("WithdrawalVault.sol", () => { const excessTotalWithdrawalFee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei - await testEip7002Mock( + await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -538,13 +533,13 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei const excessFee = 1n; const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - const { receipt } = await testEip7002Mock( + const { receipt } = await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -564,13 +559,13 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(3n); + await withdrawalsPredeployed.mock__setFee(3n); const expectedTotalWithdrawalFee = 9n; const excessTotalWithdrawalFee = 9n + 1n; let initialBalance = await getWithdrawalsPredeployedContractBalance(); - await testEip7002Mock( + await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -583,7 +578,7 @@ describe("WithdrawalVault.sol", () => { expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); initialBalance = await getWithdrawalsPredeployedContractBalance(); - await testEip7002Mock( + await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -607,7 +602,7 @@ describe("WithdrawalVault.sol", () => { const receipt = await tx.wait(); - const events = findEip7002MockEvents(receipt!, "eip7002MockRequestAdded"); + const events = findEIP7002MockEvents(receipt!); expect(events.length).to.equal(requestCount); for (let i = 0; i < requestCount; i++) { @@ -642,7 +637,7 @@ describe("WithdrawalVault.sol", () => { const initialBalance = await getWithdrawalCredentialsContractBalance(); const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - const { receipt } = await testEip7002Mock( + const { receipt } = await testEIP7002Mock( () => vault .connect(validatorsExitBus) diff --git a/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol b/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol deleted file mode 100644 index 4ed8060243..0000000000 --- a/test/common/contracts/EIP7002WithdrawalRequest_Mock.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.8.9; - -/** - * @notice This is a mock of EIP-7002's pre-deploy contract. - */ -contract EIP7002WithdrawalRequest_Mock { - bytes public fee; - bool public failOnAddRequest; - bool public failOnGetFee; - - event eip7002MockRequestAdded(bytes request, uint256 fee); - - function setFailOnAddRequest(bool _failOnAddRequest) external { - failOnAddRequest = _failOnAddRequest; - } - - function setFailOnGetFee(bool _failOnGetFee) external { - failOnGetFee = _failOnGetFee; - } - - function setFee(uint256 _fee) external { - require(_fee > 0, "fee must be greater than 0"); - fee = abi.encode(_fee); - } - - function setFeeRaw(bytes calldata _rawFeeBytes) external { - fee = _rawFeeBytes; - } - - fallback(bytes calldata input) external payable returns (bytes memory) { - if (input.length == 0) { - require(!failOnGetFee, "fail on get fee"); - - return fee; - } - - require(!failOnAddRequest, "fail on add request"); - - require(input.length == 56, "Invalid callData length"); - - emit eip7002MockRequestAdded(input, msg.value); - } -} diff --git a/test/common/contracts/EIP7002WithdrawalRequest__Mock.sol b/test/common/contracts/EIP7002WithdrawalRequest__Mock.sol new file mode 100644 index 0000000000..61d11506af --- /dev/null +++ b/test/common/contracts/EIP7002WithdrawalRequest__Mock.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.9; + +/** + * @notice This is a mock of EIP-7002's pre-deploy contract. + */ +contract EIP7002WithdrawalRequest__Mock { + bytes public fee; + bool public mock__failOnAddRequest; + bool public mock__failOnGetFee; + + bool public constant MOCK = true; + + event RequestAdded__Mock(bytes request, uint256 fee); + + function mock__setFailOnAddRequest(bool _failOnAddRequest) external { + mock__failOnAddRequest = _failOnAddRequest; + } + + function mock__setFailOnGetFee(bool _failOnGetFee) external { + mock__failOnGetFee = _failOnGetFee; + } + + function mock__setFee(uint256 _fee) external { + require(_fee > 0, "fee must be greater than 0"); + fee = abi.encode(_fee); + } + + function mock__setFeeRaw(bytes calldata _rawFeeBytes) external { + fee = _rawFeeBytes; + } + + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7002.md#add-withdrawal-request + fallback(bytes calldata input) external payable returns (bytes memory) { + // calculate the fee path + if (input.length == 0) { + require(!mock__failOnGetFee, "Inhibitor still active"); + return fee; + } + + // add withdrawal request path + require(input.length == 56, "Invalid callData length"); + require(!mock__failOnAddRequest, "fail on add request"); + + uint256 feeValue = abi.decode(fee, (uint256)); + if (msg.value < feeValue) { + revert("Insufficient value for fee"); + } + + emit RequestAdded__Mock(input, msg.value); + } +} diff --git a/test/common/contracts/TriggerableWithdrawals_Harness.sol b/test/common/contracts/TriggerableWithdrawals__Harness.sol similarity index 96% rename from test/common/contracts/TriggerableWithdrawals_Harness.sol rename to test/common/contracts/TriggerableWithdrawals__Harness.sol index a29db8a05c..74b2bb9d1b 100644 --- a/test/common/contracts/TriggerableWithdrawals_Harness.sol +++ b/test/common/contracts/TriggerableWithdrawals__Harness.sol @@ -8,7 +8,7 @@ import {TriggerableWithdrawals} from "contracts/common/lib/TriggerableWithdrawal /** * @notice This is a harness of TriggerableWithdrawals library. */ -contract TriggerableWithdrawals_Harness { +contract TriggerableWithdrawals__Harness { function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) external { TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); } diff --git a/test/common/lib/triggerableWithdrawals/eip7002Mock.ts b/test/common/lib/triggerableWithdrawals/eip7002Mock.ts index a23d7c89ec..807fbae43c 100644 --- a/test/common/lib/triggerableWithdrawals/eip7002Mock.ts +++ b/test/common/lib/triggerableWithdrawals/eip7002Mock.ts @@ -4,41 +4,34 @@ import { ethers } from "hardhat"; import { findEventsWithInterfaces } from "lib"; -const eip7002MockEventABI = ["event eip7002MockRequestAdded(bytes request, uint256 fee)"]; +const eventName = "RequestAdded__Mock"; +const eip7002MockEventABI = [`event ${eventName}(bytes request, uint256 fee)`]; const eip7002MockInterface = new ethers.Interface(eip7002MockEventABI); -type Eip7002MockTriggerableWithdrawalEvents = "eip7002MockRequestAdded"; -export function findEip7002MockEvents( - receipt: ContractTransactionReceipt, - event: Eip7002MockTriggerableWithdrawalEvents, -) { - return findEventsWithInterfaces(receipt!, event, [eip7002MockInterface]); +function encodeEIP7002Payload(pubkey: string, amount: bigint): string { + return `0x${pubkey}${amount.toString(16).padStart(16, "0")}`; } -export function encodeEip7002Payload(pubkey: string, amount: bigint): string { - return `0x${pubkey}${amount.toString(16).padStart(16, "0")}`; +export function findEIP7002MockEvents(receipt: ContractTransactionReceipt) { + return findEventsWithInterfaces(receipt!, eventName, [eip7002MockInterface]); } -export const testEip7002Mock = async ( - addTriggeranleWithdrawalRequests: () => Promise, +export const testEIP7002Mock = async ( + addTriggerableWithdrawalRequests: () => Promise, expectedPubkeys: string[], expectedAmounts: bigint[], expectedFee: bigint, ): Promise<{ tx: ContractTransactionResponse; receipt: ContractTransactionReceipt }> => { - const tx = await addTriggeranleWithdrawalRequests(); - const receipt = await tx.wait(); + const tx = await addTriggerableWithdrawalRequests(); + const receipt = (await tx.wait()) as ContractTransactionReceipt; - const events = findEip7002MockEvents(receipt!, "eip7002MockRequestAdded"); + const events = findEIP7002MockEvents(receipt); expect(events.length).to.equal(expectedPubkeys.length); for (let i = 0; i < expectedPubkeys.length; i++) { - expect(events[i].args[0]).to.equal(encodeEip7002Payload(expectedPubkeys[i], expectedAmounts[i])); + expect(events[i].args[0]).to.equal(encodeEIP7002Payload(expectedPubkeys[i], expectedAmounts[i])); expect(events[i].args[1]).to.equal(expectedFee); } - if (!receipt) { - throw new Error("No receipt"); - } - return { tx, receipt }; }; diff --git a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index d3f271d81d..ff0f267f49 100644 --- a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -5,24 +5,22 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { EIP7002WithdrawalRequest_Mock, TriggerableWithdrawals_Harness } from "typechain-types"; +import { EIP7002WithdrawalRequest__Mock, TriggerableWithdrawals__Harness } from "typechain-types"; + +import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS } from "lib"; import { Snapshot } from "test/suite"; -import { findEip7002MockEvents, testEip7002Mock } from "./eip7002Mock"; -import { - deployWithdrawalsPredeployedMock, - generateWithdrawalRequestPayload, - withdrawalsPredeployedHardcodedAddress, -} from "./utils"; +import { findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; +import { generateWithdrawalRequestPayload } from "./utils"; const EMPTY_PUBKEYS = "0x"; describe("TriggerableWithdrawals.sol", () => { let actor: HardhatEthersSigner; - let withdrawalsPredeployed: EIP7002WithdrawalRequest_Mock; - let triggerableWithdrawals: TriggerableWithdrawals_Harness; + let withdrawalsPredeployed: EIP7002WithdrawalRequest__Mock; + let triggerableWithdrawals: TriggerableWithdrawals__Harness; let originalState: string; @@ -41,10 +39,10 @@ describe("TriggerableWithdrawals.sol", () => { before(async () => { [actor] = await ethers.getSigners(); - withdrawalsPredeployed = await deployWithdrawalsPredeployedMock(1n); - triggerableWithdrawals = await ethers.deployContract("TriggerableWithdrawals_Harness"); + withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); + triggerableWithdrawals = await ethers.deployContract("TriggerableWithdrawals__Harness"); - expect(await withdrawalsPredeployed.getAddress()).to.equal(withdrawalsPredeployedHardcodedAddress); + expect(await withdrawalsPredeployed.getAddress()).to.equal(EIP7002_ADDRESS); await triggerableWithdrawals.connect(actor).deposit({ value: ethers.parseEther("1") }); }); @@ -59,15 +57,13 @@ describe("TriggerableWithdrawals.sol", () => { context("eip 7002 contract", () => { it("Should return the address of the EIP 7002 contract", async function () { - expect(await triggerableWithdrawals.getWithdrawalsContractAddress()).to.equal( - withdrawalsPredeployedHardcodedAddress, - ); + expect(await triggerableWithdrawals.getWithdrawalsContractAddress()).to.equal(EIP7002_ADDRESS); }); }); context("get triggerable withdrawal request fee", () => { it("Should get fee from the EIP 7002 contract", async function () { - await withdrawalsPredeployed.setFee(333n); + await withdrawalsPredeployed.mock__setFee(333n); expect( (await triggerableWithdrawals.getWithdrawalRequestFee()) == 333n, "withdrawal request should use fee from the EIP 7002 contract", @@ -75,7 +71,7 @@ describe("TriggerableWithdrawals.sol", () => { }); it("Should revert if fee read fails", async function () { - await withdrawalsPredeployed.setFailOnGetFee(true); + await withdrawalsPredeployed.mock__setFailOnGetFee(true); await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( triggerableWithdrawals, "WithdrawalFeeReadFailed", @@ -84,7 +80,7 @@ describe("TriggerableWithdrawals.sol", () => { ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( triggerableWithdrawals, @@ -139,7 +135,7 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeysHexString } = generateWithdrawalRequestPayload(1); const amounts = [10n]; - await withdrawalsPredeployed.setFee(3n); // Set fee to 3 gwei + await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei // 2. Should revert if fee is less than required const insufficientFee = 2n; @@ -206,7 +202,7 @@ describe("TriggerableWithdrawals.sol", () => { const fee = await getFee(); // Set mock to fail on add - await withdrawalsPredeployed.setFailOnAddRequest(true); + await withdrawalsPredeployed.mock__setFailOnAddRequest(true); await expect( triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), @@ -240,7 +236,7 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(keysCount); - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); await setBalance(await triggerableWithdrawals.getAddress(), balance); await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)) @@ -257,7 +253,7 @@ describe("TriggerableWithdrawals.sol", () => { }); it("Should revert when fee read fails", async function () { - await withdrawalsPredeployed.setFailOnGetFee(true); + await withdrawalsPredeployed.mock__setFailOnGetFee(true); const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); @@ -278,7 +274,7 @@ describe("TriggerableWithdrawals.sol", () => { ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.setFeeRaw(unexpectedFee); + await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); @@ -305,16 +301,16 @@ describe("TriggerableWithdrawals.sol", () => { const fee = 3n; const fee_not_provided = 0n; - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee_not_provided), pubkeys, fullWithdrawalAmounts, fee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addPartialWithdrawalRequests( pubkeysHexString, @@ -326,7 +322,7 @@ describe("TriggerableWithdrawals.sol", () => { fee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee_not_provided), pubkeys, mixedWithdrawalAmounts, @@ -340,23 +336,23 @@ describe("TriggerableWithdrawals.sol", () => { generateWithdrawalRequestPayload(requestCount); const fee = 3n; - await withdrawalsPredeployed.setFee(fee); + await withdrawalsPredeployed.mock__setFee(fee); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), pubkeys, fullWithdrawalAmounts, fee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), pubkeys, partialWithdrawalAmounts, fee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), pubkeys, mixedWithdrawalAmounts, @@ -365,25 +361,25 @@ describe("TriggerableWithdrawals.sol", () => { // Check extremely high fee const highFee = ethers.parseEther("10"); - await withdrawalsPredeployed.setFee(highFee); + await withdrawalsPredeployed.mock__setFee(highFee); await triggerableWithdrawals.connect(actor).deposit({ value: highFee * BigInt(requestCount) * 3n }); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, highFee), pubkeys, fullWithdrawalAmounts, highFee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, highFee), pubkeys, partialWithdrawalAmounts, highFee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, highFee), pubkeys, mixedWithdrawalAmounts, @@ -396,17 +392,17 @@ describe("TriggerableWithdrawals.sol", () => { const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - await withdrawalsPredeployed.setFee(3n); + await withdrawalsPredeployed.mock__setFee(3n); const excessFee = 4n; - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, excessFee), pubkeys, fullWithdrawalAmounts, excessFee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, excessFee), pubkeys, @@ -414,7 +410,7 @@ describe("TriggerableWithdrawals.sol", () => { excessFee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, excessFee), pubkeys, mixedWithdrawalAmounts, @@ -425,14 +421,14 @@ describe("TriggerableWithdrawals.sol", () => { const extremelyHighFee = ethers.parseEther("10"); await triggerableWithdrawals.connect(actor).deposit({ value: extremelyHighFee * BigInt(requestCount) * 3n }); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, extremelyHighFee), pubkeys, fullWithdrawalAmounts, extremelyHighFee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addPartialWithdrawalRequests( pubkeysHexString, @@ -444,7 +440,7 @@ describe("TriggerableWithdrawals.sol", () => { extremelyHighFee, ); - await testEip7002Mock( + await testEIP7002Mock( () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, extremelyHighFee), pubkeys, mixedWithdrawalAmounts, @@ -531,7 +527,7 @@ describe("TriggerableWithdrawals.sol", () => { const tx = await addRequests(); const receipt = await tx.wait(); - const events = findEip7002MockEvents(receipt!, "eip7002MockRequestAdded"); + const events = findEIP7002MockEvents(receipt!); expect(events.length).to.equal(requestCount); for (let i = 0; i < requestCount; i++) { @@ -574,7 +570,7 @@ describe("TriggerableWithdrawals.sol", () => { ) { const initialBalance = await getWithdrawalCredentialsContractBalance(); - await testEip7002Mock(addRequests, expectedPubkeys, expectedAmounts, expectedFee); + await testEIP7002Mock(addRequests, expectedPubkeys, expectedAmounts, expectedFee); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); } diff --git a/test/common/lib/triggerableWithdrawals/utils.ts b/test/common/lib/triggerableWithdrawals/utils.ts index 678a4a9fb5..64a2e418ea 100644 --- a/test/common/lib/triggerableWithdrawals/utils.ts +++ b/test/common/lib/triggerableWithdrawals/utils.ts @@ -1,25 +1,5 @@ import { ethers } from "hardhat"; -import { EIP7002WithdrawalRequest_Mock } from "typechain-types"; - -export const withdrawalsPredeployedHardcodedAddress = "0x00000961Ef480Eb55e80D19ad83579A64c007002"; - -export async function deployWithdrawalsPredeployedMock( - defaultRequestFee: bigint, -): Promise { - const withdrawalsPredeployed = await ethers.deployContract("EIP7002WithdrawalRequest_Mock"); - const withdrawalsPredeployedAddress = await withdrawalsPredeployed.getAddress(); - - await ethers.provider.send("hardhat_setCode", [ - withdrawalsPredeployedHardcodedAddress, - await ethers.provider.getCode(withdrawalsPredeployedAddress), - ]); - - const contract = await ethers.getContractAt("EIP7002WithdrawalRequest_Mock", withdrawalsPredeployedHardcodedAddress); - await contract.setFee(defaultRequestFee); - return contract; -} - function toValidatorPubKey(num: number): string { if (num < 0 || num > 0xffff) { throw new Error("Number is out of the 2-byte range (0x0000 - 0xffff)."); From f5b069f645466e3cc6b09dbce5c8a20851c516ff Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 26 Mar 2025 16:04:30 +0100 Subject: [PATCH 050/405] feat: update IStakingModule to support TW --- contracts/0.8.9/interfaces/IStakingModule.sol | 65 +++++++++++++++--- .../StakingModule__MockForStakingRouter.sol | 67 +++++++++++++++++++ 2 files changed, 123 insertions(+), 9 deletions(-) diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 82d55cf05b..0b419d755f 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -5,6 +5,62 @@ pragma solidity 0.8.9; /// @title Lido's Staking Module interface interface IStakingModule { + /// @dev Event to be emitted on StakingModule's nonce change + event NonceChanged(uint256 nonce); + + /// @dev Event to be emitted when a signing key is added to the StakingModule + event SigningKeyAdded(uint256 indexed nodeOperatorId, bytes pubkey); + + /// @dev Event to be emitted when a signing key is removed from the StakingModule + event SigningKeyRemoved(uint256 indexed nodeOperatorId, bytes pubkey); + + /// @notice Handles tracking and penalization logic for validators that remain active beyond their eligible exit window. + /// @dev This function is called to report the current exit-related status of validators belonging to a specific node operator. + /// It accepts a batch of validator public keys, each associated with the duration (in seconds) they were eligible to exit but have not. + /// This data could be used to trigger penalties for the node operator if validators have been non-exiting for too long. + /// @param nodeOperatorId The ID of the node operator whose validators statuses being delivered. + /// @param proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. + /// @param publicKeys Concatenated public keys of the validators being reported. + /// @param eligibleToExitInSec Array of durations (in seconds), each indicating how long a validator has been eligible to exit but hasn't. + function handleActiveValidatorsExitingStatus( + uint256 nodeOperatorId, + uint256 proofSlotTimestamp, + bytes calldata publicKeys, + bytes calldata eligibleToExitInSec + ) external; + + /// @notice Handles the triggerable exit events validator belonging to a specific node operator. + /// @dev This function is called when a validator is exited using the triggerable exit request on EL. + /// @param _nodeOperatorId The ID of the node operator. + /// @param publicKeys Concatenated public keys of the validators being reported. + /// @param withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. + /// @param exitType The type of exit being performed. + /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. + function onTriggerableExit( + uint256 _nodeOperatorId, + bytes calldata publicKeys, + uint256 withdrawalRequestPaidFee, + uint256 exitType + ) external; + + /// @notice Determines whether a validator exit status should be updated and will have affect on Node Operator. + /// @param _nodeOperatorId The ID of the node operator. + /// @param proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. + /// @param publicKey Validator's public key. + /// @param eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. + /// @return bool Returns true if contract should receive updated validator's status. + function shouldValidatorBePenalized( + uint256 _nodeOperatorId, + uint256 proofSlotTimestamp, + bytes calldata publicKey, + uint256 eligibleToExitInSec + ) external view returns (bool); + + + /// @notice Returns the number of seconds after which a validator is considered late. + /// @return The exit deadline threshold in seconds. + function exitDeadlineThreshold(uint256 _nodeOperatorId) external view returns (uint256); + /// @notice Returns the type of the staking module function getType() external view returns (bytes32); @@ -168,13 +224,4 @@ interface IStakingModule { /// @dev IMPORTANT: this method SHOULD revert with empty error data ONLY because of "out of gas". /// Details about error data: https://docs.soliditylang.org/en/v0.8.9/control-structures.html#error-handling-assert-require-revert-and-exceptions function onWithdrawalCredentialsChanged() external; - - /// @dev Event to be emitted on StakingModule's nonce change - event NonceChanged(uint256 nonce); - - /// @dev Event to be emitted when a signing key is added to the StakingModule - event SigningKeyAdded(uint256 indexed nodeOperatorId, bytes pubkey); - - /// @dev Event to be emitted when a signing key is removed from the StakingModule - event SigningKeyRemoved(uint256 indexed nodeOperatorId, bytes pubkey); } diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index 22cd1bc94c..a5ffc254d7 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -11,6 +11,20 @@ contract StakingModule__MockForStakingRouter is IStakingModule { event Mock__OnRewardsMinted(uint256 _totalShares); event Mock__ExitedValidatorsCountUpdated(bytes _nodeOperatorIds, bytes _stuckValidatorsCounts); + event Mock__HandleActiveValidatorsExitingStatus( + uint256 nodeOperatorId, + uint256 proofSlotTimestamp, + bytes publicKeys, + bytes eligibleToExitInSec + ); + + event Mock__OnTriggerableExit( + uint256 _nodeOperatorId, + bytes publicKeys, + uint256 withdrawalRequestPaidFee, + uint256 exitType + ); + function getType() external view returns (bytes32) { return keccak256(abi.encodePacked("staking.module")); } @@ -253,4 +267,57 @@ contract StakingModule__MockForStakingRouter is IStakingModule { onWithdrawalCredentialsChangedShouldRevert = shouldRevert; onWithdrawalCredentialsChangedShouldRunOutGas = shouldRunOutGas; } + + bool private shouldBePenalized__mocked; + + function handleActiveValidatorsExitingStatus( + uint256 nodeOperatorId, + uint256 proofSlotTimestamp, + bytes calldata publicKeys, + bytes calldata eligibleToExitInSec + ) external { + emit Mock__HandleActiveValidatorsExitingStatus( + nodeOperatorId, + proofSlotTimestamp, + publicKeys, + eligibleToExitInSec + ); + } + + function onTriggerableExit( + uint256 _nodeOperatorId, + bytes calldata publicKeys, + uint256 withdrawalRequestPaidFee, + uint256 exitType + ) external { + emit Mock__OnTriggerableExit( + _nodeOperatorId, + publicKeys, + withdrawalRequestPaidFee, + exitType + ); + } + + function shouldValidatorBePenalized( + uint256 _nodeOperatorId, + uint256 proofSlotTimestamp, + bytes calldata publicKey, + uint256 eligibleToExitInSec + ) external view returns (bool) { + return shouldBePenalized__mocked; + } + + function mock__shouldValidatorBePenalized(bool shouldBePenalized) external { + shouldBePenalized__mocked = shouldBePenalized; + } + + uint256 private exitDeadlineThreshold__mocked; + + function exitDeadlineThreshold(uint256 _nodeOperatorId) external view returns (uint256) { + return exitDeadlineThreshold__mocked; + } + + function mock__exitDeadlineThreshold(uint256 threshold) external { + exitDeadlineThreshold__mocked = threshold; + } } From 3ea1bff95e9661e0c8ac05986f56360ed1f1e892 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 26 Mar 2025 16:43:09 +0100 Subject: [PATCH 051/405] refactor: update parameter names in IStakingModule interface for consistency --- contracts/0.8.9/interfaces/IStakingModule.sol | 40 +++++++++--------- .../StakingModule__MockForStakingRouter.sol | 42 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 0b419d755f..3250acfba1 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -18,42 +18,42 @@ interface IStakingModule { /// @dev This function is called to report the current exit-related status of validators belonging to a specific node operator. /// It accepts a batch of validator public keys, each associated with the duration (in seconds) they were eligible to exit but have not. /// This data could be used to trigger penalties for the node operator if validators have been non-exiting for too long. - /// @param nodeOperatorId The ID of the node operator whose validators statuses being delivered. - /// @param proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. - /// @param publicKeys Concatenated public keys of the validators being reported. - /// @param eligibleToExitInSec Array of durations (in seconds), each indicating how long a validator has been eligible to exit but hasn't. + /// @param _nodeOperatorId The ID of the node operator whose validators statuses being delivered. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. + /// @param _publicKeys Concatenated public keys of the validators being reported. + /// @param _eligibleToExitInSec Array of durations (in seconds), each indicating how long a validator has been eligible to exit but hasn't. function handleActiveValidatorsExitingStatus( - uint256 nodeOperatorId, - uint256 proofSlotTimestamp, - bytes calldata publicKeys, - bytes calldata eligibleToExitInSec + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes calldata _publicKeys, + bytes calldata _eligibleToExitInSec ) external; /// @notice Handles the triggerable exit events validator belonging to a specific node operator. /// @dev This function is called when a validator is exited using the triggerable exit request on EL. /// @param _nodeOperatorId The ID of the node operator. - /// @param publicKeys Concatenated public keys of the validators being reported. - /// @param withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. - /// @param exitType The type of exit being performed. + /// @param _publicKeys Concatenated public keys of the validators being reported. + /// @param _withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. + /// @param _exitType The type of exit being performed. /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. function onTriggerableExit( uint256 _nodeOperatorId, - bytes calldata publicKeys, - uint256 withdrawalRequestPaidFee, - uint256 exitType + bytes calldata _publicKeys, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType ) external; /// @notice Determines whether a validator exit status should be updated and will have affect on Node Operator. /// @param _nodeOperatorId The ID of the node operator. - /// @param proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. - /// @param publicKey Validator's public key. - /// @param eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. + /// @param _publicKey Validator's public key. + /// @param _eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. /// @return bool Returns true if contract should receive updated validator's status. function shouldValidatorBePenalized( uint256 _nodeOperatorId, - uint256 proofSlotTimestamp, - bytes calldata publicKey, - uint256 eligibleToExitInSec + uint256 _proofSlotTimestamp, + bytes calldata _publicKey, + uint256 _eligibleToExitInSec ) external view returns (bool); diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index a5ffc254d7..37b9ed669c 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -271,44 +271,44 @@ contract StakingModule__MockForStakingRouter is IStakingModule { bool private shouldBePenalized__mocked; function handleActiveValidatorsExitingStatus( - uint256 nodeOperatorId, - uint256 proofSlotTimestamp, - bytes calldata publicKeys, - bytes calldata eligibleToExitInSec + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes calldata _publicKeys, + bytes calldata _eligibleToExitInSec ) external { emit Mock__HandleActiveValidatorsExitingStatus( - nodeOperatorId, - proofSlotTimestamp, - publicKeys, - eligibleToExitInSec + _nodeOperatorId, + _proofSlotTimestamp, + _publicKeys, + _eligibleToExitInSec ); } function onTriggerableExit( uint256 _nodeOperatorId, - bytes calldata publicKeys, - uint256 withdrawalRequestPaidFee, - uint256 exitType + bytes calldata _publicKeys, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType ) external { emit Mock__OnTriggerableExit( _nodeOperatorId, - publicKeys, - withdrawalRequestPaidFee, - exitType + _publicKeys, + _withdrawalRequestPaidFee, + _exitType ); } function shouldValidatorBePenalized( uint256 _nodeOperatorId, - uint256 proofSlotTimestamp, - bytes calldata publicKey, - uint256 eligibleToExitInSec + uint256 _proofSlotTimestamp, + bytes calldata _publicKey, + uint256 _eligibleToExitInSec ) external view returns (bool) { return shouldBePenalized__mocked; } - function mock__shouldValidatorBePenalized(bool shouldBePenalized) external { - shouldBePenalized__mocked = shouldBePenalized; + function mock__shouldValidatorBePenalized(bool _shouldBePenalized) external { + shouldBePenalized__mocked = _shouldBePenalized; } uint256 private exitDeadlineThreshold__mocked; @@ -317,7 +317,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { return exitDeadlineThreshold__mocked; } - function mock__exitDeadlineThreshold(uint256 threshold) external { - exitDeadlineThreshold__mocked = threshold; + function mock__exitDeadlineThreshold(uint256 _threshold) external { + exitDeadlineThreshold__mocked = _threshold; } } From adb717953cf58d1b066b9607c3ed449d1cfccd02 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 28 Mar 2025 11:53:32 +0100 Subject: [PATCH 052/405] refactor: impl new ISM interface in SR --- contracts/0.8.9/StakingRouter.sol | 85 +++++++++++------- contracts/0.8.9/interfaces/IStakingModule.sol | 17 ++-- .../stakingRouter.module-sync.test.ts | 87 ------------------- 3 files changed, 61 insertions(+), 128 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index be08a6eaaf..e5fa6c2b70 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -437,18 +437,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// distribute new stake and staking fees between the modules. There can only be single call of this function /// per oracle reporting frame. /// - /// 2. In the first part of the second data submission phase, the oracle calls - /// `StakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator` on the staking router which passes the - /// counts by node operator to the staking module by calling `IStakingModule.updateStuckValidatorsCount`. - /// This can be done multiple times for the same module, passing data for different subsets of node operators. - /// - /// 3. In the second part of the second data submission phase, the oracle calls + /// 2. In the second part of the second data submission phase, the oracle calls /// `StakingRouter.reportStakingModuleExitedValidatorsCountByNodeOperator` on the staking router which passes /// the counts by node operator to the staking module by calling `IStakingModule.updateExitedValidatorsCount`. /// This can be done multiple times for the same module, passing data for different subsets of node /// operators. /// - /// 4. At the end of the second data submission phase, it's expected for the aggregate exited validators count + /// 3. At the end of the second data submission phase, it's expected for the aggregate exited validators count /// across all module's node operators (stored in the module) to match the total count for this module /// (stored in the staking router). However, it might happen that the second phase of data submission doesn't /// finish until the new oracle reporting frame is started, in which case staking router will emit a warning @@ -457,7 +452,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// the exited and maybe stuck validator counts during the whole reporting frame. Handling this condition is /// the responsibility of each staking module. /// - /// 5. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the stuck + /// 4. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the stuck /// and exited validator counts per node operator for the current reporting frame, the oracle calls /// `StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished` which, in turn, calls /// `IStakingModule.onExitedAndStuckValidatorsCountsUpdated` on all modules. @@ -638,27 +633,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } - /// @notice Updates stuck validators counts per node operator for the staking module with - /// the specified id. See the docs for `updateExitedValidatorsCountByStakingModule` for the - /// description of the overall update process. - /// - /// @param _stakingModuleId The id of the staking modules to be updated. - /// @param _nodeOperatorIds Ids of the node operators to be updated. - /// @param _stuckValidatorsCounts New counts of stuck validators for the specified node operators. - /// - /// @dev The function is restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. - function reportStakingModuleStuckValidatorsCountByNodeOperator( - uint256 _stakingModuleId, - bytes calldata _nodeOperatorIds, - bytes calldata _stuckValidatorsCounts - ) - external - onlyRole(REPORT_EXITED_VALIDATORS_ROLE) - { - _checkValidatorsByNodeOperatorReportData(_nodeOperatorIds, _stuckValidatorsCounts); - _getIStakingModuleById(_stakingModuleId).updateStuckValidatorsCount(_nodeOperatorIds, _stuckValidatorsCounts); - } - /// @notice Finalizes the reporting of the exited and stuck validators counts for the current /// reporting frame. /// @@ -1503,4 +1477,57 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version function _getStakingModuleSummary(IStakingModule stakingModule) internal view returns (uint256, uint256, uint256) { return stakingModule.getStakingModuleSummary(); } + + /// @notice Handles tracking and penalization logic for validators that remain active beyond their eligible exit window. + /// @dev This function is called to report the current exit-related status of validators belonging to a specific node operator. + /// It accepts a batch of validator public keys, each associated with the duration (in seconds) they were eligible to exit but have not. + /// This data could be used to trigger penalties for the node operator if validators have been non-exiting for too long. + /// @param _stakingModuleId The ID of the staking module. + /// @param _nodeOperatorId The ID of the node operator whose validators statuses being delivered. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. + /// @param _publicKeys Concatenated public keys of the validators being reported. + /// @param _eligibleToExitInSec Array of durations (in seconds), each indicating how long a validator has been eligible to exit but hasn't. + function handleActiveValidatorsExitingStatus( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes calldata _publicKeys, + bytes calldata _eligibleToExitInSec + ) + external + onlyRole(REPORT_EXITED_VALIDATORS_ROLE) + { + _getIStakingModuleById(_stakingModuleId).handleActiveValidatorsExitingStatus( + _nodeOperatorId, + _proofSlotTimestamp, + _publicKeys, + _eligibleToExitInSec + ); + } + + /// @notice Handles the triggerable exit events validator belonging to a specific node operator. + /// @dev This function is called when a validator is exited using the triggerable exit request on EL. + /// @param _stakingModuleId The ID of the staking module. + /// @param _nodeOperatorId The ID of the node operator. + /// @param _publicKeys Concatenated public keys of the validators being reported. + /// @param _withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. + /// @param _exitType The type of exit being performed. + /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. + function onTriggerableExit( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + bytes calldata _publicKeys, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType + ) + external + onlyRole(REPORT_EXITED_VALIDATORS_ROLE) + { + _getIStakingModuleById(_stakingModuleId).onTriggerableExit( + _nodeOperatorId, + _publicKeys, + _withdrawalRequestPaidFee, + _exitType + ); + } } diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 3250acfba1..5b013bf2b5 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -15,8 +15,9 @@ interface IStakingModule { event SigningKeyRemoved(uint256 indexed nodeOperatorId, bytes pubkey); /// @notice Handles tracking and penalization logic for validators that remain active beyond their eligible exit window. - /// @dev This function is called to report the current exit-related status of validators belonging to a specific node operator. - /// It accepts a batch of validator public keys, each associated with the duration (in seconds) they were eligible to exit but have not. + /// @dev This function is called by the StakingRouter to report the current exit-related status of validators + /// belonging to a specific node operator. It accepts a batch of validator public keys, each associated + /// with the duration (in seconds) they were eligible to exit but have not. /// This data could be used to trigger penalties for the node operator if validators have been non-exiting for too long. /// @param _nodeOperatorId The ID of the node operator whose validators statuses being delivered. /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. @@ -30,7 +31,8 @@ interface IStakingModule { ) external; /// @notice Handles the triggerable exit events validator belonging to a specific node operator. - /// @dev This function is called when a validator is exited using the triggerable exit request on EL. + /// @dev This function is called by the StakingRouter when a validator is exited using the triggerable + /// exit request on Execution Layer. /// @param _nodeOperatorId The ID of the node operator. /// @param _publicKeys Concatenated public keys of the validators being reported. /// @param _withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. @@ -150,15 +152,6 @@ interface IStakingModule { bytes calldata _vettedSigningKeysCounts ) external; - /// @notice Updates the number of the validators of the given node operator that were requested - /// to exit but failed to do so in the max allowed time - /// @param _nodeOperatorIds bytes packed array of the node operators id - /// @param _stuckValidatorsCounts bytes packed array of the new number of STUCK validators for the node operators - function updateStuckValidatorsCount( - bytes calldata _nodeOperatorIds, - bytes calldata _stuckValidatorsCounts - ) external; - /// @notice Updates the number of the validators in the EXITED state for node operator with given id /// @param _nodeOperatorIds bytes packed array of the node operators id /// @param _exitedValidatorsCounts bytes packed array of the new number of EXITED validators for the node operators diff --git a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts index f1131e727b..0f19594f56 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts @@ -777,93 +777,6 @@ describe("StakingRouter.sol:module-sync", () => { }); }); - context("reportStakingModuleStuckValidatorsCountByNodeOperator", () => { - const NODE_OPERATOR_IDS = bigintToHex(1n, true, 8); - const STUCK_VALIDATOR_COUNTS = bigintToHex(100n, true, 16); - - it("Reverts if the caller does not have the role", async () => { - await expect( - stakingRouter - .connect(user) - .reportStakingModuleStuckValidatorsCountByNodeOperator(moduleId, NODE_OPERATOR_IDS, STUCK_VALIDATOR_COUNTS), - ).to.be.revertedWithOZAccessControlError(user.address, await stakingRouter.REPORT_EXITED_VALIDATORS_ROLE()); - }); - - it("Reverts if the node operators ids are packed incorrectly", async () => { - const incorrectlyPackedNodeOperatorIds = bufToHex(new Uint8Array([1]), true, 7); - - await expect( - stakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator( - moduleId, - incorrectlyPackedNodeOperatorIds, - STUCK_VALIDATOR_COUNTS, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "InvalidReportData") - .withArgs(3n); - }); - - it("Reverts if the validator counts are packed incorrectly", async () => { - const incorrectlyPackedValidatorCounts = bufToHex(new Uint8Array([100]), true, 15); - - await expect( - stakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator( - moduleId, - NODE_OPERATOR_IDS, - incorrectlyPackedValidatorCounts, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "InvalidReportData") - .withArgs(3n); - }); - - it("Reverts if the number of node operators does not match validator counts", async () => { - const tooManyValidatorCounts = STUCK_VALIDATOR_COUNTS + bigintToHex(101n, false, 16); - - await expect( - stakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator( - moduleId, - NODE_OPERATOR_IDS, - tooManyValidatorCounts, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "InvalidReportData") - .withArgs(2n); - }); - - it("Reverts if the number of node operators does not match validator counts", async () => { - const tooManyValidatorCounts = STUCK_VALIDATOR_COUNTS + bigintToHex(101n, false, 16); - - await expect( - stakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator( - moduleId, - NODE_OPERATOR_IDS, - tooManyValidatorCounts, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "InvalidReportData") - .withArgs(2n); - }); - - it("Reverts if the node operators ids is empty", async () => { - await expect(stakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator(moduleId, "0x", "0x")) - .to.be.revertedWithCustomError(stakingRouter, "InvalidReportData") - .withArgs(1n); - }); - - it("Updates stuck validators count on the module", async () => { - await expect( - stakingRouter.reportStakingModuleStuckValidatorsCountByNodeOperator( - moduleId, - NODE_OPERATOR_IDS, - STUCK_VALIDATOR_COUNTS, - ), - ) - .to.emit(stakingModule, "Mock__StuckValidatorsCountUpdated") - .withArgs(NODE_OPERATOR_IDS, STUCK_VALIDATOR_COUNTS); - }); - }); - context("onValidatorsCountsByNodeOperatorReportingFinished", () => { it("Reverts if the caller does not have the role", async () => { await expect( From 68092722f190403e14c67915f947a8d2ea4b08de Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 28 Mar 2025 14:27:09 +0100 Subject: [PATCH 053/405] refactor: update validator count handling in StakingRouter and IStakingModule interfaces --- contracts/0.8.9/StakingRouter.sol | 40 +++++++------------ contracts/0.8.9/interfaces/IStakingModule.sol | 6 +-- .../StakingModule__MockForStakingRouter.sol | 28 ++++++------- 3 files changed, 30 insertions(+), 44 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index e5fa6c2b70..d31af179cd 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -60,8 +60,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version error StakingModuleWrongName(); error UnexpectedCurrentValidatorsCount( uint256 currentModuleExitedValidatorsCount, - uint256 currentNodeOpExitedValidatorsCount, - uint256 currentNodeOpStuckValidatorsCount + uint256 currentNodeOpExitedValidatorsCount ); error UnexpectedFinalExitedValidatorsCount ( uint256 newModuleTotalExitedValidatorsCount, @@ -133,6 +132,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version bytes32 public constant STAKING_MODULE_MANAGE_ROLE = keccak256("STAKING_MODULE_MANAGE_ROLE"); bytes32 public constant STAKING_MODULE_UNVETTING_ROLE = keccak256("STAKING_MODULE_UNVETTING_ROLE"); bytes32 public constant REPORT_EXITED_VALIDATORS_ROLE = keccak256("REPORT_EXITED_VALIDATORS_ROLE"); + bytes32 public constant REPORT_EXITED_VALIDATORS_STATUS = keccak256("REPORT_EXITED_VALIDATORS_STATUS"); bytes32 public constant UNSAFE_SET_EXITED_VALIDATORS_ROLE = keccak256("UNSAFE_SET_EXITED_VALIDATORS_ROLE"); bytes32 public constant REPORT_REWARDS_MINTED_ROLE = keccak256("REPORT_REWARDS_MINTED_ROLE"); @@ -455,7 +455,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// 4. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the stuck /// and exited validator counts per node operator for the current reporting frame, the oracle calls /// `StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished` which, in turn, calls - /// `IStakingModule.onExitedAndStuckValidatorsCountsUpdated` on all modules. + /// `IStakingModule.onExitedValidatorsCountsUpdated` on all modules. /// /// @dev The function is restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. function updateExitedValidatorsCountByStakingModule( @@ -540,15 +540,10 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @notice The expected current number of exited validators of the node operator /// that is being corrected. uint256 currentNodeOperatorExitedValidatorsCount; - /// @notice The expected current number of stuck validators of the node operator - /// that is being corrected. - uint256 currentNodeOperatorStuckValidatorsCount; /// @notice The corrected number of exited validators of the module. uint256 newModuleExitedValidatorsCount; /// @notice The corrected number of exited validators of the node operator. uint256 newNodeOperatorExitedValidatorsCount; - /// @notice The corrected number of stuck validators of the node operator. - uint256 newNodeOperatorStuckValidatorsCount; } /// @notice Sets exited validators count for the given module and given node operator in that module @@ -559,7 +554,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// /// @param _stakingModuleId Id of the staking module. /// @param _nodeOperatorId Id of the node operator. - /// @param _triggerUpdateFinish Whether to call `onExitedAndStuckValidatorsCountsUpdated` on the module + /// @param _triggerUpdateFinish Whether to call `onExitedValidatorsCountsUpdated` on the module /// after applying the corrections. /// @param _correction See the docs for the `ValidatorsCountsCorrection` struct. /// @@ -582,7 +577,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ( /* uint256 targetLimitMode */, /* uint256 targetValidatorsCount */, - uint256 stuckValidatorsCount, + /* uint256 stuckValidatorsCount, */, /* uint256 refundedValidatorsCount */, /* uint256 stuckPenaltyEndTimestamp */, uint256 totalExitedValidators, @@ -591,13 +586,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ) = stakingModule.getNodeOperatorSummary(_nodeOperatorId); if (_correction.currentModuleExitedValidatorsCount != stakingModuleState.exitedValidatorsCount || - _correction.currentNodeOperatorExitedValidatorsCount != totalExitedValidators || - _correction.currentNodeOperatorStuckValidatorsCount != stuckValidatorsCount + _correction.currentNodeOperatorExitedValidatorsCount != totalExitedValidators ) { revert UnexpectedCurrentValidatorsCount( stakingModuleState.exitedValidatorsCount, - totalExitedValidators, - stuckValidatorsCount + totalExitedValidators ); } @@ -605,8 +598,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version stakingModule.unsafeUpdateValidatorsCount( _nodeOperatorId, - _correction.newNodeOperatorExitedValidatorsCount, - _correction.newNodeOperatorStuckValidatorsCount + _correction.newNodeOperatorExitedValidatorsCount ); ( @@ -629,7 +621,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ); } - stakingModule.onExitedAndStuckValidatorsCountsUpdated(); + stakingModule.onExitedValidatorsCountsUpdated(); } } @@ -657,13 +649,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version (uint256 exitedValidatorsCount, , ) = _getStakingModuleSummary(moduleContract); if (exitedValidatorsCount == stakingModule.exitedValidatorsCount) { // oracle finished updating exited validators for all node ops - try moduleContract.onExitedAndStuckValidatorsCountsUpdated() {} + try moduleContract.onExitedValidatorsCountsUpdated() {} catch (bytes memory lowLevelRevertData) { /// @dev This check is required to prevent incorrect gas estimation of the method. /// Without it, Ethereum nodes that use binary search for gas estimation may - /// return an invalid value when the onExitedAndStuckValidatorsCountsUpdated() + /// return an invalid value when the onExitedValidatorsCountsUpdated() /// reverts because of the "out of gas" error. Here we assume that the - /// onExitedAndStuckValidatorsCountsUpdated() method doesn't have reverts with + /// onExitedValidatorsCountsUpdated() method doesn't have reverts with /// empty error data except "out of gas". if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); emit ExitedAndStuckValidatorsCountsUpdateFailed( @@ -837,18 +829,16 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ( uint256 targetLimitMode, uint256 targetValidatorsCount, - uint256 stuckValidatorsCount, + /* uint256 stuckValidatorsCount */, uint256 refundedValidatorsCount, - uint256 stuckPenaltyEndTimestamp, + /* uint256 stuckPenaltyEndTimestamp */, uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount ) = stakingModule.getNodeOperatorSummary(_nodeOperatorId); summary.targetLimitMode = targetLimitMode; summary.targetValidatorsCount = targetValidatorsCount; - summary.stuckValidatorsCount = stuckValidatorsCount; summary.refundedValidatorsCount = refundedValidatorsCount; - summary.stuckPenaltyEndTimestamp = stuckPenaltyEndTimestamp; summary.totalExitedValidators = totalExitedValidators; summary.totalDepositedValidators = totalDepositedValidators; summary.depositableValidatorsCount = depositableValidatorsCount; @@ -1495,7 +1485,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version bytes calldata _eligibleToExitInSec ) external - onlyRole(REPORT_EXITED_VALIDATORS_ROLE) + onlyRole(REPORT_EXITED_VALIDATORS_STATUS) { _getIStakingModuleById(_stakingModuleId).handleActiveValidatorsExitingStatus( _nodeOperatorId, diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 5b013bf2b5..6d6db9685f 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -179,11 +179,9 @@ interface IStakingModule { /// 'unsafely' means that this method can both increase and decrease exited and stuck counters /// @param _nodeOperatorId Id of the node operator /// @param _exitedValidatorsCount New number of EXITED validators for the node operator - /// @param _stuckValidatorsCount New number of STUCK validator for the node operator function unsafeUpdateValidatorsCount( uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount, - uint256 _stuckValidatorsCount + uint256 _exitedValidatorsCount ) external; /// @notice Obtains deposit data to be used by StakingRouter to deposit to the Ethereum Deposit @@ -208,7 +206,7 @@ interface IStakingModule { /// /// @dev IMPORTANT: this method SHOULD revert with empty error data ONLY because of "out of gas". /// Details about error data: https://docs.soliditylang.org/en/v0.8.9/control-structures.html#error-handling-assert-require-revert-and-exceptions - function onExitedAndStuckValidatorsCountsUpdated() external; + function onExitedValidatorsCountsUpdated() external; /// @notice Called by StakingRouter when withdrawal credentials are changed. /// @dev This method MUST discard all StakingModule's unused deposit data cause they become diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index 37b9ed669c..28cbd74ece 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -208,16 +208,14 @@ contract StakingModule__MockForStakingRouter is IStakingModule { event Mock__ValidatorsCountUnsafelyUpdated( uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount, - uint256 _stuckValidatorsCoun + uint256 _exitedValidatorsCount ); function unsafeUpdateValidatorsCount( uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount, - uint256 _stuckValidatorsCount + uint256 _exitedValidatorsCount ) external { - emit Mock__ValidatorsCountUnsafelyUpdated(_nodeOperatorId, _exitedValidatorsCount, _stuckValidatorsCount); + emit Mock__ValidatorsCountUnsafelyUpdated(_nodeOperatorId, _exitedValidatorsCount); } function obtainDepositData( @@ -228,24 +226,24 @@ contract StakingModule__MockForStakingRouter is IStakingModule { signatures = new bytes(96 * _depositsCount); } - event Mock__onExitedAndStuckValidatorsCountsUpdated(); + event Mock__onExitedValidatorsCountsUpdated(); - bool private onExitedAndStuckValidatorsCountsUpdatedShouldRevert = false; - bool private onExitedAndStuckValidatorsCountsUpdatedShouldRunOutGas = false; + bool private onExitedValidatorsCountsUpdatedShouldRevert = false; + bool private onExitedValidatorsCountsUpdatedShouldRunOutGas = false; - function onExitedAndStuckValidatorsCountsUpdated() external { - require(!onExitedAndStuckValidatorsCountsUpdatedShouldRevert, "revert reason"); + function onExitedValidatorsCountsUpdated() external { + require(!onExitedValidatorsCountsUpdatedShouldRevert, "revert reason"); - if (onExitedAndStuckValidatorsCountsUpdatedShouldRunOutGas) { + if (onExitedValidatorsCountsUpdatedShouldRunOutGas) { revert(); } - emit Mock__onExitedAndStuckValidatorsCountsUpdated(); + emit Mock__onExitedValidatorsCountsUpdated(); } - function mock__onExitedAndStuckValidatorsCountsUpdated(bool shouldRevert, bool shouldRunOutGas) external { - onExitedAndStuckValidatorsCountsUpdatedShouldRevert = shouldRevert; - onExitedAndStuckValidatorsCountsUpdatedShouldRunOutGas = shouldRunOutGas; + function mock__onExitedValidatorsCountsUpdated(bool shouldRevert, bool shouldRunOutGas) external { + onExitedValidatorsCountsUpdatedShouldRevert = shouldRevert; + onExitedValidatorsCountsUpdatedShouldRunOutGas = shouldRunOutGas; } event Mock__WithdrawalCredentialsChanged(); From f8d35f2b2b126aa00862e5f1453d15100d22f3d6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 28 Mar 2025 14:30:01 +0100 Subject: [PATCH 054/405] refactor: rename REPORT_EXITED_VALIDATORS_STATUS constant to REPORT_EXITED_VALIDATORS_STATUS_ROLE for clarity --- contracts/0.8.9/StakingRouter.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index d31af179cd..70a01ec795 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -132,7 +132,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version bytes32 public constant STAKING_MODULE_MANAGE_ROLE = keccak256("STAKING_MODULE_MANAGE_ROLE"); bytes32 public constant STAKING_MODULE_UNVETTING_ROLE = keccak256("STAKING_MODULE_UNVETTING_ROLE"); bytes32 public constant REPORT_EXITED_VALIDATORS_ROLE = keccak256("REPORT_EXITED_VALIDATORS_ROLE"); - bytes32 public constant REPORT_EXITED_VALIDATORS_STATUS = keccak256("REPORT_EXITED_VALIDATORS_STATUS"); + bytes32 public constant REPORT_EXITED_VALIDATORS_STATUS_ROLE = keccak256("REPORT_EXITED_VALIDATORS_STATUS_ROLE"); bytes32 public constant UNSAFE_SET_EXITED_VALIDATORS_ROLE = keccak256("UNSAFE_SET_EXITED_VALIDATORS_ROLE"); bytes32 public constant REPORT_REWARDS_MINTED_ROLE = keccak256("REPORT_REWARDS_MINTED_ROLE"); @@ -1485,7 +1485,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version bytes calldata _eligibleToExitInSec ) external - onlyRole(REPORT_EXITED_VALIDATORS_STATUS) + onlyRole(REPORT_EXITED_VALIDATORS_STATUS_ROLE) { _getIStakingModuleById(_stakingModuleId).handleActiveValidatorsExitingStatus( _nodeOperatorId, From bf6aad9a0946ead1a3b06efab262f3059dadaaaf Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 28 Mar 2025 14:43:07 +0100 Subject: [PATCH 055/405] refactor: update documentation for validator exit handling in StakingRouter and IStakingModule interfaces --- contracts/0.8.9/StakingRouter.sol | 32 +++++++-------- contracts/0.8.9/interfaces/IStakingModule.sol | 40 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 70a01ec795..4e8878168c 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1468,20 +1468,20 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version return stakingModule.getStakingModuleSummary(); } - /// @notice Handles tracking and penalization logic for validators that remain active beyond their eligible exit window. - /// @dev This function is called to report the current exit-related status of validators belonging to a specific node operator. - /// It accepts a batch of validator public keys, each associated with the duration (in seconds) they were eligible to exit but have not. - /// This data could be used to trigger penalties for the node operator if validators have been non-exiting for too long. + /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. + /// @dev This function is called to report the current exit-related status of a validator belonging to a specific node operator. + /// It accepts a validator's public key, associated with the duration (in seconds) it was eligible to exit but has not exited. + /// This data could be used to trigger penalties for the node operator if the validator has been non-exiting for too long. /// @param _stakingModuleId The ID of the staking module. - /// @param _nodeOperatorId The ID of the node operator whose validators statuses being delivered. - /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. - /// @param _publicKeys Concatenated public keys of the validators being reported. - /// @param _eligibleToExitInSec Array of durations (in seconds), each indicating how long a validator has been eligible to exit but hasn't. + /// @param _nodeOperatorId The ID of the node operator whose validator status is being delivered. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. + /// @param _publicKey The public key of the validator being reported. + /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. function handleActiveValidatorsExitingStatus( uint256 _stakingModuleId, uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, - bytes calldata _publicKeys, + bytes calldata _publicKey, bytes calldata _eligibleToExitInSec ) external @@ -1490,23 +1490,23 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version _getIStakingModuleById(_stakingModuleId).handleActiveValidatorsExitingStatus( _nodeOperatorId, _proofSlotTimestamp, - _publicKeys, + _publicKey, _eligibleToExitInSec ); } - /// @notice Handles the triggerable exit events validator belonging to a specific node operator. - /// @dev This function is called when a validator is exited using the triggerable exit request on EL. + /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. + /// @dev This function is called when a validator is exited using the triggerable exit request on the Execution Layer (EL). /// @param _stakingModuleId The ID of the staking module. /// @param _nodeOperatorId The ID of the node operator. - /// @param _publicKeys Concatenated public keys of the validators being reported. - /// @param _withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. + /// @param _publicKey The public key of the validator being reported. + /// @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the Execution Layer (EL). /// @param _exitType The type of exit being performed. /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. function onTriggerableExit( uint256 _stakingModuleId, uint256 _nodeOperatorId, - bytes calldata _publicKeys, + bytes calldata _publicKey, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) @@ -1515,7 +1515,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version { _getIStakingModuleById(_stakingModuleId).onTriggerableExit( _nodeOperatorId, - _publicKeys, + _publicKey, _withdrawalRequestPaidFee, _exitType ); diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 6d6db9685f..cbe5fd509a 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -14,43 +14,43 @@ interface IStakingModule { /// @dev Event to be emitted when a signing key is removed from the StakingModule event SigningKeyRemoved(uint256 indexed nodeOperatorId, bytes pubkey); - /// @notice Handles tracking and penalization logic for validators that remain active beyond their eligible exit window. - /// @dev This function is called by the StakingRouter to report the current exit-related status of validators - /// belonging to a specific node operator. It accepts a batch of validator public keys, each associated - /// with the duration (in seconds) they were eligible to exit but have not. - /// This data could be used to trigger penalties for the node operator if validators have been non-exiting for too long. - /// @param _nodeOperatorId The ID of the node operator whose validators statuses being delivered. - /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. - /// @param _publicKeys Concatenated public keys of the validators being reported. - /// @param _eligibleToExitInSec Array of durations (in seconds), each indicating how long a validator has been eligible to exit but hasn't. + /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. + /// @dev This function is called by the StakingRouter to report the current exit-related status of a validator + /// belonging to a specific node operator. It accepts a validator's public key, associated + /// with the duration (in seconds) it was eligible to exit but has not exited. + /// This data could be used to trigger penalties for the node operator if the validator has exceeded the allowed exit window. + /// @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. + /// @param _publicKey The public key of the validator being reported. + /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. function handleActiveValidatorsExitingStatus( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, - bytes calldata _publicKeys, + bytes calldata _publicKey, bytes calldata _eligibleToExitInSec ) external; - /// @notice Handles the triggerable exit events validator belonging to a specific node operator. + /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. /// @dev This function is called by the StakingRouter when a validator is exited using the triggerable - /// exit request on Execution Layer. + /// exit request on the Execution Layer (EL). /// @param _nodeOperatorId The ID of the node operator. - /// @param _publicKeys Concatenated public keys of the validators being reported. - /// @param _withdrawalRequestPaidFee Fee amount paid to send withdrawal request on EL. + /// @param _publicKey The public key of the validator being reported. + /// @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the Execution Layer (EL). /// @param _exitType The type of exit being performed. /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. function onTriggerableExit( uint256 _nodeOperatorId, - bytes calldata _publicKeys, + bytes calldata _publicKey, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external; - /// @notice Determines whether a validator exit status should be updated and will have affect on Node Operator. + /// @notice Determines whether a validator's exit status should be updated and will have an effect on the Node Operator. /// @param _nodeOperatorId The ID of the node operator. - /// @param _proofSlotTimestamp The timestamp (slot time) when the validators were last known to be in an active ongoing state. - /// @param _publicKey Validator's public key. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. + /// @param _publicKey The public key of the validator. /// @param _eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. - /// @return bool Returns true if contract should receive updated validator's status. + /// @return bool Returns true if the contract should receive the updated status of the validator. function shouldValidatorBePenalized( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, @@ -58,8 +58,8 @@ interface IStakingModule { uint256 _eligibleToExitInSec ) external view returns (bool); - /// @notice Returns the number of seconds after which a validator is considered late. + /// @param _nodeOperatorId The ID of the node operator. /// @return The exit deadline threshold in seconds. function exitDeadlineThreshold(uint256 _nodeOperatorId) external view returns (uint256); From 795df7c93a23979321f2d0fe8d8eff1c34b889ce Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 3 Apr 2025 01:19:47 +0400 Subject: [PATCH 056/405] feat: deliver exit hash by trustfull entitites & deliver reports --- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 155 +++++++++ contracts/0.8.9/oracle/ValidatorsExitBus.sol | 251 +++++++++++++- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 54 ++- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 326 ++++++++++++++++++ ...alidator-exit-bus-oracle.happyPath.test.ts | 12 +- ...r-exit-bus-oracle.submitReportData.test.ts | 65 ++++ ...t-bus-oracle.triggerExitHashVerify.test.ts | 12 +- ...it-bus-oracle.triggerExitsDirectly.test.ts | 137 ++++++++ 8 files changed, 948 insertions(+), 64 deletions(-) create mode 100644 contracts/0.8.9/lib/ReportExitLimitUtils.sol create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol new file mode 100644 index 0000000000..2b2e1793e1 --- /dev/null +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import { UnstructuredStorage } from "./UnstructuredStorage.sol"; + +// MSB ------------------------------------------------------------------------------> LSB +// 256______________160____________________________128_______________32____________________________ 0 +// |_________________|______________________________|_________________|_____________________________| +// | maxExitRequests | maxExitRequestsGrowthBlocks | prevExitRequests | prevExitRequestsBlockNumber | +// |<--- 96 bits --->|<---------- 32 bits -------->|<--- 96 bits ---->|<----- 32 bits ------------->| +// + +struct ExitRequestLimitData { + uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests + uint96 prevExitRequestsLimit; // limit value (<= `maxExitRequestLimit`) obtained on the previous exit request + uint32 maxExitRequestsLimitGrowthBlocks; // limit regeneration speed expressed in blocks + uint96 maxExitRequestsLimit; // maximum limit value +} + + +library ReportExitLimitUtilsStorage { + using UnstructuredStorage for bytes32; + + uint256 internal constant MAX_EXIT_REQUESTS_LIMIT_OFFSET = 160; + uint256 internal constant MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET = 128; + uint256 internal constant PREV_EXIT_REQUESTS_LIMIT_OFFSET = 32; + uint256 internal constant PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET = 0; + + function getStorageExitRequestLimit(bytes32 _position) + internal + view + returns (ExitRequestLimitData memory data) + { + uint256 slotValue = _position.getStorageUint256(); + + data.prevExitRequestsBlockNumber = uint32(slotValue >> PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET); + data.prevExitRequestsLimit = uint96(slotValue >> PREV_EXIT_REQUESTS_LIMIT_OFFSET); + data.maxExitRequestsLimitGrowthBlocks = uint32(slotValue >> MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET); + data.maxExitRequestsLimit = uint96(slotValue >> MAX_EXIT_REQUESTS_LIMIT_OFFSET); + } + + function setStorageExitRequestLimit(bytes32 _position, ExitRequestLimitData memory _data) internal { + _position.setStorageUint256( + (uint256(_data.prevExitRequestsBlockNumber) << PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET) | + (uint256(_data.prevExitRequestsLimit) << PREV_EXIT_REQUESTS_LIMIT_OFFSET) | + (uint256(_data.maxExitRequestsLimitGrowthBlocks) << MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET) | + (uint256(_data.maxExitRequestsLimit) << MAX_EXIT_REQUESTS_LIMIT_OFFSET) + ); + } +} + +library ReportExitLimitUtils { + + /** + * @notice Calculate exit requests limit + * @dev using `_constGasMin` to make gas consumption independent of the current block number + */ + function calculateCurrentExitRequestLimit(ExitRequestLimitData memory _data) internal view returns(uint256 limit) { + uint256 exitRequestLimitIncPerBlock; + if (_data.maxExitRequestsLimitGrowthBlocks != 0) { + exitRequestLimitIncPerBlock = _data.maxExitRequestsLimit / _data.maxExitRequestsLimitGrowthBlocks; + } + + uint256 blocksPassed = block.number - _data.prevExitRequestsBlockNumber; + uint256 projectedLimit = _data.prevExitRequestsLimit + blocksPassed * exitRequestLimitIncPerBlock; + + limit = _constGasMin( + projectedLimit, + _data.maxExitRequestsLimit + ); + } + + + /** + * @notice update exit requests limit repr after exit request + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data exit request limit struct + * @param _newPrevExitRequestsLimit new value for the `prevExitRequests` field + */ + function updatePrevExitRequestsLimit( + ExitRequestLimitData memory _data, + uint256 _newPrevExitRequestsLimit + ) internal view returns (ExitRequestLimitData memory) { + // assert(_data.prevExitRequestsBlockNumber != 0); + + _data.prevExitRequestsLimit = uint96(_newPrevExitRequestsLimit); + _data.prevExitRequestsBlockNumber = uint32(block.number); + + return _data; + } + + + /** + * @notice update exit request limit repr with the desired limits + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data exit request limit struct + * @param _maxExitRequestsLimit exit request limit max value + * @param _exitRequestsLimitIncreasePerBlock exit request limit increase (restoration) per block + */ + function setExitReportLimit( + ExitRequestLimitData memory _data, + uint256 _maxExitRequestsLimit, + uint256 _exitRequestsLimitIncreasePerBlock + ) internal view returns (ExitRequestLimitData memory) { + require(_maxExitRequestsLimit != 0, "ZERO_MAX_EXIT_REQUESTS_LIMIT"); + require(_maxExitRequestsLimit <= type(uint96).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); + require(_maxExitRequestsLimit >= _exitRequestsLimitIncreasePerBlock, "TOO_LARGE_LIMIT_INCREASE"); + require( + (_exitRequestsLimitIncreasePerBlock == 0) + || (_maxExitRequestsLimit / _exitRequestsLimitIncreasePerBlock <= type(uint32).max), + "TOO_SMALL_LIMIT_INCREASE" + ); + + if ( + _data.prevExitRequestsBlockNumber == 0 || + _data.maxExitRequestsLimit == 0 || + _maxExitRequestsLimit < _data.prevExitRequestsLimit + ) { + _data.prevExitRequestsLimit = uint96(_maxExitRequestsLimit); + } + _data.maxExitRequestsLimitGrowthBlocks= + _exitRequestsLimitIncreasePerBlock != 0 ? uint32(_maxExitRequestsLimit/ _exitRequestsLimitIncreasePerBlock) : 0; + + _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); + + // TODO: check + _data.prevExitRequestsBlockNumber = uint32(block.number); + + return _data; + } + + /** + * TODO: discuss this part + * @notice check if max exit request limit is set. Otherwise there are no limits on exits + */ + function isExitReportLimitSet(ExitRequestLimitData memory _data) internal pure returns(bool){ + return _data.maxExitRequestsLimit != 0; + } + + + /** + * @notice find a minimum of two numbers with a constant gas consumption + * @dev doesn't use branching logic inside + * @param _lhs left hand side value + * @param _rhs right hand side value + */ + function _constGasMin(uint256 _lhs, uint256 _rhs) internal pure returns (uint256 min) { + uint256 lhsIsLess; + assembly { + lhsIsLess := lt(_lhs, _rhs) // lhsIsLess = (_lhs < _rhs) ? 1 : 0 + } + min = (_lhs * lhsIsLess) + (_rhs * (1 - lhsIsLess)); + } +} \ No newline at end of file diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 5804ba32d9..712470b672 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -1,10 +1,12 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; +import { Versioned } from "../utils/Versioned.sol"; +import { ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData } from "../lib/ReportExitLimitUtils.sol"; interface IWithdrawalVault { function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; @@ -12,8 +14,10 @@ interface IWithdrawalVault { function getWithdrawalRequestFee() external view returns (uint256); } -contract ValidatorsExitBus is AccessControlEnumerable { +abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { using UnstructuredStorage for bytes32; + using ReportExitLimitUtilsStorage for bytes32; + using ReportExitLimitUtils for ExitRequestLimitData; /// @dev Errors error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); @@ -22,16 +26,37 @@ contract ValidatorsExitBus is AccessControlEnumerable { error TriggerableWithdrawalRefundFailed(); error ExitHashWasNotSubmitted(); error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + error UnsupportedRequestsDataFormat(uint256 format); + error InvalidRequestsDataLength(); + error InvalidRequestsData(); + error ActorOutOfReportLimit(); + error RequestsAlreadyDelivered(); + error ExitRequestsLimit(); /// @dev Events event MadeRefund( address sender, uint256 refundValue ); + event StoredExitRequestHash( bytes32 exitRequestHash ); + event ValidatorExitRequest( + uint256 indexed stakingModuleId, + uint256 indexed nodeOperatorId, + uint256 indexed validatorIndex, + bytes validatorPubkey, + uint256 timestamp + ); + + event ExitRequestsLimitSet( + uint256 _maxExitRequestsLimit, + uint256 _exitRequestsLimitIncreasePerBlock + ); + + struct DeliveryHistory { /// @dev Key index in exit request array uint256 lastDeliveredKeyIndex; @@ -48,26 +73,158 @@ contract ValidatorsExitBus is AccessControlEnumerable { DeliveryHistory[] deliverHistory; } + struct ExitRequestData { bytes data; uint256 dataFormat; + // TODO: maybe add requestCount for early exit and make it more safe + } + + struct ValidatorExitData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + uint256 validatorIndex; + bytes validatorPubkey; } + bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); + bytes32 public constant DIRECT_EXIT_HASH_ROLE = keccak256("DIRECT_EXIT_HASH_ROLE"); + bytes32 public constant EXIT_REPORT_LIMIT_ROLE = keccak256("EXIT_REPORT_LIMIT_ROLE"); + bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); + /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; uint256 internal constant PUBLIC_KEY_LENGTH = 48; + ILidoLocator internal immutable LOCATOR; + + + /// @notice The list format of the validator exit requests data. Used when all + /// requests fit into a single transaction. + /// + /// Each validator exit request is described by the following 64-byte array: + /// + /// MSB <------------------------------------------------------- LSB + /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | + /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | + /// + /// All requests are tightly packed into a byte array where requests follow + /// one another without any separator or padding, and passed to the `data` + /// field of the report structure. + /// + /// Requests must be sorted in the ascending order by the following compound + /// key: (moduleId, nodeOpId, validatorIndex). + /// + uint256 public constant DATA_FORMAT_LIST = 1; + /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); - bytes32 private constant LOCATOR_CONTRACT_POSITION = keccak256("lido.ValidatorsExitBus.locatorContract"); + constructor(address lidoLocator) { + LOCATOR = ILidoLocator(lidoLocator); + } + + function submitReportHash(bytes32 exitReportHash) external onlyRole(SUBMIT_REPORT_HASH_ROLE) { + uint256 contractVersion = getContractVersion(); + _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0,0)); + } + + function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external{ + bytes calldata data = request.data; + _checkContractVersion(contractVersion); + + RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(data, request.dataFormat))]; + + if (requestStatus.contractVersion == 0) { + revert ExitHashWasNotSubmitted(); + } - function _setLocatorAddress(address addr) internal { - if (addr == address(0)) revert ZeroAddress(); + if (request.dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(request.dataFormat); + } + + if (request.data.length % PACKED_REQUEST_LENGTH != 0) { + revert InvalidRequestsDataLength(); + } - LOCATOR_CONTRACT_POSITION.setStorageAddress(addr); + // TODO: hash requestsCount too + + if (requestStatus.totalItemsCount == type(uint256).max ) { + requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; + } + + uint256 deliveredItemsCount = requestStatus.deliveredItemsCount; + uint256 restToDeliver = requestStatus.totalItemsCount - deliveredItemsCount; + + if (restToDeliver == 0 ) { + revert RequestsAlreadyDelivered(); + } + + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + + uint256 requestsToDeliver; + + // check if limit set + if (exitRequestLimitData.isExitReportLimitSet()) { + uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + if (limit == 0) { + revert ExitRequestsLimit(); + } + + requestsToDeliver = restToDeliver <= limit ? restToDeliver : limit; + + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - requestsToDeliver)); + } else { + // TODO: do we need to store prev exit limit here + requestsToDeliver = restToDeliver; + } + + uint256 offset; + uint256 offsetPastEnd; + + assembly { + offset := add(data.offset, mul(deliveredItemsCount, PACKED_REQUEST_LENGTH)) + offsetPastEnd := add(offset, mul(requestsToDeliver, PACKED_REQUEST_LENGTH)) + } + + bytes calldata pubkey; + + assembly { + pubkey.length := 48 + } + + uint256 timestamp = block.timestamp; + uint256 lastDeliveredKeyIndex = deliveredItemsCount; + + while (offset < offsetPastEnd) { + uint256 dataWithoutPubkey; + assembly { + // 16 most significant bytes are taken by module id, node op id, and val index + dataWithoutPubkey := shr(128, calldataload(offset)) + // the next 48 bytes are taken by the pubkey + pubkey.offset := add(offset, 16) + // totalling to 64 bytes + offset := add(offset, 64) + } + + uint64 valIndex = uint64(dataWithoutPubkey); + uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); + uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + if (moduleId == 0) { + // emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); + revert InvalidRequestsData(); + } + + requestStatus.deliverHistory.push(DeliveryHistory(lastDeliveredKeyIndex, timestamp)); + lastDeliveredKeyIndex = lastDeliveredKeyIndex + 1; + + emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); + } + + requestStatus.deliveredItemsCount = deliveredItemsCount + requestsToDeliver; } /// @notice Triggers exits on the EL via the Withdrawal Vault contract after @@ -75,14 +232,14 @@ contract ValidatorsExitBus is AccessControlEnumerable { // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable { uint256 prevBalance = address(this).balance - msg.value; - RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(request.data, request.dataFormat))]; bytes calldata data = request.data; + RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(data, request.dataFormat))]; if (requestStatus.contractVersion == 0) { revert ExitHashWasNotSubmitted(); } - address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress(); + address locatorAddr = address(LOCATOR); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); @@ -94,6 +251,7 @@ contract ValidatorsExitBus is AccessControlEnumerable { bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); + // TODO: create library for reading DATA for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); @@ -137,28 +295,85 @@ contract ValidatorsExitBus is AccessControlEnumerable { assert(address(this).balance == prevBalance); } + function triggerExitsDirectly(ValidatorExitData calldata validator) external payable onlyRole(DIRECT_EXIT_HASH_ROLE) { + uint256 prevBalance = address(this).balance - msg.value; + address locatorAddr = address(LOCATOR); + address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); + uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); + uint256 timestamp = block.timestamp; + + if (msg.value < withdrawalFee ) { + revert InsufficientPayment(withdrawalFee, 1, msg.value); + } + + //TODO: check limit + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + + // check if limit set + if (exitRequestLimitData.isExitReportLimitSet()) { + uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + if (limit == 0) { + revert ExitRequestsLimit(); + } + + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - 1)); + } + + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee}(validator.validatorPubkey); + + emit ValidatorExitRequest(validator.stakingModuleId, validator.nodeOperatorId, validator.validatorIndex, validator.validatorPubkey, timestamp); + + uint256 refund = msg.value - withdrawalFee; + + if (refund > 0) { + (bool success, ) = msg.sender.call{value: refund}(""); + + if (!success) { + revert TriggerableWithdrawalRefundFailed(); + } + + emit MadeRefund(msg.sender, refund); + } + + assert(address(this).balance == prevBalance); + } + + function setExitReportLimit(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitReportLimit(_maxExitRequestsLimit, _exitRequestsLimitIncreasePerBlock) + ); + + emit ExitRequestsLimitSet(_maxExitRequestsLimit, _exitRequestsLimitIncreasePerBlock); + } + + function getDeliveryHistory(bytes32 exitReportHash) external view returns (DeliveryHistory[] memory) { + mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); + RequestStatus storage request = hashes[exitReportHash]; + + return request.deliverHistory; + } + function _storeExitRequestHash( bytes32 exitRequestHash, uint256 totalItemsCount, uint256 deliveredItemsCount, uint256 contractVersion, - uint256 lastDeliveredKeyIndex + DeliveryHistory memory history ) internal { - if (deliveredItemsCount == 0) { - return; - } - mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); - RequestStatus storage request = hashes[exitRequestHash]; + if (request.contractVersion != 0) { + return; + } + request.totalItemsCount = totalItemsCount; request.deliveredItemsCount = deliveredItemsCount; request.contractVersion = contractVersion; - request.deliverHistory.push(DeliveryHistory({ - timestamp: block.timestamp, - lastDeliveredKeyIndex: lastDeliveredKeyIndex - })); + if (history.timestamp != 0) { + request.deliverHistory.push(history); + } + emit StoredExitRequestHash(exitRequestHash); } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index d1a2f07951..ca52125adf 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -11,6 +11,7 @@ import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { BaseOracle } from "./BaseOracle.sol"; import { ValidatorsExitBus } from "./ValidatorsExitBus.sol"; +import { ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData } from "../lib/ReportExitLimitUtils.sol"; interface IOracleReportSanityChecker { @@ -21,12 +22,12 @@ interface IOracleReportSanityChecker { contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus { using UnstructuredStorage for bytes32; using SafeCast for uint256; + using ReportExitLimitUtilsStorage for bytes32; + using ReportExitLimitUtils for ExitRequestLimitData; + error AdminCannotBeZero(); error SenderNotAllowed(); - error UnsupportedRequestsDataFormat(uint256 format); - error InvalidRequestsData(); - error InvalidRequestsDataLength(); error UnexpectedRequestsDataLength(); error InvalidRequestsDataSortOrder(); error ArgumentOutOfBounds(); @@ -37,14 +38,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus uint256 requestedValidatorIndex ); - event ValidatorExitRequest( - uint256 indexed stakingModuleId, - uint256 indexed nodeOperatorId, - uint256 indexed validatorIndex, - bytes validatorPubkey, - uint256 timestamp - ); - event WarnDataIncompleteProcessing( uint256 indexed refSlot, uint256 requestsProcessed, @@ -89,7 +82,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus bytes32 internal constant DATA_PROCESSING_STATE_POSITION = keccak256("lido.ValidatorsExitBusOracle.dataProcessingState"); - ILidoLocator internal immutable LOCATOR; + // ILidoLocator internal immutable LOCATOR; /// /// Initialization & admin functions @@ -97,8 +90,8 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus constructor(uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator) BaseOracle(secondsPerSlot, genesisTime) + ValidatorsExitBus(lidoLocator) { - LOCATOR = ILidoLocator(lidoLocator); } function initialize( @@ -116,7 +109,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus function finalizeUpgrade_v2() external { _updateContractVersion(2); - _setLocatorAddress(address(LOCATOR)); + // TODO: after deleted last exited keys clean here slots } /// @notice Resume accepting validator exit requests @@ -184,24 +177,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus bytes data; } - /// @notice The list format of the validator exit requests data. Used when all - /// requests fit into a single transaction. - /// - /// Each validator exit request is described by the following 64-byte array: - /// - /// MSB <------------------------------------------------------- LSB - /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | - /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | - /// - /// All requests are tightly packed into a byte array where requests follow - /// one another without any separator or padding, and passed to the `data` - /// field of the report structure. - /// - /// Requests must be sorted in the ascending order by the following compound - /// key: (moduleId, nodeOpId, validatorIndex). - /// - uint256 public constant DATA_FORMAT_LIST = 1; - /// @notice Submits report data for processing. /// /// @param data The data. See the `ReportData` structure's docs for details. @@ -346,10 +321,21 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus revert InvalidRequestsDataLength(); } - // TODO: next iteration will check ref slot deliveredReportAmount IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) .checkExitBusOracleReport(data.requestsCount); + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + + if (exitRequestLimitData.isExitReportLimitSet()) { + uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + + if (limit < data.requestsCount) { + revert ExitRequestsLimit(); + } + + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - data.requestsCount)); + } + if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); } @@ -454,7 +440,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus if (requestsCount == 0) { return; } - _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, requestsCount - 1); + _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(block.timestamp, requestsCount - 1)); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts new file mode 100644 index 0000000000..edc64aec2b --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -0,0 +1,326 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; + +import { de0x, numberToHex } from "lib"; + +import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; + +describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + let withdrawalVault: WithdrawalVault__MockForVebo; + + let oracleVersion: bigint; + let exitRequests: ExitRequest[]; + let exitRequestHash: string; + let exitRequest: ExitRequestData; + let authorizedEntity: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + const LAST_PROCESSING_REF_SLOT = 1; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ExitRequestData { + dataFormat: number; + data: string; + } + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + withdrawalVault = deployed.withdrawalVault; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + withdrawalVault, + resumeAfterDeploy: true, + lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, + }); + + oracleVersion = await oracle.getContractVersion(); + }; + + before(async () => { + [admin, authorizedEntity, stranger] = await ethers.getSigners(); + + await deploy(); + }); + + it("Initially, report was not submitted", async () => { + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + ]; + + exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + await expect(oracle.emitExitEvents(exitRequest, 2)) + .to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted") + .withArgs(); + }); + + it("Wrong contract version", async () => { + await expect(oracle.emitExitEvents(exitRequest, 1)) + .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") + .withArgs(2, 1); + }); + + it("Should revert without SUBMIT_REPORT_HASH_ROLE role", async () => { + const request = [exitRequest.data, exitRequest.dataFormat]; + const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["(bytes, uint256)"], [request])); + + await expect(oracle.connect(stranger).submitReportHash(hash)).to.be.revertedWithOZAccessControlError( + await stranger.getAddress(), + await oracle.SUBMIT_REPORT_HASH_ROLE(), + ); + }); + + it("Should store exit hash for authorized entity", async () => { + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + + await oracle.grantRole(role, authorizedEntity); + + exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), + ); + + const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + + await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(exitRequestHash); + }); + + it("Emit ValidatorExit event", async () => { + const emitTx = await oracle.emitExitEvents(exitRequest, 2); + const block = await emitTx.getBlock(); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[0].moduleId, + exitRequests[0].nodeOpId, + exitRequests[0].valIndex, + exitRequests[0].valPubkey, + block?.timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[1].moduleId, + exitRequests[1].nodeOpId, + exitRequests[1].valIndex, + exitRequests[1].valPubkey, + block?.timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[2].moduleId, + exitRequests[2].nodeOpId, + exitRequests[2].valIndex, + exitRequests[2].valPubkey, + block?.timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[3].moduleId, + exitRequests[3].nodeOpId, + exitRequests[3].valIndex, + exitRequests[3].valPubkey, + block?.timestamp, + ); + }); + + it("Should revert if wrong DATA_FORMAT", async () => { + const request = [exitRequest.data, 2]; + const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], request)); + const submitTx = await oracle.connect(authorizedEntity).submitReportHash(hash); + await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(hash); + exitRequest = { + dataFormat: 2, + data: encodeExitRequestsDataList(exitRequests), + }; + await expect(oracle.emitExitEvents(exitRequest, 2)) + .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") + .withArgs(2); + }); + + it("Should deliver part of request if limit is smaller than number of requests", async () => { + const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(role, authorizedEntity); + const exitLimitTx = await oracle.connect(authorizedEntity).setExitReportLimit(2, 1); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1); + + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), + ); + + const history0 = await oracle.getDeliveryHistory(exitRequestHash); + expect(history0.length).to.eq(0); + + const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + await expect(submitTx).to.emit(oracle, "StoredExitRequestHash"); + + const emitTx = await oracle.emitExitEvents(exitRequest, 2); + + const receipt = await emitTx.wait(); + expect(receipt?.logs.length).to.eq(2); + + const block = await emitTx.getBlock(); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[0].moduleId, + exitRequests[0].nodeOpId, + exitRequests[0].valIndex, + exitRequests[0].valPubkey, + block?.timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[1].moduleId, + exitRequests[1].nodeOpId, + exitRequests[1].valIndex, + exitRequests[1].valPubkey, + block?.timestamp, + ); + + const history1 = await oracle.getDeliveryHistory(exitRequestHash); + expect(history1.length).to.eq(2); + expect(history1[0].lastDeliveredKeyIndex).to.eq(0); + expect(history1[1].lastDeliveredKeyIndex).to.eq(1); + + const emitTx2 = await oracle.emitExitEvents(exitRequest, 2); + + const receipt2 = await emitTx2.wait(); + expect(receipt2?.logs.length).to.eq(1); + + const block2 = await emitTx2.getBlock(); + + await expect(emitTx2) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[2].moduleId, + exitRequests[2].nodeOpId, + exitRequests[2].valIndex, + exitRequests[2].valPubkey, + block2?.timestamp, + ); + + const history2 = await oracle.getDeliveryHistory(exitRequestHash); + expect(history2.length).to.eq(3); + expect(history2[0].lastDeliveredKeyIndex).to.eq(0); + expect(history2[1].lastDeliveredKeyIndex).to.eq(1); + expect(history2[2].lastDeliveredKeyIndex).to.eq(2); + + const emitTx3 = await oracle.emitExitEvents(exitRequest, 2); + + const receipt3 = await emitTx2.wait(); + expect(receipt3?.logs.length).to.eq(1); + + const block3 = await emitTx3.getBlock(); + + await expect(emitTx3) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[3].moduleId, + exitRequests[3].nodeOpId, + exitRequests[3].valIndex, + exitRequests[3].valPubkey, + block3?.timestamp, + ); + + const history3 = await oracle.getDeliveryHistory(exitRequestHash); + expect(history3.length).to.eq(4); + expect(history3[0].lastDeliveredKeyIndex).to.eq(0); + expect(history3[1].lastDeliveredKeyIndex).to.eq(1); + expect(history3[2].lastDeliveredKeyIndex).to.eq(2); + expect(history3[3].lastDeliveredKeyIndex).to.eq(3); + + const emitTx4 = await oracle.emitExitEvents(exitRequest, 2); + + const receipt4 = await emitTx2.wait(); + expect(receipt4?.logs.length).to.eq(1); + + const block4 = await emitTx4.getBlock(); + + await expect(emitTx4) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[4].moduleId, + exitRequests[4].nodeOpId, + exitRequests[4].valIndex, + exitRequests[4].valPubkey, + block4?.timestamp, + ); + + const history4 = await oracle.getDeliveryHistory(exitRequestHash); + expect(history4.length).to.eq(5); + expect(history4[0].lastDeliveredKeyIndex).to.eq(0); + expect(history4[1].lastDeliveredKeyIndex).to.eq(1); + expect(history4[2].lastDeliveredKeyIndex).to.eq(2); + expect(history4[3].lastDeliveredKeyIndex).to.eq(3); + expect(history4[4].lastDeliveredKeyIndex).to.eq(4); + + await expect(oracle.emitExitEvents(exitRequest, 2)).to.be.revertedWithCustomError( + oracle, + "RequestsAlreadyDelivered", + ); + }); +}); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 7e51d5d920..122c47b43f 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -241,13 +241,13 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { expect(procState.requestsSubmitted).to.equal(exitRequests.length); }); - it("last requested validator indices are updated", async () => { - const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n]); - const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n]); + // it("last requested validator indices are updated", async () => { + // const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n]); + // const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n]); - expect([...indices1]).to.have.ordered.members([2n, -1n, -1n]); - expect([...indices2]).to.have.ordered.members([1n, -1n, -1n]); - }); + // expect([...indices1]).to.have.ordered.members([2n, -1n, -1n]); + // expect([...indices2]).to.have.ordered.members([1n, -1n, -1n]); + // }); it("no data can be submitted for the same reference slot again", async () => { await expect(oracle.connect(member2).submitReportData(reportFields, oracleVersion)).to.be.revertedWithCustomError( diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 5051af732e..8faa49bf3e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -704,4 +704,69 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { ]); }); }); + + context("VEB limits", () => { + let originalState: string; + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + after(async () => await Snapshot.restore(originalState)); + + let report: ReportFields; + let hash: string; + + it("Set exit limit", async () => { + const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(role, admin); + const exitLimitTx = await oracle.connect(admin).setExitReportLimit(2, 1); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1); + }); + + it("emits ValidatorExitRequest events", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, + ]; + const { reportData } = await prepareReportAndSubmitHash(requests); + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "ExitRequestsLimit", + ); + + const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); + const block = await tx.getBlock(); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + requests[0].moduleId, + requests[0].nodeOpId, + requests[0].valIndex, + requests[0].valPubkey, + block?.timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + requests[1].moduleId, + requests[1].nodeOpId, + requests[1].valIndex, + requests[1].valPubkey, + block?.timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + requests[2].moduleId, + requests[2].nodeOpId, + requests[2].valIndex, + requests[2].valPubkey, + block?.timestamp, + ); + }); + }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index 39b8e95b0d..cd6218cde0 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -210,13 +210,13 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect(procState.requestsSubmitted).to.equal(exitRequests.length); }); - it("last requested validator indices are updated", async () => { - const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n, 3n]); - const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n, 3n]); + // it("last requested validator indices are updated", async () => { + // const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n, 3n]); + // const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n, 3n]); - expect([...indices1]).to.have.ordered.members([2n, -1n, -1n, -1n]); - expect([...indices2]).to.have.ordered.members([3n, -1n, -1n, -1n]); - }); + // expect([...indices1]).to.have.ordered.members([2n, -1n, -1n, -1n]); + // expect([...indices2]).to.have.ordered.members([3n, -1n, -1n, -1n]); + // }); it("someone submitted exit report data and triggered exit", async () => { const tx = await oracle.triggerExits( diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts new file mode 100644 index 0000000000..c0f255f488 --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -0,0 +1,137 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; + +import { de0x, numberToHex } from "lib"; + +import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; + +describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { + let consensus: HashConsensus__Harness; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + let withdrawalVault: WithdrawalVault__MockForVebo; + + let oracleVersion: bigint; + let exitRequests: ExitRequest[]; + let exitRequestHash: string; + let exitRequest: ExitRequestData; + let authorizedEntity: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + let validatorExitData: ValidatorExitData; + + const LAST_PROCESSING_REF_SLOT = 1; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + interface ExitRequestData { + dataFormat: number; + data: string; + } + + interface ValidatorExitData { + stakingModuleId: number; + nodeOperatorId: number; + validatorIndex: number; + validatorPubkey: string; + } + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + withdrawalVault = deployed.withdrawalVault; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + withdrawalVault, + resumeAfterDeploy: true, + lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, + }); + + oracleVersion = await oracle.getContractVersion(); + }; + + before(async () => { + [admin, authorizedEntity, stranger] = await ethers.getSigners(); + + await deploy(); + }); + + it("Should revert without DIRECT_EXIT_HASH_ROLE role", async () => { + validatorExitData = { + stakingModuleId: 1, + nodeOperatorId: 0, + validatorIndex: 0, + validatorPubkey: PUBKEYS[0], + }; + + await expect( + oracle.connect(stranger).triggerExitsDirectly(validatorExitData, { + value: 2, + }), + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_HASH_ROLE()); + }); + + it("Not enough fee", async () => { + const role = await oracle.DIRECT_EXIT_HASH_ROLE(); + + await oracle.grantRole(role, authorizedEntity); + + await expect( + oracle.connect(authorizedEntity).triggerExitsDirectly(validatorExitData, { + value: 0, + }), + ) + .to.be.revertedWithCustomError(oracle, "InsufficientPayment") + .withArgs(1, 1, 0); + }); + + it("Emit ValidatorExit event and should trigger withdrawals", async () => { + const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(validatorExitData, { + value: 2, + }); + const block = await tx.getBlock(); + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(PUBKEYS[0]); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validatorExitData.stakingModuleId, + validatorExitData.nodeOperatorId, + validatorExitData.validatorIndex, + validatorExitData.validatorPubkey, + block?.timestamp, + ); + }); +}); From f5b6acc8253f80915148935ea8d90e59df074570 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 4 Apr 2025 14:55:23 +0400 Subject: [PATCH 057/405] fix: remove last exited validators code --- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 6 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 - .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 87 --------- ...r-exit-bus-oracle.submitReportData.test.ts | 181 ++++++------------ 4 files changed, 62 insertions(+), 214 deletions(-) diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index 2b2e1793e1..2b9765f9e8 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -12,7 +12,7 @@ import { UnstructuredStorage } from "./UnstructuredStorage.sol"; // struct ExitRequestLimitData { - uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests + uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests uint96 prevExitRequestsLimit; // limit value (<= `maxExitRequestLimit`) obtained on the previous exit request uint32 maxExitRequestsLimitGrowthBlocks; // limit regeneration speed expressed in blocks uint96 maxExitRequestsLimit; // maximum limit value @@ -52,6 +52,8 @@ library ReportExitLimitUtilsStorage { library ReportExitLimitUtils { + error Debug(uint256 limit, uint256 block, uint256 prev); + /** * @notice Calculate exit requests limit * @dev using `_constGasMin` to make gas consumption independent of the current block number @@ -69,6 +71,8 @@ library ReportExitLimitUtils { projectedLimit, _data.maxExitRequestsLimit ); + + } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 712470b672..f9fbdee514 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -306,10 +306,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { revert InsufficientPayment(withdrawalFee, 1, msg.value); } - //TODO: check limit ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - // check if limit set if (exitRequestLimitData.isExitReportLimitSet()) { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (limit == 0) { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index ca52125adf..c99e599c2b 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -13,7 +13,6 @@ import { BaseOracle } from "./BaseOracle.sol"; import { ValidatorsExitBus } from "./ValidatorsExitBus.sol"; import { ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData } from "../lib/ReportExitLimitUtils.sol"; - interface IOracleReportSanityChecker { function checkExitBusOracleReport(uint256 _exitRequestsCount) external view; } @@ -31,12 +30,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus error UnexpectedRequestsDataLength(); error InvalidRequestsDataSortOrder(); error ArgumentOutOfBounds(); - error NodeOpValidatorIndexMustIncrease( - uint256 moduleId, - uint256 nodeOpId, - uint256 prevRequestedValidatorIndex, - uint256 requestedValidatorIndex - ); event WarnDataIncompleteProcessing( uint256 indexed refSlot, @@ -44,10 +37,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus uint256 requestsCount ); - event StoreOracleExitRequestHashStart( - bytes32 exitRequestHash - ); - struct DataProcessingState { uint64 refSlot; uint64 requestsCount; @@ -55,11 +44,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus uint16 dataFormat; } - struct RequestedValidator { - bool requested; - uint64 index; - } - /// @notice An ACL role granting the permission to submit the data for a committee report. bytes32 public constant SUBMIT_DATA_ROLE = keccak256("SUBMIT_DATA_ROLE"); @@ -82,8 +66,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus bytes32 internal constant DATA_PROCESSING_STATE_POSITION = keccak256("lido.ValidatorsExitBusOracle.dataProcessingState"); - // ILidoLocator internal immutable LOCATOR; - /// /// Initialization & admin functions /// @@ -214,31 +196,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus return TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256(); } - /// @notice Returns the latest validator indices that were requested to exit for the given - /// `nodeOpIds` in the given `moduleId`. For node operators that were never requested to exit - /// any validator, index is set to -1. - /// - /// @param moduleId ID of the staking module. - /// @param nodeOpIds IDs of the staking module's node operators. - /// - function getLastRequestedValidatorIndices(uint256 moduleId, uint256[] calldata nodeOpIds) - external view returns (int256[] memory) - { - if (moduleId > type(uint24).max) revert ArgumentOutOfBounds(); - - int256[] memory indices = new int256[](nodeOpIds.length); - - for (uint256 i = 0; i < nodeOpIds.length; ++i) { - uint256 nodeOpId = nodeOpIds[i]; - if (nodeOpId > type(uint40).max) revert ArgumentOutOfBounds(); - uint256 nodeOpKey = _computeNodeOpKey(moduleId, nodeOpId); - RequestedValidator memory validator = _storageLastRequestedValidatorIndices()[nodeOpKey]; - indices[i] = validator.requested ? int256(uint256(validator.index)) : -1; - } - - return indices; - } - struct ProcessingState { /// @notice Reference slot for the current reporting frame. uint256 currentFrameRefSlot; @@ -366,9 +323,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus offsetPastEnd := add(offset, data.length) } - uint256 lastDataWithoutPubkey = 0; - uint256 lastNodeOpKey = 0; - RequestedValidator memory lastRequestedVal; bytes calldata pubkey; assembly { @@ -390,11 +344,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus // dataWithoutPubkey // MSB <---------------------------------------------------------------------- LSB // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - // - if (dataWithoutPubkey <= lastDataWithoutPubkey) { - revert InvalidRequestsDataSortOrder(); - } - uint64 valIndex = uint64(dataWithoutPubkey); uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); @@ -403,37 +352,8 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus revert InvalidRequestsData(); } - uint256 nodeOpKey = _computeNodeOpKey(moduleId, nodeOpId); - if (nodeOpKey != lastNodeOpKey) { - if (lastNodeOpKey != 0) { - _storageLastRequestedValidatorIndices()[lastNodeOpKey] = lastRequestedVal; - } - lastRequestedVal = _storageLastRequestedValidatorIndices()[nodeOpKey]; - lastNodeOpKey = nodeOpKey; - } - - if (lastRequestedVal.requested && valIndex <= lastRequestedVal.index) { - revert NodeOpValidatorIndexMustIncrease( - moduleId, - nodeOpId, - lastRequestedVal.index, - valIndex - ); - } - - lastRequestedVal = RequestedValidator(true, valIndex); - lastDataWithoutPubkey = dataWithoutPubkey; - emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); } - - if (lastNodeOpKey != 0) { - _storageLastRequestedValidatorIndices()[lastNodeOpKey] = lastRequestedVal; - } - } - - function _computeNodeOpKey(uint256 moduleId, uint256 nodeOpId) internal pure returns (uint256) { - return (moduleId << 40) | nodeOpId; } function _storeOracleExitRequestHash(bytes32 exitRequestHash, uint256 requestsCount, uint256 contractVersion) internal { @@ -447,13 +367,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus /// Storage helpers /// - function _storageLastRequestedValidatorIndices() internal pure returns ( - mapping(uint256 => RequestedValidator) storage r - ) { - bytes32 position = LAST_REQUESTED_VALIDATOR_INDICES_POSITION; - assembly { r.slot := position } - } - struct StorageDataProcessingState { DataProcessingState value; } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 8faa49bf3e..aad853c120 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -38,6 +38,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { let member2: HardhatEthersSigner; let member3: HardhatEthersSigner; let stranger: HardhatEthersSigner; + let authorizedEntity: HardhatEthersSigner; const LAST_PROCESSING_REF_SLOT = 1; @@ -102,9 +103,9 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { return { reportData, reportHash }; }; - async function getLastRequestedValidatorIndex(moduleId: number, nodeOpId: number) { - return (await oracle.getLastRequestedValidatorIndices(moduleId, [nodeOpId]))[0]; - } + // async function getLastRequestedValidatorIndex(moduleId: number, nodeOpId: number) { + // return (await oracle.getLastRequestedValidatorIndices(moduleId, [nodeOpId]))[0]; + // } const deploy = async () => { const deployed = await deployVEBO(admin.address); @@ -130,7 +131,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }; before(async () => { - [admin, member1, member2, member3, stranger] = await ethers.getSigners(); + [admin, member1, member2, member3, stranger, authorizedEntity] = await ethers.getSigners(); await deploy(); }); @@ -394,106 +395,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); }); - context(`requires validator indices for the same node operator to increase`, () => { - let originalState: string; - - before(async () => { - originalState = await Snapshot.take(); - await consensus.advanceTimeToNextFrameStart(); - }); - - after(async () => await Snapshot.restore(originalState)); - - it(`requesting NO 5-3 to exit validator 0`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, - ]); - await oracle.connect(member1).submitReportData(reportData, oracleVersion); - expect(await getLastRequestedValidatorIndex(5, 3)).to.equal(0); - }); - - it(`cannot request NO 5-3 to exit validator 0 again`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, - ]); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") - .withArgs(5, 3, 0, 0); - }); - - it(`requesting NO 5-3 to exit validator 1`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[1] }, - ]); - await oracle.connect(member1).submitReportData(reportData, oracleVersion, { from: member1 }); - expect(await getLastRequestedValidatorIndex(5, 3)).to.equal(1); - }); - - it(`cannot request NO 5-3 to exit validator 1 again`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[1] }, - ]); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") - .withArgs(5, 3, 1, 1); - }); - - it(`cannot request NO 5-3 to exit validator 0 again`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, - ]); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") - .withArgs(5, 3, 1, 0); - }); - - it(`cannot request NO 5-3 to exit validator 1 again (multiple requests)`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[0] }, - { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[0] }, - ]); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") - .withArgs(5, 3, 1, 1); - }); - - it(`cannot request NO 5-3 to exit validator 1 again (multiple requests, case 2)`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[2] }, - { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[3] }, - { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[4] }, - ]); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "NodeOpValidatorIndexMustIncrease") - .withArgs(5, 3, 1, 1); - }); - - it(`cannot request NO 5-3 to exit validator 2 two times per request`, async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[2] }, - { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, - ]); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( - oracle, - "InvalidRequestsDataSortOrder", - ); - }); - }); - context(`only consensus member or SUBMIT_DATA_ROLE can submit report on unpaused contract`, () => { let originalState: string; @@ -713,32 +614,40 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); after(async () => await Snapshot.restore(originalState)); - let report: ReportFields; - let hash: string; - it("Set exit limit", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, admin); - const exitLimitTx = await oracle.connect(admin).setExitReportLimit(2, 1); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1); + const exitLimitTx = await oracle.connect(admin).setExitReportLimit(4, 1); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1); }); - it("emits ValidatorExitRequest events", async () => { + it("deliver report by actor different from oracle", async () => { const requests = [ { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 2, valIndex: 3, valPubkey: PUBKEYS[3] }, ]; const { reportData } = await prepareReportAndSubmitHash(requests); - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( - oracle, - "ExitRequestsLimit", + const exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [reportData.data, reportData.dataFormat]), ); - const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); - const block = await tx.getBlock(); + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + await oracle.grantRole(role, authorizedEntity); - await expect(tx) + const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(exitRequestHash); + + const exitRequest = { + dataFormat: reportData.dataFormat, + data: reportData.data, + }; + + const emitTx = await oracle.emitExitEvents(exitRequest, 2); + const block = await emitTx.getBlock(); + + await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs( requests[0].moduleId, @@ -748,7 +657,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { block?.timestamp, ); - await expect(tx) + await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs( requests[1].moduleId, @@ -757,16 +666,40 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { requests[1].valPubkey, block?.timestamp, ); + }); + + it("emits ValidatorExitRequest events", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 3, valIndex: 4, valPubkey: PUBKEYS[4] }, + ]; + const { reportData } = await prepareReportAndSubmitHash(requests); + + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( + oracle, + "ExitRequestsLimit", + ); + + const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); + const timestamp = await consensus.getTime(); await expect(tx) .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - requests[2].moduleId, - requests[2].nodeOpId, - requests[2].valIndex, - requests[2].valPubkey, - block?.timestamp, - ); + .withArgs(requests[0].moduleId, requests[0].nodeOpId, requests[0].valIndex, requests[0].valPubkey, timestamp); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[1].moduleId, requests[1].nodeOpId, requests[1].valIndex, requests[1].valPubkey, timestamp); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[2].moduleId, requests[2].nodeOpId, requests[2].valIndex, requests[2].valPubkey, timestamp); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[3].moduleId, requests[3].nodeOpId, requests[3].valIndex, requests[3].valPubkey, timestamp); }); }); }); From a8107075536c2db232a63d125444ffba3bb6c3b7 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 8 Apr 2025 13:55:50 +0200 Subject: [PATCH 058/405] feat: validator exit verifier prototype --- contracts/0.8.25/ValidatorExitVerifier.sol | 386 ++++++++++++++ .../0.8.25/interfaces/IStakingRouter.sol | 14 + .../interfaces/IValidatorsExitBusOracle.sol | 23 + .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 66 +++ lib/eip4788-beaconRoots.ts | 23 + lib/index.ts | 1 + test/0.8.25/contracts/StakingRouter_Mock.sol | 24 + .../ValidatorsExitBusOracle_Mock.sol | 114 ++++ test/0.8.25/validatorExitVerifier.test.ts | 491 ++++++++++++++++++ test/0.8.25/validatorExitVerifierHelpers.ts | 86 +++ test/0.8.25/validatorState.ts | 142 +++++ .../contracts/ValidatorsExitBus__Harness.sol | 16 + .../oracle/validator-exit-bus.helpers.test.ts | 166 ++++++ 13 files changed, 1552 insertions(+) create mode 100644 contracts/0.8.25/ValidatorExitVerifier.sol create mode 100644 contracts/0.8.25/interfaces/IStakingRouter.sol create mode 100644 contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol create mode 100644 lib/eip4788-beaconRoots.ts create mode 100644 test/0.8.25/contracts/StakingRouter_Mock.sol create mode 100644 test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol create mode 100644 test/0.8.25/validatorExitVerifier.test.ts create mode 100644 test/0.8.25/validatorExitVerifierHelpers.ts create mode 100644 test/0.8.25/validatorState.ts create mode 100644 test/0.8.9/oracle/validator-exit-bus.helpers.test.ts diff --git a/contracts/0.8.25/ValidatorExitVerifier.sol b/contracts/0.8.25/ValidatorExitVerifier.sol new file mode 100644 index 0000000000..1a7c952292 --- /dev/null +++ b/contracts/0.8.25/ValidatorExitVerifier.sol @@ -0,0 +1,386 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +import {BeaconBlockHeader, Validator} from "./lib/BeaconTypes.sol"; +import {GIndex} from "./lib/GIndex.sol"; +import {SSZ} from "./lib/SSZ.sol"; +import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; +import {IValidatorsExitBusOracle, DeliveryHistory} from "./interfaces/IValidatorsExitBusOracle.sol"; +import {IStakingRouter} from "./interfaces/IStakingRouter.sol"; + +struct ExitRequestData { + bytes data; + uint256 dataFormat; +} + +struct ValidatorWitness { + // The index of an exit request in the VEBO exit requests data + uint32 exitRequestIndex; + // -------------------- Validator details ------------------- + bytes32 withdrawalCredentials; + uint64 effectiveBalance; + bool slashed; + uint64 activationEligibilityEpoch; + uint64 activationEpoch; + uint64 withdrawableEpoch; + // ------------------------ Proof --------------------------- + bytes32[] validatorProof; +} + +struct ProvableBeaconBlockHeader { + BeaconBlockHeader header; // Header of the block which root is known at 'rootsTimestamp'. + uint64 rootsTimestamp; // Timestamp passed to EIP-4788 block roots contract to retrieve the known block root. +} + +// A witness for a block header which root is accessible via `historical_summaries` field. +struct HistoricalHeaderWitness { + BeaconBlockHeader header; + GIndex rootGIndex; // The generalized index of the old block root in the historical_summaries. + bytes32[] proof; // The Merkle proof for the old block header against the state's historical_summaries root. +} + +/** + * @title ValidatorExitVerifier + * @notice Verifies validator proofs to ensure they are unexited after an exit request. + * Allows permissionless report the status of validators which are assumed to have exited but have not. + * @dev Uses EIP-4788 to confirm the correctness of a given beacon block root. + */ +contract ValidatorExitVerifier { + using SSZ for Validator; + using SSZ for BeaconBlockHeader; + + struct ExitRequestsDeliveryHistory { + uint256 totalItemsCount; + uint256 deliveredItemsCount; + DeliveryHistory[] deliveryHistory; + } + + /// @notice EIP-4788 contract address that provides a mapping of timestamp -> known beacon block root. + address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + uint64 constant FAR_FUTURE_EPOCH = type(uint64).max; + + uint64 public immutable GENESIS_TIME; + uint32 public immutable SLOTS_PER_EPOCH; + uint32 public immutable SECONDS_PER_SLOT; + uint32 public immutable SHARD_COMMITTEE_PERIOD_IN_SECONDS; + + /** + * @notice The GIndex pointing to BeaconState.validators[0] for the "previous" fork. + * @dev Used to derive the correct GIndex when verifying proofs for a block prior to pivot. + */ + GIndex public immutable GI_FIRST_VALIDATOR_PREV; + + /** + * @notice The GIndex pointing to BeaconState.validators[0] for the "current" fork. + * @dev Used to derive the correct GIndex when verifying proofs for a block after the pivot slot. + */ + GIndex public immutable GI_FIRST_VALIDATOR_CURR; + + /** + * @notice The GIndex pointing to BeaconState.historical_summaries for the "previous" fork. + * @dev Used when verifying old blocks (i.e., blocks with slot < PIVOT_SLOT). + */ + GIndex public immutable GI_HISTORICAL_SUMMARIES_PREV; + + /** + * @notice The GIndex pointing to BeaconState.historical_summaries for the "current" fork. + * @dev Used when verifying old blocks (i.e., blocks with slot >= PIVOT_SLOT). + */ + GIndex public immutable GI_HISTORICAL_SUMMARIES_CURR; + + /// @notice The first slot this verifier will accept proofs for. + uint64 public immutable FIRST_SUPPORTED_SLOT; + + /// @notice The first slot of the currently-compatible fork. + uint64 public immutable PIVOT_SLOT; + + ILidoLocator public immutable LOCATOR; + + error RootNotFound(); + error InvalidGIndex(); + error InvalidBlockHeader(); + error UnsupportedSlot(uint64 slot); + error InvalidPivotSlot(); + error ZeroLidoLocatorAddress(); + error ExitRequestNotEligibleOnProvableBeaconBlock( + uint64 provableBeaconBlockTimestamp, + uint64 eligibleExitRequestTimestamp + ); + error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); + error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + + /** + * @dev The previous and current forks can be essentially the same. + * @param lidoLocator The address of the LidoLocator contract. + * @param gIFirstValidatorPrev GIndex pointing to validators[0] on the previous fork. + * @param gIFirstValidatorCurr GIndex pointing to validators[0] on the current fork. + * @param gIHistoricalSummariesPrev GIndex pointing to the historical_summaries on the previous fork. + * @param gIHistoricalSummariesCurr GIndex pointing to the historical_summaries on the current fork. + * @param firstSupportedSlot The earliest slot number that proofs can be submitted for verification. + * @param pivotSlot The pivot slot number used to differentiate "previous" vs "current" fork indexing. + * @param slotsPerEpoch Number of slots per epoch in Ethereum consensus. + * @param secondsPerSlot Duration of a single slot, in seconds, in Ethereum consensus. + * @param genesisTime Genesis timestamp of the Ethereum Beacon chain. + * @param shardCommitteePeriodInSeconds The length of the shard committee period, in seconds. + */ + constructor( + address lidoLocator, + GIndex gIFirstValidatorPrev, + GIndex gIFirstValidatorCurr, + GIndex gIHistoricalSummariesPrev, + GIndex gIHistoricalSummariesCurr, + uint64 firstSupportedSlot, + uint64 pivotSlot, + uint32 slotsPerEpoch, + uint32 secondsPerSlot, + uint64 genesisTime, + uint32 shardCommitteePeriodInSeconds + ) { + if (lidoLocator == address(0)) revert ZeroLidoLocatorAddress(); + if (firstSupportedSlot > pivotSlot) revert InvalidPivotSlot(); + + LOCATOR = ILidoLocator(lidoLocator); + + GI_FIRST_VALIDATOR_PREV = gIFirstValidatorPrev; + GI_FIRST_VALIDATOR_CURR = gIFirstValidatorCurr; + + GI_HISTORICAL_SUMMARIES_PREV = gIHistoricalSummariesPrev; + GI_HISTORICAL_SUMMARIES_CURR = gIHistoricalSummariesCurr; + + FIRST_SUPPORTED_SLOT = firstSupportedSlot; + PIVOT_SLOT = pivotSlot; + SLOTS_PER_EPOCH = slotsPerEpoch; + SECONDS_PER_SLOT = secondsPerSlot; + GENESIS_TIME = genesisTime; + SHARD_COMMITTEE_PERIOD_IN_SECONDS = shardCommitteePeriodInSeconds; + } + + // ------------------------- External Functions ------------------------- + + /** + * @notice Verifies that provided validators are still active (not exited) at the given beacon block. + * If they are unexpectedly still active, it reports them back to the Staking Router. + * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. + * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. + * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited. + */ + function verifyActiveValidatorsAfterExitRequest( + ProvableBeaconBlockHeader calldata beaconBlock, + ValidatorWitness[] calldata validatorWitnesses, + ExitRequestData calldata exitRequests + ) external { + _verifyBeaconBlockRoot(beaconBlock); + + IValidatorsExitBusOracle vebo = IValidatorsExitBusOracle(LOCATOR.validatorsExitBusOracle()); + IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); + + ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + + for (uint256 i = 0; i < validatorWitnesses.length; i++) { + (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = vebo.unpackExitRequest( + exitRequests.data, + exitRequests.dataFormat, + validatorWitnesses[i].exitRequestIndex + ); + + uint64 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( + _getExitRequestTimestamp(requestsDeliveryHistory, validatorWitnesses[i].exitRequestIndex), + beaconBlock.header.slot, + validatorWitnesses[i].activationEpoch + ); + + _verifyValidatorIsNotExited(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); + + stakingRouter.reportUnexitedValidator(moduleId, nodeOpId, pubkey, secondsSinceEligibleExitRequest); + } + } + + /** + * @notice Verifies historical blocks (via historical_summaries) and checks that certain validators + * are still active at that old block. If they're still active, it reports them to Staking Router. + * @dev The oldBlock.header must have slot >= FIRST_SUPPORTED_SLOT. + * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. + * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. + * @param oldBlock Historical block header witness data and its proof. + * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited in oldBlock.header. + */ + function verifyHistoricalActiveValidatorsAfterExitRequest( + ProvableBeaconBlockHeader calldata beaconBlock, + HistoricalHeaderWitness calldata oldBlock, + ValidatorWitness[] calldata validatorWitnesses, + ExitRequestData calldata exitRequests + ) external { + _verifyBeaconBlockRoot(beaconBlock); + _verifyHistoricalBeaconBlockRoot(beaconBlock, oldBlock); + + IValidatorsExitBusOracle vebo = IValidatorsExitBusOracle(LOCATOR.validatorsExitBusOracle()); + IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); + + ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + + for (uint256 i = 0; i < validatorWitnesses.length; i++) { + (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = vebo.unpackExitRequest( + exitRequests.data, + exitRequests.dataFormat, + validatorWitnesses[i].exitRequestIndex + ); + + uint64 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( + _getExitRequestTimestamp(requestsDeliveryHistory, validatorWitnesses[i].exitRequestIndex), + oldBlock.header.slot, + validatorWitnesses[i].activationEpoch + ); + + _verifyValidatorIsNotExited(oldBlock.header, validatorWitnesses[i], pubkey, valIndex); + + stakingRouter.reportUnexitedValidator(moduleId, nodeOpId, pubkey, secondsSinceEligibleExitRequest); + } + } + + /** + * @dev Verifies the beacon block header is known in EIP-4788. + * @param beaconBlock The provable beacon block header and the EIP-4788 timestamp. + */ + function _verifyBeaconBlockRoot(ProvableBeaconBlockHeader calldata beaconBlock) internal view { + if (beaconBlock.header.slot < FIRST_SUPPORTED_SLOT) { + revert UnsupportedSlot(beaconBlock.header.slot); + } + + (bool success, bytes memory data) = BEACON_ROOTS.staticcall(abi.encode(beaconBlock.rootsTimestamp)); + if (!success || data.length == 0) { + revert RootNotFound(); + } + + bytes32 trustedRoot = abi.decode(data, (bytes32)); + if (trustedRoot != beaconBlock.header.hashTreeRoot()) { + revert InvalidBlockHeader(); + } + } + + function _verifyHistoricalBeaconBlockRoot( + ProvableBeaconBlockHeader calldata beaconBlock, + HistoricalHeaderWitness calldata oldBlock + ) internal view { + if (oldBlock.header.slot < FIRST_SUPPORTED_SLOT) { + revert UnsupportedSlot(oldBlock.header.slot); + } + + if (!_getHistoricalSummariesGI(beaconBlock.header.slot).isParentOf(oldBlock.rootGIndex)) { + revert InvalidGIndex(); + } + + SSZ.verifyProof({ + proof: oldBlock.proof, + root: beaconBlock.header.stateRoot, + leaf: oldBlock.header.hashTreeRoot(), + gI: oldBlock.rootGIndex + }); + } + + /** + * @dev Verifies that a validator is still active (exitEpoch == FAR_FUTURE_EPOCH) and proves it against the state root. + */ + function _verifyValidatorIsNotExited( + BeaconBlockHeader calldata header, + ValidatorWitness calldata witness, + bytes memory pubkey, + uint256 validatorIndex + ) internal view { + Validator memory validator = Validator({ + pubkey: pubkey, + withdrawalCredentials: witness.withdrawalCredentials, + effectiveBalance: witness.effectiveBalance, + slashed: witness.slashed, + activationEligibilityEpoch: witness.activationEligibilityEpoch, + activationEpoch: witness.activationEpoch, + exitEpoch: FAR_FUTURE_EPOCH, + withdrawableEpoch: witness.withdrawableEpoch + }); + + SSZ.verifyProof({ + proof: witness.validatorProof, + root: header.stateRoot, + leaf: validator.hashTreeRoot(), + gI: _getValidatorGI(validatorIndex, header.slot) + }); + } + + /** + * @dev Determines how many seconds have passed since a validator was first eligible to exit after ValidatorsExitBusOracle exit request. + * @param validatorExitRequestTimestamp The timestamp when the validator's exit request was submitted. + * @param referenceSlot A reference slot, used to measure the elapsed duration since the validator became eligible to exit. + * @param validatorActivationEpoch The epoch in which the validator was activated. + * @return uint64 The elapsed seconds since the earliest eligible exit request time. + */ + function _getSecondsSinceExitRequestEligible( + uint64 validatorExitRequestTimestamp, + uint64 referenceSlot, + uint64 validatorActivationEpoch + ) internal view returns (uint64) { + // The earliest a validator can voluntarily exit is after the Shard Committee Period + // subsequent to its activation epoch. + uint64 earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + + (validatorActivationEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + + SHARD_COMMITTEE_PERIOD_IN_SECONDS; + + // The actual eligible timestamp is the max between the exit request submission time + // and the earliest possible voluntary exit time. + uint64 eligibleExitRequestTimestamp = validatorExitRequestTimestamp > earliestPossibleVoluntaryExitTimestamp + ? validatorExitRequestTimestamp + : earliestPossibleVoluntaryExitTimestamp; + + uint64 referenceTimestamp = GENESIS_TIME + referenceSlot * SECONDS_PER_SLOT; + + if (referenceTimestamp < eligibleExitRequestTimestamp) { + revert ExitRequestNotEligibleOnProvableBeaconBlock(referenceTimestamp, eligibleExitRequestTimestamp); + } + + return referenceTimestamp - eligibleExitRequestTimestamp; + } + + function _getValidatorGI(uint256 offset, uint64 stateSlot) internal view returns (GIndex) { + GIndex gI = stateSlot < PIVOT_SLOT ? GI_FIRST_VALIDATOR_PREV : GI_FIRST_VALIDATOR_CURR; + return gI.shr(offset); + } + + function _getHistoricalSummariesGI(uint64 stateSlot) internal view returns (GIndex) { + return stateSlot < PIVOT_SLOT ? GI_HISTORICAL_SUMMARIES_PREV : GI_HISTORICAL_SUMMARIES_CURR; + } + + function _getExitRequestDeliveryHistory( + IValidatorsExitBusOracle vebo, + ExitRequestData calldata exitRequests + ) internal view returns (ExitRequestsDeliveryHistory memory) { + bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); + (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) = vebo + .getExitRequestsDeliveryHistory(exitRequestsHash); + + return ExitRequestsDeliveryHistory(totalItemsCount, deliveredItemsCount, history); + } + + function _getExitRequestTimestamp( + ExitRequestsDeliveryHistory memory history, + uint256 index + ) internal pure returns (uint64 validatorExitRequestTimestamp) { + if (index >= history.totalItemsCount) { + revert KeyIndexOutOfRange(index, history.totalItemsCount); + } + + if (index > history.deliveredItemsCount - 1) { + revert KeyWasNotUnpacked(index, history.deliveredItemsCount - 1); + } + + for (uint256 i = 0; i < history.deliveryHistory.length; i++) { + if (history.deliveryHistory[i].lastDeliveredKeyIndex >= index) { + return history.deliveryHistory[i].timestamp; + } + } + + // As the loop should always end prematurely with the `return` statement, + // this code should be unreachable. We assert `false` just to be safe. + assert(false); + } +} diff --git a/contracts/0.8.25/interfaces/IStakingRouter.sol b/contracts/0.8.25/interfaces/IStakingRouter.sol new file mode 100644 index 0000000000..8796aefe3f --- /dev/null +++ b/contracts/0.8.25/interfaces/IStakingRouter.sol @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +interface IStakingRouter { + function reportUnexitedValidator( + uint256 moduleId, + uint256 nodeOperatorId, + bytes calldata publicKey, + uint256 secondsSinceEligibleExitRequest + ) external; +} diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol b/contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol new file mode 100644 index 0000000000..3b0ec9daf7 --- /dev/null +++ b/contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +pragma solidity 0.8.25; + +struct DeliveryHistory { + uint64 timestamp; + // Key index in exit request array + uint256 lastDeliveredKeyIndex; +} + +interface IValidatorsExitBusOracle { + function getExitRequestsDeliveryHistory( + bytes32 exitRequestsHash + ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history); + + function unpackExitRequest( + bytes calldata exitRequests, + uint256 dataFormat, + uint256 index + ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); +} diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index d1a2f07951..44518b21ec 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -311,6 +311,72 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus result.requestsSubmitted = procState.requestsProcessed; } + function unpackExitRequest( + bytes calldata exitRequests, + uint256 dataFormat, + uint256 index + ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { + if (dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(dataFormat); + } + + if (exitRequests.length % PACKED_REQUEST_LENGTH != 0) { + revert InvalidRequestsDataLength(); + } + + if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { + revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); + } + + uint256 itemOffset; + uint256 dataWithoutPubkey; + + assembly { + // Compute the start of this packed request (item) + itemOffset := add(exitRequests.offset, mul(PACKED_REQUEST_LENGTH, index)) + + // Load the first 16 bytes which contain moduleId (24 bits), + // nodeOpId (40 bits), and valIndex (64 bits). + dataWithoutPubkey := shr(128, calldataload(itemOffset)) + } + + // dataWithoutPubkey format (128 bits total): + // MSB <-------------------- 128 bits --------------------> LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + + valIndex = uint64(dataWithoutPubkey); + nodeOpId = uint40(dataWithoutPubkey >> 64); + moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + // Allocate a new bytes array in memory for the pubkey + pubkey = new bytes(PUBLIC_KEY_LENGTH); + + assembly { + // Starting offset in calldata for the pubkey part + let pubkeyCalldataOffset := add(itemOffset, 16) + + // Memory location of the 'pubkey' bytes array data + let pubkeyMemPtr := add(pubkey, 32) + + // Copy the 48 bytes of the pubkey from calldata into memory + calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + } + + return (pubkey, nodeOpId, moduleId, valIndex); + } + + function getExitRequestsDeliveryHistory( + bytes32 exitRequestsHash + ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { + RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; + + if (requestStatus.contractVersion == 0) { + revert ExitHashWasNotSubmitted(); + } + + return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); + } + /// /// Implementation & helpers /// diff --git a/lib/eip4788-beaconRoots.ts b/lib/eip4788-beaconRoots.ts new file mode 100644 index 0000000000..5ce929262c --- /dev/null +++ b/lib/eip4788-beaconRoots.ts @@ -0,0 +1,23 @@ +import { impersonate } from "lib"; + +// Address of the Beacon Block Storage contract, which exposes beacon chain roots. +// This corresponds to `BEACON_ROOTS_ADDRESS` as specified in EIP-4788. +export const BEACON_ROOTS_ADDRESS = "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02"; + +export const updateBeaconBlockRoot = async (root: string): Promise => { + const beaconRootUpdater = await impersonate( + "0xfffffffffffffffffffffffffffffffffffffffe", + 999999999999999999999999999n, + ); + + const transaction = await beaconRootUpdater.sendTransaction({ + to: BEACON_ROOTS_ADDRESS, + value: 0, + data: root, + }); + + const blockDetails = await transaction.getBlock(); + if (!blockDetails) throw new Error("Failed to retrieve block details."); + + return blockDetails.timestamp; +}; diff --git a/lib/index.ts b/lib/index.ts index a2dde748d8..f506852ff5 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,6 +7,7 @@ export * from "./deploy"; export * from "./dsm"; export * from "./ec"; export * from "./eip712"; +export * from "./eip4788-beaconRoots"; export * from "./ens"; export * from "./event"; export * from "./keccak"; diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol new file mode 100644 index 0000000000..d522730377 --- /dev/null +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IStakingRouter} from "contracts/0.8.25/interfaces/IStakingRouter.sol"; + +contract StakingRouter_Mock is IStakingRouter { + // An event to track when reportUnexitedValidator is called + event UnexitedValidatorReported( + uint256 moduleId, + uint256 nodeOperatorId, + bytes publicKey, + uint256 secondsSinceEligibleExitRequest + ); + + function reportUnexitedValidator( + uint256 moduleId, + uint256 nodeOperatorId, + bytes calldata publicKey, + uint256 secondsSinceEligibleExitRequest + ) external { + // Emit an event so that testing frameworks can detect this call + emit UnexitedValidatorReported(moduleId, nodeOperatorId, publicKey, secondsSinceEligibleExitRequest); + } +} diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol new file mode 100644 index 0000000000..84c4724967 --- /dev/null +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IValidatorsExitBusOracle, DeliveryHistory} from "contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol"; + +struct MockExitRequestDeliveryHistory { + uint256 totalItemsCount; + uint256 deliveredItemsCount; + DeliveryHistory[] history; +} + +struct MockExitRequestData { + bytes pubkey; + uint256 nodeOpId; + uint256 moduleId; + uint256 valIndex; +} +contract ValidatorsExitBusOracle_Mock is IValidatorsExitBusOracle { + bytes32 _hash; + MockExitRequestDeliveryHistory private _history; + MockExitRequestData[] private _data; + + function setExitRequests( + bytes32 exitRequestsHash, + MockExitRequestDeliveryHistory calldata history, + MockExitRequestData[] calldata data + ) external { + _hash = exitRequestsHash; + _history = history; + + for (uint256 i = 0; i < data.length; i++) { + _data.push(data[i]); + } + } + + function getExitRequestsDeliveryHistory( + bytes32 exitRequestsHash + ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { + require(exitRequestsHash == _hash, "Mock error, Invalid exitRequestsHash"); + totalItemsCount = _history.totalItemsCount; + deliveredItemsCount = _history.deliveredItemsCount; + history = _history.history; + } + + function unpackExitRequest( + bytes calldata exitRequests, + uint256 dataFormat, + uint256 index + ) external view override returns (bytes memory, uint256, uint256, uint256) { + require(keccak256(abi.encode(exitRequests, dataFormat)) == _hash, "Mock error, Invalid exitRequestsHash"); + + MockExitRequestData memory data = _data[index]; + return (data.pubkey, data.nodeOpId, data.moduleId, data.valIndex); + } +} + +library ExitRequests { + uint256 internal constant PACKED_REQUEST_LENGTH = 64; + uint256 internal constant PUBLIC_KEY_LENGTH = 48; + + error ExitRequestIndexOutOfRange(uint256 exitRequestIndex); + + function unpackExitRequest( + bytes calldata exitRequests, + uint256 exitRequestIndex + ) internal pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { + if (exitRequestIndex >= count(exitRequests)) { + revert ExitRequestIndexOutOfRange(exitRequestIndex); + } + + uint256 itemOffset; + uint256 dataWithoutPubkey; + + assembly { + // Compute the start of this packed request (item) + itemOffset := add(exitRequests.offset, mul(PACKED_REQUEST_LENGTH, exitRequestIndex)) + + // Load the first 16 bytes which contain moduleId (24 bits), + // nodeOpId (40 bits), and valIndex (64 bits). + dataWithoutPubkey := shr(128, calldataload(itemOffset)) + } + + // dataWithoutPubkey format (128 bits total): + // MSB <-------------------- 128 bits --------------------> LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + + valIndex = uint64(dataWithoutPubkey); + nodeOpId = uint40(dataWithoutPubkey >> 64); + moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + // Allocate a new bytes array in memory for the pubkey + pubkey = new bytes(PUBLIC_KEY_LENGTH); + + assembly { + // Starting offset in calldata for the pubkey part + let pubkeyCalldataOffset := add(itemOffset, 16) + + // Memory location of the 'pubkey' bytes array data + let pubkeyMemPtr := add(pubkey, 32) + + // Copy the 48 bytes of the pubkey from calldata into memory + calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + } + + return (pubkey, nodeOpId, moduleId, valIndex); + } + + /** + * @dev Counts how many exit requests are packed in the given calldata array. + */ + function count(bytes calldata exitRequests) internal pure returns (uint256) { + return exitRequests.length / PACKED_REQUEST_LENGTH; + } +} diff --git a/test/0.8.25/validatorExitVerifier.test.ts b/test/0.8.25/validatorExitVerifier.test.ts new file mode 100644 index 0000000000..c10ad8709e --- /dev/null +++ b/test/0.8.25/validatorExitVerifier.test.ts @@ -0,0 +1,491 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { StakingRouter_Mock, ValidatorExitVerifier, ValidatorsExitBusOracle_Mock } from "typechain-types"; +import { ILidoLocator } from "typechain-types/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol"; + +import { updateBeaconBlockRoot } from "lib"; + +import { deployLidoLocator } from "test/deploy"; +import { Snapshot } from "test/suite"; + +import { + encodeExitRequestsDataListWithFormat, + ExitRequest, + findStakingRouterMockEvents, + toHistoricalHeaderWitness, + toProvableBeaconBlockHeader, + toValidatorWitness, +} from "./validatorExitVerifierHelpers"; +import { ACTIVE_VALIDATOR_PROOF } from "./validatorState"; + +const EMPTY_REPORT = { data: "0x", dataFormat: 1n }; + +describe("ValidatorExitVerifier.sol", () => { + let originalState: string; + + beforeEach(async () => { + originalState = await Snapshot.take(); + }); + + afterEach(async () => { + await Snapshot.restore(originalState); + }); + + const FIRST_SUPPORTED_SLOT = 1; + const PIVOT_SLOT = 2; + const SLOTS_PER_EPOCH = 32; + const SECONDS_PER_SLOT = 12; + const GENESIS_TIME = 1606824000; + const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; + const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; + + describe("ValidatorExitVerifier Constructor", () => { + const GI_FIRST_VALIDATOR_PREV = `0x${"1".repeat(64)}`; + const GI_FIRST_VALIDATOR_CURR = `0x${"2".repeat(64)}`; + const GI_HISTORICAL_SUMMARIES_PREV = `0x${"3".repeat(64)}`; + const GI_HISTORICAL_SUMMARIES_CURR = `0x${"4".repeat(64)}`; + + let validatorExitVerifier: ValidatorExitVerifier; + + before(async () => { + validatorExitVerifier = await ethers.deployContract("ValidatorExitVerifier", [ + LIDO_LOCATOR, + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_HISTORICAL_SUMMARIES_PREV, + GI_HISTORICAL_SUMMARIES_CURR, + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]); + }); + + it("sets all parameters correctly", async () => { + expect(await validatorExitVerifier.LOCATOR()).to.equal(LIDO_LOCATOR); + expect(await validatorExitVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); + expect(await validatorExitVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); + expect(await validatorExitVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); + expect(await validatorExitVerifier.GI_HISTORICAL_SUMMARIES_PREV()).to.equal(GI_HISTORICAL_SUMMARIES_PREV); + expect(await validatorExitVerifier.GI_HISTORICAL_SUMMARIES_CURR()).to.equal(GI_HISTORICAL_SUMMARIES_CURR); + expect(await validatorExitVerifier.FIRST_SUPPORTED_SLOT()).to.equal(FIRST_SUPPORTED_SLOT); + expect(await validatorExitVerifier.PIVOT_SLOT()).to.equal(PIVOT_SLOT); + expect(await validatorExitVerifier.SLOTS_PER_EPOCH()).to.equal(SLOTS_PER_EPOCH); + expect(await validatorExitVerifier.SECONDS_PER_SLOT()).to.equal(SECONDS_PER_SLOT); + expect(await validatorExitVerifier.GENESIS_TIME()).to.equal(GENESIS_TIME); + expect(await validatorExitVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()).to.equal( + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ); + }); + + it("reverts with 'InvalidPivotSlot' if firstSupportedSlot > pivotSlot", async () => { + await expect( + ethers.deployContract("ValidatorExitVerifier", [ + LIDO_LOCATOR, + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_HISTORICAL_SUMMARIES_PREV, + GI_HISTORICAL_SUMMARIES_CURR, + 200_000, // firstSupportedSlot + 100_000, // pivotSlot < firstSupportedSlot + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]), + ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidPivotSlot"); + }); + }); + + describe("verifyActiveValidatorsAfterExitRequest method", () => { + const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000056000000000028"; + const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000003b00"; + + let validatorExitVerifier: ValidatorExitVerifier; + + let locator: ILidoLocator; + let locatorAddr: string; + + let vebo: ValidatorsExitBusOracle_Mock; + let veboAddr: string; + + let stakingRouter: StakingRouter_Mock; + let stakingRouterAddr: string; + + before(async () => { + vebo = await ethers.deployContract("ValidatorsExitBusOracle_Mock"); + veboAddr = await vebo.getAddress(); + + stakingRouter = await ethers.deployContract("StakingRouter_Mock"); + stakingRouterAddr = await stakingRouter.getAddress(); + + locator = await deployLidoLocator({ validatorsExitBusOracle: veboAddr, stakingRouter: stakingRouterAddr }); + locatorAddr = await locator.getAddress(); + + validatorExitVerifier = await ethers.deployContract("ValidatorExitVerifier", [ + locatorAddr, + GI_FIRST_VALIDATOR_INDEX, + GI_FIRST_VALIDATOR_INDEX, + GI_HISTORICAL_SUMMARIES_INDEX, + GI_HISTORICAL_SUMMARIES_INDEX, + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]); + }); + + it("accepts a valid proof and does not revert", async () => { + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests( + encodedExitRequestsHash, + { + totalItemsCount: 1n, + deliveredItemsCount: 1n, + history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], + }, + exitRequests, + ); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + const tx = await validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ); + + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + + const event = events[0]; + expect(event.args[0]).to.equal(moduleId); + expect(event.args[1]).to.equal(nodeOpId); + expect(event.args[2]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(event.args[3]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + }); + + it("accepts a valid historical proof and does not revert", async () => { + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests( + encodedExitRequestsHash, + { + totalItemsCount: 1n, + deliveredItemsCount: 1n, + history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], + }, + exitRequests, + ); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + const tx = await validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ); + + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + + const event = events[0]; + expect(event.args[0]).to.equal(moduleId); + expect(event.args[1]).to.equal(nodeOpId); + expect(event.args[2]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(event.args[3]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + }); + + it("reverts with 'UnsupportedSlot' when slot < FIRST_SUPPORTED_SLOT", async () => { + // Use a slot smaller than FIRST_SUPPORTED_SLOT + const invalidHeader = { + ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + slot: 0, + }; + + await expect( + validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + { + rootsTimestamp: 1n, + header: invalidHeader, + }, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'RootNotFound' if the staticcall to the block roots contract fails/returns empty", async () => { + const badTimestamp = 999_999_999; + await expect( + validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + { + rootsTimestamp: badTimestamp, + header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + }, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "RootNotFound"); + }); + + it("reverts with 'InvalidBlockHeader' if the block root from contract doesn't match the header root", async () => { + const bogusBlockRoot = "0xbadbadbad0000000000000000000000000000000000000000000000000000000"; + const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); + + await expect( + validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidBlockHeader"); + }); + + it("reverts if the validator proof is incorrect", async () => { + const intervalInSecondsBetweenProvableBlockAndExitRequest = 1000; + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const veboExitRequestTimestamp = blockRootTimestamp - intervalInSecondsBetweenProvableBlockAndExitRequest; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests( + encodedExitRequestsHash, + { + totalItemsCount: 1n, + deliveredItemsCount: 1n, + history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], + }, + exitRequests, + ); + + const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + // Mutate one proof entry to break it + const badWitness = { + exitRequestIndex: 0n, + ...ACTIVE_VALIDATOR_PROOF.validator, + validatorProof: [ + ...ACTIVE_VALIDATOR_PROOF.validatorProof.slice(0, -1), + "0xbadbadbad0000000000000000000000000000000000000000000000000000000", // corrupt last entry + ], + }; + + await expect( + validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, timestamp), + [badWitness], + encodedExitRequests, + ), + ).to.be.reverted; + }); + + it("reverts with 'UnsupportedSlot' if beaconBlock slot < FIRST_SUPPORTED_SLOT", async () => { + const invalidHeader = { + ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + slot: 0, + }; + + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + { + rootsTimestamp: 1n, + header: invalidHeader, + }, + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'UnsupportedSlot' if oldBlock slot < FIRST_SUPPORTED_SLOT", async () => { + const invalidHeader = { + ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + slot: 0, + }; + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + { + header: invalidHeader, + rootGIndex: ACTIVE_VALIDATOR_PROOF.historicalSummariesGI, + proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, + }, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'RootNotFound' if block root contract call fails", async () => { + const badTimestamp = 999_999_999; + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, badTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "RootNotFound"); + }); + + it("reverts with 'InvalidBlockHeader' if returned root doesn't match the new block header root", async () => { + const bogusBlockRoot = "0xbadbadbad0000000000000000000000000000000000000000000000000000000"; + const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); + + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidBlockHeader"); + }); + + it("reverts with 'InvalidGIndex' if oldBlock.rootGIndex is not under the historicalSummaries root", async () => { + // Provide an obviously wrong rootGIndex that won't match the parent's + const invalidRootGIndex = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + + const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), + { + header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, + rootGIndex: invalidRootGIndex, + }, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidGIndex"); + }); + + it("reverts if the oldBlock proof is corrupted", async () => { + const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), + { + header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + rootGIndex: ACTIVE_VALIDATOR_PROOF.historicalSummariesGI, + // Mutate one proof entry to break the historical block proof + proof: [ + ...ACTIVE_VALIDATOR_PROOF.historicalRootProof.slice(0, -1), + "0xbadbadbad0000000000000000000000000000000000000000000000000000000", + ], + }, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], + EMPTY_REPORT, + ), + ).to.be.reverted; + }); + + it("reverts if the validatorProof in the witness is corrupted", async () => { + const intervalInSecondsBetweenProvableBlockAndExitRequest = 1000; + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const veboExitRequestTimestamp = blockRootTimestamp - intervalInSecondsBetweenProvableBlockAndExitRequest; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests( + encodedExitRequestsHash, + { + totalItemsCount: 1n, + deliveredItemsCount: 1n, + history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], + }, + exitRequests, + ); + + const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + // Mutate one proof entry to break it + const badWitness = { + exitRequestIndex: 0n, + ...ACTIVE_VALIDATOR_PROOF.validator, + validatorProof: [ + ...ACTIVE_VALIDATOR_PROOF.validatorProof.slice(0, -1), + "0xbadbadbad0000000000000000000000000000000000000000000000000000000", // corrupt last entry + ], + }; + + await expect( + validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [badWitness], + encodedExitRequests, + ), + ).to.be.reverted; + }); + }); +}); diff --git a/test/0.8.25/validatorExitVerifierHelpers.ts b/test/0.8.25/validatorExitVerifierHelpers.ts new file mode 100644 index 0000000000..3f5cc8c6d3 --- /dev/null +++ b/test/0.8.25/validatorExitVerifierHelpers.ts @@ -0,0 +1,86 @@ +import { expect } from "chai"; +import { ContractTransactionReceipt, ethers, keccak256 } from "ethers"; + +import { + HistoricalHeaderWitnessStruct, + ProvableBeaconBlockHeaderStruct, + ValidatorWitnessStruct, +} from "typechain-types/contracts/0.8.25/ValidatorExitVerifier"; + +import { de0x, findEventsWithInterfaces, numberToHex } from "lib"; + +import { BlockHeader, ValidatorStateProof } from "./validatorState"; + +export interface ExitRequest { + pubkey: string; + nodeOpId: number; + moduleId: number; + valIndex: number; +} + +export const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, pubkey }: ExitRequest) => { + const pubkeyHex = de0x(pubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +export const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); +}; + +export const encodeExitRequestsDataListWithFormat = (requests: ExitRequest[]) => { + const encodedExitRequests = { data: encodeExitRequestsDataList(requests), dataFormat: 1n }; + + const encodedExitRequestsHash = keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes", "uint256"], + [encodedExitRequests.data, encodedExitRequests.dataFormat], + ), + ); + + return { encodedExitRequests, encodedExitRequestsHash }; +}; + +const stakingRouterMockEventABI = [ + "event UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", +]; +const stakingRouterMockInterface = new ethers.Interface(stakingRouterMockEventABI); +type StakingRouterMockEvents = "UnexitedValidatorReported"; + +export function findStakingRouterMockEvents(receipt: ContractTransactionReceipt, event: StakingRouterMockEvents) { + return findEventsWithInterfaces(receipt!, event, [stakingRouterMockInterface]); +} + +export function toProvableBeaconBlockHeader( + header: BlockHeader, + rootsTimestamp: number, +): ProvableBeaconBlockHeaderStruct { + return { + header: header, + rootsTimestamp, + }; +} + +export function toValidatorWitness( + validatorStateProof: ValidatorStateProof, + exitRequestIndex: number, +): ValidatorWitnessStruct { + return { + exitRequestIndex, + withdrawalCredentials: validatorStateProof.validator.withdrawalCredentials, + effectiveBalance: validatorStateProof.validator.effectiveBalance, + activationEligibilityEpoch: validatorStateProof.validator.activationEligibilityEpoch, + activationEpoch: validatorStateProof.validator.activationEpoch, + withdrawableEpoch: validatorStateProof.validator.withdrawableEpoch, + slashed: validatorStateProof.validator.slashed, + validatorProof: validatorStateProof.validatorProof, + }; +} + +export function toHistoricalHeaderWitness(validatorStateProf: ValidatorStateProof): HistoricalHeaderWitnessStruct { + return { + header: validatorStateProf.beaconBlockHeader, + rootGIndex: validatorStateProf.historicalSummariesGI, + proof: validatorStateProf.historicalRootProof, + }; +} diff --git a/test/0.8.25/validatorState.ts b/test/0.8.25/validatorState.ts new file mode 100644 index 0000000000..99519dcb51 --- /dev/null +++ b/test/0.8.25/validatorState.ts @@ -0,0 +1,142 @@ +export type BlockHeader = { + slot: number; + proposerIndex: string; + parentRoot: string; + stateRoot: string; + bodyRoot: string; +}; + +export type ValidatorState = { + pubkey: string; + index: number; + withdrawalCredentials: string; + effectiveBalance: bigint; + activationEligibilityEpoch: bigint; + activationEpoch: bigint; + exitEpoch: bigint; + withdrawableEpoch: bigint; + slashed: boolean; +}; + +export type ValidatorStateProof = { + beaconBlockHeaderRoot: string; + beaconBlockHeader: BlockHeader; + futureBeaconBlockHeaderRoot: string; + futureBeaconBlockHeader: BlockHeader; + validator: ValidatorState; + validatorProof: string[]; + historicalSummariesGI: string; + historicalRootProof: string[]; +}; + +export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { + beaconBlockHeaderRoot: "0xa7f100995b35584c670fe25aa97ae23a8305f5eba8eee3532dedfcc8cf934dca", + beaconBlockHeader: { + slot: 10080800, + proposerIndex: "1337", + parentRoot: "0x03aa03b69bedd0e423ba545d38e216c4bf2f423e6f5a308477501b9a31ff8d8f", + stateRoot: "0x508ee9ba052583d9cae510e7333d9776514d42cd10b853395dc24c275a95bc1d", + bodyRoot: "0x8db50db3356352a01197abd32a52f97c2bb9b48bdbfb045ea4a7f67c9b84be0b", + }, + futureBeaconBlockHeaderRoot: "0xca237c523d507a91b2b91389d517c0d4b03e66732984b5d56c74a47a06eb7ef4", + futureBeaconBlockHeader: { + slot: 14411095, + proposerIndex: "31415", + parentRoot: "0x391127160b857e9cdec243ea70f42082d28135c75880c2b5c505b98dec726c79", + stateRoot: "0x972b36a298aa6bc1d205d115f0384fe1e3a301625907c07f5344b26337d5f494", + bodyRoot: "0x8db50db3356352a01197abd32a52f97c2bb9b48bdbfb045ea4a7f67c9b84be0b", + }, + validator: { + pubkey: "0x800000c8a5364c1d1e3c4cdb65a28fd21daff4e1fb426c0fb09808105467e4a490d8b3507e7efffbd71024129f1a6b8d", + withdrawalCredentials: "0x0100000000000000000000007cd73ab82e3a8e74a3fdfd6a41fed60536b8e501", + effectiveBalance: 32000000000n, + activationEligibilityEpoch: 207905n, + activationEpoch: 217838n, + exitEpoch: 18446744073709551615n, + withdrawableEpoch: 18446744073709551615n, + slashed: false, + index: 773833, + }, + validatorProof: [ + "0xcb6bfee06d1227e0f2d9cca5bd508b7fc1069379141f44b0d683eb5aec483005", + "0x1c8852d46a4244090d9b25822086fb3616072c2ae7b8a89d04b4db9953ed922d", + "0x671048760e5cadb005cf8ed6a11fd398b882cb2610c8ab25c0cd8f1bb2a663dc", + "0x5fa5cf691165e3159b86e357c2a4e82c867014e7ec2570e38d3cc3bb694b35e2", + "0xe5ef1dd73ffa166b176139a24d4d8b53361df9dc26f5ac51c0bf642d9b5dbf25", + "0xdb356970833ed8b780d20530aa5e0a8bd5ebd2c751c4e9ddc25e0097c629e750", + "0xceb46d7f9478540174155825a82db4b38201d4d4c047dbefb7546eaea942a6de", + "0x89c916b9678fbcde3d7d07c26de94fd62c2ae51800b392a83b6f346126c40c6d", + "0x1da07003bdc86171360808803bbeb41919e25118c7e8aefb9a21f46d5f19e72b", + "0xad57317afc56b03b6e198ed270b64db4a8f25f132dbf6b56d287c97c6b525db9", + "0x40f9f5e8fe27eadfcf3c3af2ff0e02ccdce8b536cd4faf5b8ed0a36d40247663", + "0x05b761f89ed65cf91ac63aad3c8c50bb2aa0c277639d0fd784b6e0b2ccf05395", + "0x3fd79435deff850fae1bdef0d77a3ffe93b092172e225837cf4ef141fa5689cb", + "0x044709022ba087a75f6ea66b7a3a1e23fe3712fd351c401f03b578ba8aa0a603", + "0xe45e266fed3b13b3c8a81fa3064b5af5e25f9b274da2da4032358766d23a9eac", + "0x046d692534483df5307eb2d69c5a1f8b27068ad1dda96423f854fc88e19571a8", + "0x7f9ef0a29605f457a735757148c16f88bda95ee0eaaf7e5351fa6ea3aa3cf305", + "0x1a1965b540ad413b822af6f49160553bd0fd6f9adefcdf5ef862262af43ddd54", + "0x56206a2520034ea75dab955bc85a305b4681191255111c2c8d27ac23173e5647", + "0x5ee416708837b80e3f2b625cbd130839d8efdbe88bcbb0076ffdd8cd2229c103", + "0xb0019865e6408ce0d5a36a6188d7c1e3272976c6a1ccbc58e6c35cca19a8fb6c", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0x455d180000000000000000000000000000000000000000000000000000000000", + "0x87ed190000000000000000000000000000000000000000000000000000000000", + "0xb95e35337be0ebfa1ae00f659346dfce7bb59865d4bde0299df3e548c24e00aa", + "0x001b9a4b331100497e69174269986fcd37e62145bf51123cb67fb3108c2422fd", + "0x339028e1baffbe94bcf2d5e671de99ff958e0c8afd8c1844370dc1af2fa00315", + "0xa48b01f6407ef8dc6b77f5df0fa4fef5b1b9795c7e99c13fa8aad0eac6036676", + ], + historicalSummariesGI: "0x000000000000000000000000000000000000000000000000000000ec00000000", + historicalRootProof: [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0100000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x2658397f87f190d84814e4595b3ec8eb0110ab5be675d59434d5a3dfd5ef760d", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xe537052d30df4f0436cd5a3c5debd331c770d9df46da47e0e3db74906186fa09", + "0x4616e1d9312a92eb228e8cd5483fa1fca64d99781d62129bc53718d194b98c45", + ], +}; diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 87e6cd5410..4279f2369d 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -34,4 +34,20 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { function getDataProcessingState() external view returns (DataProcessingState memory) { return _storageDataProcessingState().value; } + + function storeExitRequestHash( + bytes32 exitRequestHash, + uint256 totalItemsCount, + uint256 deliveredItemsCount, + uint256 contractVersion, + uint256 lastDeliveredKeyIndex + ) external { + _storeExitRequestHash( + exitRequestHash, + totalItemsCount, + deliveredItemsCount, + contractVersion, + lastDeliveredKeyIndex + ); + } } diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts new file mode 100644 index 0000000000..254173cacb --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -0,0 +1,166 @@ +import { expect } from "chai"; +import { keccak256 } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { ValidatorsExitBus__Harness } from "typechain-types"; + +import { de0x, numberToHex } from "lib"; + +import { deployVEBO } from "test/deploy"; +import { Snapshot } from "test/suite"; + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", +]; + +const DATA_FORMAT_LIST = 1; + +describe("ValidatorsExitBusOracle.sol:helpers", () => { + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + + interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; + } + + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; + }; + + const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); + }; + + const deploy = async () => { + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + }; + + before(async () => { + [admin] = await ethers.getSigners(); + + await deploy(); + }); + + context("unpackExitRequest", () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it("reports items unpacked correctly (happy path)", async () => { + const exitRequests = [ + { moduleId: 1, nodeOpId: 1, valIndex: 1, valPubkey: PUBKEYS[0] }, + { moduleId: 2, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 3, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 4, nodeOpId: 4, valIndex: 4, valPubkey: PUBKEYS[3] }, + ]; + + const data = encodeExitRequestsDataList(exitRequests); + + for (let i = 0; i < exitRequests.length; i++) { + const { pubkey, nodeOpId, moduleId, valIndex } = await oracle.unpackExitRequest(data, DATA_FORMAT_LIST, i); + const expectedRequest = exitRequests[i]; + + expect(pubkey).to.equal(expectedRequest.valPubkey); + expect(nodeOpId).to.equal(expectedRequest.nodeOpId); + expect(moduleId).to.equal(expectedRequest.moduleId); + expect(valIndex).to.equal(expectedRequest.valIndex); + } + }); + + it("reverts if data format is not LIST (i.e., not 1)", async () => { + const exitRequests = [{ moduleId: 1, nodeOpId: 1, valIndex: 1, valPubkey: PUBKEYS[0] }]; + const data = encodeExitRequestsDataList(exitRequests); + const invalidDataFormat = 2; + + await expect(oracle.unpackExitRequest(data, invalidDataFormat, 0)) + .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") + .withArgs(invalidDataFormat); + }); + + it("reverts if exitRequests length is not multiple of PACKED_REQUEST_LENGTH", async () => { + // PACKED_REQUEST_LENGTH is 64 bytes. Let's make the data 63 bytes instead. + await expect(oracle.unpackExitRequest("0x" + "ff".repeat(63), DATA_FORMAT_LIST, 0)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataLength", + ); + + // PACKED_REQUEST_LENGTH is 64 bytes. Let's make the data 65 bytes instead. + await expect(oracle.unpackExitRequest("0x" + "ff".repeat(65), DATA_FORMAT_LIST, 0)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataLength", + ); + }); + + it("reverts if the index is out of range (KeyIndexOutOfRange)", async () => { + // We have only 1 request => 64 bytes + const exitRequests = [{ moduleId: 1, nodeOpId: 1, valIndex: 1, valPubkey: PUBKEYS[0] }]; + const data = encodeExitRequestsDataList(exitRequests); + + // There is exactly 1 request, so index=1 is out of range (should be 0) + await expect(oracle.unpackExitRequest(data, DATA_FORMAT_LIST, 1)) + .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") + .withArgs(1, 1); // index=1, total=1 + }); + }); + + context("getExitRequestsDeliveryHistory", () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it("reverts if exitRequestsHash was never submitted (contractVersion = 0)", async () => { + const fakeHash = keccak256("0x1111"); + + await expect(oracle.getExitRequestsDeliveryHistory(fakeHash)).to.be.revertedWithCustomError( + oracle, + "ExitHashWasNotSubmitted", + ); + }); + + it("returns correct data for a previously stored exitRequestsHash", async () => { + const exitRequestsHash = keccak256("0x1111"); + const totalItemsCount = 5; + const deliveredItemsCount = 2; + const contractVersion = 42; + const lastDeliveredKeyIndex = 1; + + // Call the helper to store the hash + await oracle.storeExitRequestHash( + exitRequestsHash, + totalItemsCount, + deliveredItemsCount, + contractVersion, + lastDeliveredKeyIndex, + ); + + const [returnedTotalItemsCount, returnedDeliveredItemsCount, returnedHistory] = + await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + + expect(returnedTotalItemsCount).to.equal(totalItemsCount); + expect(returnedDeliveredItemsCount).to.equal(deliveredItemsCount); + expect(returnedHistory.length).to.equal(1); + const [firstDelivery] = returnedHistory; + expect(firstDelivery.lastDeliveredKeyIndex).to.equal(lastDeliveredKeyIndex); + }); + }); +}); From e07436524d55fcce6cf42e3c47338142b9f1bbac Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 8 Apr 2025 14:33:53 +0200 Subject: [PATCH 059/405] feat: update staking router interface --- contracts/0.8.25/ValidatorExitVerifier.sol | 68 ++++++++++++------- .../0.8.25/interfaces/IStakingRouter.sol | 11 +-- test/0.8.25/contracts/StakingRouter_Mock.sol | 14 +++- test/0.8.25/validatorExitVerifier.test.ts | 12 ++-- test/0.8.25/validatorExitVerifierHelpers.ts | 2 +- 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitVerifier.sol b/contracts/0.8.25/ValidatorExitVerifier.sol index 1a7c952292..3f5196c6d6 100644 --- a/contracts/0.8.25/ValidatorExitVerifier.sol +++ b/contracts/0.8.25/ValidatorExitVerifier.sol @@ -178,23 +178,32 @@ contract ValidatorExitVerifier { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + uint64 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { + ValidatorWitness calldata witness = validatorWitnesses[i]; + (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = vebo.unpackExitRequest( exitRequests.data, exitRequests.dataFormat, - validatorWitnesses[i].exitRequestIndex + witness.exitRequestIndex ); uint64 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( - _getExitRequestTimestamp(requestsDeliveryHistory, validatorWitnesses[i].exitRequestIndex), - beaconBlock.header.slot, - validatorWitnesses[i].activationEpoch + requestsDeliveryHistory, + witness, + proofSlotTimestamp ); _verifyValidatorIsNotExited(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); - stakingRouter.reportUnexitedValidator(moduleId, nodeOpId, pubkey, secondsSinceEligibleExitRequest); + stakingRouter.shouldValidatorBePenalized( + moduleId, + nodeOpId, + proofSlotTimestamp, + pubkey, + secondsSinceEligibleExitRequest + ); } } @@ -220,23 +229,32 @@ contract ValidatorExitVerifier { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + uint64 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { + ValidatorWitness calldata witness = validatorWitnesses[i]; + (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = vebo.unpackExitRequest( exitRequests.data, exitRequests.dataFormat, - validatorWitnesses[i].exitRequestIndex + witness.exitRequestIndex ); uint64 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( - _getExitRequestTimestamp(requestsDeliveryHistory, validatorWitnesses[i].exitRequestIndex), - oldBlock.header.slot, - validatorWitnesses[i].activationEpoch + requestsDeliveryHistory, + witness, + proofSlotTimestamp ); - _verifyValidatorIsNotExited(oldBlock.header, validatorWitnesses[i], pubkey, valIndex); + _verifyValidatorIsNotExited(oldBlock.header, witness, pubkey, valIndex); - stakingRouter.reportUnexitedValidator(moduleId, nodeOpId, pubkey, secondsSinceEligibleExitRequest); + stakingRouter.shouldValidatorBePenalized( + moduleId, + nodeOpId, + proofSlotTimestamp, + pubkey, + secondsSinceEligibleExitRequest + ); } } @@ -309,21 +327,21 @@ contract ValidatorExitVerifier { } /** - * @dev Determines how many seconds have passed since a validator was first eligible to exit after ValidatorsExitBusOracle exit request. - * @param validatorExitRequestTimestamp The timestamp when the validator's exit request was submitted. - * @param referenceSlot A reference slot, used to measure the elapsed duration since the validator became eligible to exit. - * @param validatorActivationEpoch The epoch in which the validator was activated. + * @dev Determines how many seconds have passed since a validator was first eligible + * to exit after ValidatorsExitBusOracle exit request. * @return uint64 The elapsed seconds since the earliest eligible exit request time. */ function _getSecondsSinceExitRequestEligible( - uint64 validatorExitRequestTimestamp, - uint64 referenceSlot, - uint64 validatorActivationEpoch + ExitRequestsDeliveryHistory memory history, + ValidatorWitness calldata witness, + uint64 referenceSlotTimestamp ) internal view returns (uint64) { + uint64 validatorExitRequestTimestamp = _getExitRequestTimestamp(history, witness.exitRequestIndex); + // The earliest a validator can voluntarily exit is after the Shard Committee Period // subsequent to its activation epoch. uint64 earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + - (validatorActivationEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + + (witness.activationEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + SHARD_COMMITTEE_PERIOD_IN_SECONDS; // The actual eligible timestamp is the max between the exit request submission time @@ -332,13 +350,11 @@ contract ValidatorExitVerifier { ? validatorExitRequestTimestamp : earliestPossibleVoluntaryExitTimestamp; - uint64 referenceTimestamp = GENESIS_TIME + referenceSlot * SECONDS_PER_SLOT; - - if (referenceTimestamp < eligibleExitRequestTimestamp) { - revert ExitRequestNotEligibleOnProvableBeaconBlock(referenceTimestamp, eligibleExitRequestTimestamp); + if (referenceSlotTimestamp < eligibleExitRequestTimestamp) { + revert ExitRequestNotEligibleOnProvableBeaconBlock(referenceSlotTimestamp, eligibleExitRequestTimestamp); } - return referenceTimestamp - eligibleExitRequestTimestamp; + return referenceSlotTimestamp - eligibleExitRequestTimestamp; } function _getValidatorGI(uint256 offset, uint64 stateSlot) internal view returns (GIndex) { @@ -383,4 +399,8 @@ contract ValidatorExitVerifier { // this code should be unreachable. We assert `false` just to be safe. assert(false); } + + function _slotToTimestamp(uint64 slot) internal view returns (uint64) { + return GENESIS_TIME + slot * SECONDS_PER_SLOT; + } } diff --git a/contracts/0.8.25/interfaces/IStakingRouter.sol b/contracts/0.8.25/interfaces/IStakingRouter.sol index 8796aefe3f..7cf603803a 100644 --- a/contracts/0.8.25/interfaces/IStakingRouter.sol +++ b/contracts/0.8.25/interfaces/IStakingRouter.sol @@ -5,10 +5,11 @@ pragma solidity 0.8.25; interface IStakingRouter { - function reportUnexitedValidator( - uint256 moduleId, - uint256 nodeOperatorId, - bytes calldata publicKey, - uint256 secondsSinceEligibleExitRequest + function shouldValidatorBePenalized( + uint256 _moduleId, + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes calldata _publicKey, + uint256 _eligibleToExitInSec ) external; } diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol index d522730377..848e8f1cf8 100644 --- a/test/0.8.25/contracts/StakingRouter_Mock.sol +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -4,21 +4,29 @@ pragma solidity 0.8.25; import {IStakingRouter} from "contracts/0.8.25/interfaces/IStakingRouter.sol"; contract StakingRouter_Mock is IStakingRouter { - // An event to track when reportUnexitedValidator is called + // An event to track when shouldValidatorBePenalized is called event UnexitedValidatorReported( uint256 moduleId, uint256 nodeOperatorId, + uint256 proofSlotTimestamp, bytes publicKey, uint256 secondsSinceEligibleExitRequest ); - function reportUnexitedValidator( + function shouldValidatorBePenalized( uint256 moduleId, uint256 nodeOperatorId, + uint256 _proofSlotTimestamp, bytes calldata publicKey, uint256 secondsSinceEligibleExitRequest ) external { // Emit an event so that testing frameworks can detect this call - emit UnexitedValidatorReported(moduleId, nodeOperatorId, publicKey, secondsSinceEligibleExitRequest); + emit UnexitedValidatorReported( + moduleId, + nodeOperatorId, + _proofSlotTimestamp, + publicKey, + secondsSinceEligibleExitRequest + ); } } diff --git a/test/0.8.25/validatorExitVerifier.test.ts b/test/0.8.25/validatorExitVerifier.test.ts index c10ad8709e..fe6bc06165 100644 --- a/test/0.8.25/validatorExitVerifier.test.ts +++ b/test/0.8.25/validatorExitVerifier.test.ts @@ -146,6 +146,7 @@ describe("ValidatorExitVerifier.sol", () => { GENESIS_TIME + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * SECONDS_PER_SLOT; + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; const moduleId = 1; const nodeOpId = 2; @@ -184,8 +185,9 @@ describe("ValidatorExitVerifier.sol", () => { const event = events[0]; expect(event.args[0]).to.equal(moduleId); expect(event.args[1]).to.equal(nodeOpId); - expect(event.args[2]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); - expect(event.args[3]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + expect(event.args[2]).to.equal(proofSlotTimestamp); + expect(event.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(event.args[4]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); }); it("accepts a valid historical proof and does not revert", async () => { @@ -194,6 +196,7 @@ describe("ValidatorExitVerifier.sol", () => { GENESIS_TIME + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * SECONDS_PER_SLOT; + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; const moduleId = 1; const nodeOpId = 2; @@ -233,8 +236,9 @@ describe("ValidatorExitVerifier.sol", () => { const event = events[0]; expect(event.args[0]).to.equal(moduleId); expect(event.args[1]).to.equal(nodeOpId); - expect(event.args[2]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); - expect(event.args[3]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + expect(event.args[2]).to.equal(proofSlotTimestamp); + expect(event.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(event.args[4]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); }); it("reverts with 'UnsupportedSlot' when slot < FIRST_SUPPORTED_SLOT", async () => { diff --git a/test/0.8.25/validatorExitVerifierHelpers.ts b/test/0.8.25/validatorExitVerifierHelpers.ts index 3f5cc8c6d3..52604d8836 100644 --- a/test/0.8.25/validatorExitVerifierHelpers.ts +++ b/test/0.8.25/validatorExitVerifierHelpers.ts @@ -42,7 +42,7 @@ export const encodeExitRequestsDataListWithFormat = (requests: ExitRequest[]) => }; const stakingRouterMockEventABI = [ - "event UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", + "event UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", ]; const stakingRouterMockInterface = new ethers.Interface(stakingRouterMockEventABI); type StakingRouterMockEvents = "UnexitedValidatorReported"; From 77bc2bf858929ff5cbaff2ad0482f948906719cf Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 8 Apr 2025 19:32:44 +0400 Subject: [PATCH 060/405] fix: pause on VEB & exits moved in VEB --- .../0.8.9/interfaces/IValidatorExitBus.sol | 40 ++++++ contracts/0.8.9/lib/ExitRequestUtils.sol | 9 ++ contracts/0.8.9/lib/ReportExitLimitUtils.sol | 1 + contracts/0.8.9/oracle/ValidatorsExitBus.sol | 116 ++++++++++++++---- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 87 +------------ .../contracts/ValidatorsExitBus__Harness.sol | 5 + 6 files changed, 151 insertions(+), 107 deletions(-) create mode 100644 contracts/0.8.9/interfaces/IValidatorExitBus.sol create mode 100644 contracts/0.8.9/lib/ExitRequestUtils.sol diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol new file mode 100644 index 0000000000..72d1f95e5b --- /dev/null +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +interface IValidatorsExitBus { + struct ExitRequestData { + bytes data; + uint256 dataFormat; + } + + struct ValidatorExitData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + uint256 validatorIndex; + bytes validatorPubkey; + } + + struct DeliveryHistory { + uint256 lastDeliveredKeyIndex; + uint256 timestamp; + } + + function submitReportHash(bytes32 exitReportHash) external; + + function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external; + + function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable; + + function triggerExitsDirectly(ValidatorExitData calldata validator) external payable returns (uint256); + + function setExitReportLimit(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock) external; + + function getDeliveryHistory(bytes32 exitReportHash) external view returns (DeliveryHistory[] memory); + + function resume() external; + + function pauseFor(uint256 _duration) external; + + function pauseUntil(uint256 _pauseUntilInclusive) external; +} diff --git a/contracts/0.8.9/lib/ExitRequestUtils.sol b/contracts/0.8.9/lib/ExitRequestUtils.sol new file mode 100644 index 0000000000..9fd5c284e1 --- /dev/null +++ b/contracts/0.8.9/lib/ExitRequestUtils.sol @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + + +library ExitRequestUtils { + /// Method + +} \ No newline at end of file diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index 2b9765f9e8..bb6d1211dd 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -129,6 +129,7 @@ library ReportExitLimitUtils { _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); // TODO: check + // in the beginning should be 0 ? _data.prevExitRequestsBlockNumber = uint32(block.number); return _data; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index f9fbdee514..8df40f365c 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -7,6 +7,8 @@ import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; import { Versioned } from "../utils/Versioned.sol"; import { ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData } from "../lib/ReportExitLimitUtils.sol"; +import { PausableUntil } from "../utils/PausableUntil.sol"; +import { IValidatorsExitBus} from "../interfaces/IValidatorExitBus.sol"; interface IWithdrawalVault { function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; @@ -14,7 +16,7 @@ interface IWithdrawalVault { function getWithdrawalRequestFee() external view returns (uint256); } -abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { +abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ReportExitLimitUtilsStorage for bytes32; using ReportExitLimitUtils for ExitRequestLimitData; @@ -56,13 +58,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { uint256 _exitRequestsLimitIncreasePerBlock ); - - struct DeliveryHistory { - /// @dev Key index in exit request array - uint256 lastDeliveredKeyIndex; - /// @dev Block timestamp - uint256 timestamp; - } struct RequestStatus { // Total items count in report (by default type(uint32).max, update on first report delivery) uint256 totalItemsCount; @@ -74,22 +69,14 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { DeliveryHistory[] deliverHistory; } - struct ExitRequestData { - bytes data; - uint256 dataFormat; - // TODO: maybe add requestCount for early exit and make it more safe - } - - struct ValidatorExitData { - uint256 stakingModuleId; - uint256 nodeOperatorId; - uint256 validatorIndex; - bytes validatorPubkey; - } - bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); bytes32 public constant DIRECT_EXIT_HASH_ROLE = keccak256("DIRECT_EXIT_HASH_ROLE"); bytes32 public constant EXIT_REPORT_LIMIT_ROLE = keccak256("EXIT_REPORT_LIMIT_ROLE"); + /// @notice An ACL role granting the permission to pause accepting validator exit requests + bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); + /// @notice An ACL role granting the permission to resume accepting validator exit requests + bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); + bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); /// Length in bytes of packed request @@ -126,12 +113,12 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { LOCATOR = ILidoLocator(lidoLocator); } - function submitReportHash(bytes32 exitReportHash) external onlyRole(SUBMIT_REPORT_HASH_ROLE) { + function submitReportHash(bytes32 exitReportHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0,0)); } - function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external{ + function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external whenResumed { bytes calldata data = request.data; _checkContractVersion(contractVersion); @@ -230,7 +217,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { /// @notice Triggers exits on the EL via the Withdrawal Vault contract after /// @dev This function verifies that the hash of the provided exit request data exists in storage // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. - function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable { + function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable whenResumed { uint256 prevBalance = address(this).balance - msg.value; bytes calldata data = request.data; RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(data, request.dataFormat))]; @@ -295,7 +282,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { assert(address(this).balance == prevBalance); } - function triggerExitsDirectly(ValidatorExitData calldata validator) external payable onlyRole(DIRECT_EXIT_HASH_ROLE) { + function triggerExitsDirectly(ValidatorExitData calldata validator) external payable whenResumed onlyRole(DIRECT_EXIT_HASH_ROLE) returns (uint256) { uint256 prevBalance = address(this).balance - msg.value; address locatorAddr = address(LOCATOR); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); @@ -334,6 +321,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { } assert(address(this).balance == prevBalance); + + return refund; } function setExitReportLimit(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { @@ -351,6 +340,83 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, Versioned { return request.deliverHistory; } + /// @notice Resume accepting validator exit requests + /// + /// @dev Reverts with `PausedExpected()` if contract is already resumed + /// @dev Reverts with `AccessControl:...` reason if sender has no `RESUME_ROLE` + /// + function resume() external whenPaused onlyRole(RESUME_ROLE) { + _resume(); + } + + /// @notice Pause accepting validator exit requests util in after duration + /// + /// @param _duration pause duration, seconds (use `PAUSE_INFINITELY` for unlimited) + /// @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 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); + } + + /// Internal functions + + + function _processExitRequestsList(bytes calldata data) internal { + uint256 offset; + uint256 offsetPastEnd; + assembly { + offset := data.offset + offsetPastEnd := add(offset, data.length) + } + + bytes calldata pubkey; + + assembly { + pubkey.length := 48 + } + + uint256 timestamp = _getTimestamp(); + + while (offset < offsetPastEnd) { + uint256 dataWithoutPubkey; + assembly { + // 16 most significant bytes are taken by module id, node op id, and val index + dataWithoutPubkey := shr(128, calldataload(offset)) + // the next 48 bytes are taken by the pubkey + pubkey.offset := add(offset, 16) + // totalling to 64 bytes + offset := add(offset, 64) + } + // dataWithoutPubkey + // MSB <---------------------------------------------------------------------- LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + uint64 valIndex = uint64(dataWithoutPubkey); + uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); + uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + if (moduleId == 0) { + revert InvalidRequestsData(); + } + + emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); + } + } + + function _getTimestamp() internal virtual view returns (uint256) { + return block.timestamp; // solhint-disable-line not-rely-on-time + } + function _storeExitRequestHash( bytes32 exitRequestHash, uint256 totalItemsCount, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index c99e599c2b..0d0b835c8f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -6,7 +6,6 @@ import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; import { Math256 } from "../../common/lib/Math256.sol"; -import { PausableUntil } from "../utils/PausableUntil.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { BaseOracle } from "./BaseOracle.sol"; @@ -18,7 +17,7 @@ interface IOracleReportSanityChecker { } -contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus { +contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { using UnstructuredStorage for bytes32; using SafeCast for uint256; using ReportExitLimitUtilsStorage for bytes32; @@ -47,20 +46,13 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus /// @notice An ACL role granting the permission to submit the data for a committee report. bytes32 public constant SUBMIT_DATA_ROLE = keccak256("SUBMIT_DATA_ROLE"); - /// @notice An ACL role granting the permission to pause accepting validator exit requests - bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); - - /// @notice An ACL role granting the permission to resume accepting validator exit requests - bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - /// @dev Storage slot: uint256 totalRequestsProcessed bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); - /// @dev Storage slot: mapping(uint256 => RequestedValidator) lastRequestedValidatorIndices - /// A mapping from the (moduleId, nodeOpId) packed key to the last requested validator index. - bytes32 internal constant LAST_REQUESTED_VALIDATOR_INDICES_POSITION = - keccak256("lido.ValidatorsExitBusOracle.lastRequestedValidatorIndices"); + /// @dev [DEPRECATED] Storage slot: mapping(uint256 => RequestedValidator) lastRequestedValidatorIndices + /// This mapping was previously used for storing last requested validator indexes per (moduleId, nodeOpId) key. + /// This code was removed from the contract, but slots can still contain logic. /// @dev Storage slot: DataProcessingState dataProcessingState bytes32 internal constant DATA_PROCESSING_STATE_POSITION = @@ -91,36 +83,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus function finalizeUpgrade_v2() external { _updateContractVersion(2); - // TODO: after deleted last exited keys clean here slots - } - /// @notice Resume accepting validator exit requests - /// - /// @dev Reverts with `PausedExpected()` if contract is already resumed - /// @dev Reverts with `AccessControl:...` reason if sender has no `RESUME_ROLE` - /// - function resume() external whenPaused onlyRole(RESUME_ROLE) { - _resume(); - } - - /// @notice Pause accepting validator exit requests util in after duration - /// - /// @param _duration pause duration, seconds (use `PAUSE_INFINITELY` for unlimited) - /// @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 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); } /// @@ -281,6 +244,7 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) .checkExitBusOracleReport(data.requestsCount); + // TODO: move this logic in separate method ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (exitRequestLimitData.isExitReportLimitSet()) { @@ -315,47 +279,6 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus ); } - function _processExitRequestsList(bytes calldata data) internal { - uint256 offset; - uint256 offsetPastEnd; - assembly { - offset := data.offset - offsetPastEnd := add(offset, data.length) - } - - bytes calldata pubkey; - - assembly { - pubkey.length := 48 - } - - uint256 timestamp = _getTime(); - - while (offset < offsetPastEnd) { - uint256 dataWithoutPubkey; - assembly { - // 16 most significant bytes are taken by module id, node op id, and val index - dataWithoutPubkey := shr(128, calldataload(offset)) - // the next 48 bytes are taken by the pubkey - pubkey.offset := add(offset, 16) - // totalling to 64 bytes - offset := add(offset, 64) - } - // dataWithoutPubkey - // MSB <---------------------------------------------------------------------- LSB - // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - uint64 valIndex = uint64(dataWithoutPubkey); - uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); - uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - if (moduleId == 0) { - revert InvalidRequestsData(); - } - - emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); - } - } - function _storeOracleExitRequestHash(bytes32 exitRequestHash, uint256 requestsCount, uint256 contractVersion) internal { if (requestsCount == 0) { return; diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 87e6cd5410..14d7677477 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -31,6 +31,11 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { return ITimeProvider(consensus).getTime(); } + function _getTimestamp() internal view override returns (uint256) { + address consensus = CONSENSUS_CONTRACT_POSITION.getStorageAddress(); + return ITimeProvider(consensus).getTime(); + } + function getDataProcessingState() external view returns (DataProcessingState memory) { return _storageDataProcessingState().value; } From 3ddd4b187d5b1d31d99dc55d33ae275935926c81 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 8 Apr 2025 19:47:00 +0400 Subject: [PATCH 061/405] fix: linter --- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 1 - ...it-bus-oracle.triggerExitsDirectly.test.ts | 32 +------------------ 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index edc64aec2b..e63d932528 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -23,7 +23,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { let admin: HardhatEthersSigner; let withdrawalVault: WithdrawalVault__MockForVebo; - let oracleVersion: bigint; let exitRequests: ExitRequest[]; let exitRequestHash: string; let exitRequest: ExitRequestData; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index c0f255f488..38a3d2fe5d 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -6,9 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; -import { de0x, numberToHex } from "lib"; - -import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; +import { deployVEBO, initVEBO } from "test/deploy"; const PUBKEYS = [ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -24,28 +22,12 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { let admin: HardhatEthersSigner; let withdrawalVault: WithdrawalVault__MockForVebo; - let oracleVersion: bigint; - let exitRequests: ExitRequest[]; - let exitRequestHash: string; - let exitRequest: ExitRequestData; let authorizedEntity: HardhatEthersSigner; let stranger: HardhatEthersSigner; let validatorExitData: ValidatorExitData; const LAST_PROCESSING_REF_SLOT = 1; - interface ExitRequest { - moduleId: number; - nodeOpId: number; - valIndex: number; - valPubkey: string; - } - - interface ExitRequestData { - dataFormat: number; - data: string; - } - interface ValidatorExitData { stakingModuleId: number; nodeOperatorId: number; @@ -53,16 +35,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { validatorPubkey: string; } - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; - }; - - const encodeExitRequestsDataList = (requests: ExitRequest[]) => { - return "0x" + requests.map(encodeExitRequestHex).join(""); - }; - const deploy = async () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; @@ -77,8 +49,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); - - oracleVersion = await oracle.getContractVersion(); }; before(async () => { From a9225317034937695f4887187838b6b1af6ff5aa Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 8 Apr 2025 20:04:14 +0400 Subject: [PATCH 062/405] fix: linter --- .../oracle/validator-exit-bus-oracle.emitExitEvents.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index e63d932528..0bc7d66049 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -67,8 +67,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); - - oracleVersion = await oracle.getContractVersion(); }; before(async () => { From 5c6c32d0b1162e163780fae94e084e0fc07d47e6 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 9 Apr 2025 14:35:59 +0200 Subject: [PATCH 063/405] refactor: move eip4788 helper to dedicated folder --- lib/{eip4788-beaconRoots.ts => eips/eip4788.ts} | 0 lib/eips/index.ts | 1 + lib/index.ts | 1 - 3 files changed, 1 insertion(+), 1 deletion(-) rename lib/{eip4788-beaconRoots.ts => eips/eip4788.ts} (100%) diff --git a/lib/eip4788-beaconRoots.ts b/lib/eips/eip4788.ts similarity index 100% rename from lib/eip4788-beaconRoots.ts rename to lib/eips/eip4788.ts diff --git a/lib/eips/index.ts b/lib/eips/index.ts index a4aec2e3aa..93662f8400 100644 --- a/lib/eips/index.ts +++ b/lib/eips/index.ts @@ -1,2 +1,3 @@ export * from "./eip712"; +export * from "./eip4788"; export * from "./eip7002"; diff --git a/lib/index.ts b/lib/index.ts index 9e70b43786..b66cdef958 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,7 +7,6 @@ export * from "./deploy"; export * from "./dsm"; export * from "./ec"; export * from "./eips"; -export * from "./eip4788-beaconRoots"; export * from "./ens"; export * from "./event"; export * from "./keccak"; From 12d2db8ec1763936e27d6137c357002a05827121 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 10 Apr 2025 13:52:04 +0400 Subject: [PATCH 064/405] fix: refactoring --- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 2 - contracts/0.8.9/oracle/ValidatorsExitBus.sol | 84 +++++-------------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 14 ++-- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 54 +++++------- ...r-exit-bus-oracle.submitReportData.test.ts | 19 +---- ...it-bus-oracle.triggerExitsDirectly.test.ts | 4 +- 6 files changed, 51 insertions(+), 126 deletions(-) diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index bb6d1211dd..ee0871f83c 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -128,8 +128,6 @@ library ReportExitLimitUtils { _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); - // TODO: check - // in the beginning should be 0 ? _data.prevExitRequestsBlockNumber = uint32(block.number); return _data; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 8df40f365c..b040f06423 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -31,7 +31,6 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab error UnsupportedRequestsDataFormat(uint256 format); error InvalidRequestsDataLength(); error InvalidRequestsData(); - error ActorOutOfReportLimit(); error RequestsAlreadyDelivered(); error ExitRequestsLimit(); @@ -137,81 +136,38 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab } // TODO: hash requestsCount too - if (requestStatus.totalItemsCount == type(uint256).max ) { requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; } uint256 deliveredItemsCount = requestStatus.deliveredItemsCount; - uint256 restToDeliver = requestStatus.totalItemsCount - deliveredItemsCount; + uint256 undeliveredItemsCount = requestStatus.totalItemsCount - deliveredItemsCount; - if (restToDeliver == 0 ) { - revert RequestsAlreadyDelivered(); + if (undeliveredItemsCount == 0 ) { + revert RequestsAlreadyDelivered(); } ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + uint256 toDeliver; - uint256 requestsToDeliver; - - // check if limit set if (exitRequestLimitData.isExitReportLimitSet()) { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (limit == 0) { revert ExitRequestsLimit(); } - requestsToDeliver = restToDeliver <= limit ? restToDeliver : limit; + toDeliver = undeliveredItemsCount > limit + ? limit + : undeliveredItemsCount; - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - requestsToDeliver)); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - toDeliver)); } else { - // TODO: do we need to store prev exit limit here - requestsToDeliver = restToDeliver; + toDeliver = undeliveredItemsCount; } + _processExitRequestsList(request.data, deliveredItemsCount, toDeliver); - uint256 offset; - uint256 offsetPastEnd; - - assembly { - offset := add(data.offset, mul(deliveredItemsCount, PACKED_REQUEST_LENGTH)) - offsetPastEnd := add(offset, mul(requestsToDeliver, PACKED_REQUEST_LENGTH)) - } - - bytes calldata pubkey; - - assembly { - pubkey.length := 48 - } - - uint256 timestamp = block.timestamp; - uint256 lastDeliveredKeyIndex = deliveredItemsCount; - - while (offset < offsetPastEnd) { - uint256 dataWithoutPubkey; - assembly { - // 16 most significant bytes are taken by module id, node op id, and val index - dataWithoutPubkey := shr(128, calldataload(offset)) - // the next 48 bytes are taken by the pubkey - pubkey.offset := add(offset, 16) - // totalling to 64 bytes - offset := add(offset, 64) - } - - uint64 valIndex = uint64(dataWithoutPubkey); - uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); - uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - if (moduleId == 0) { - // emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); - revert InvalidRequestsData(); - } - - requestStatus.deliverHistory.push(DeliveryHistory(lastDeliveredKeyIndex, timestamp)); - lastDeliveredKeyIndex = lastDeliveredKeyIndex + 1; - - emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); - } - - requestStatus.deliveredItemsCount = deliveredItemsCount + requestsToDeliver; + requestStatus.deliverHistory.push(DeliveryHistory(deliveredItemsCount + toDeliver - 1, _getTimestamp())); + requestStatus.deliveredItemsCount += toDeliver; } /// @notice Triggers exits on the EL via the Withdrawal Vault contract after @@ -287,7 +243,6 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab address locatorAddr = address(LOCATOR); address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - uint256 timestamp = block.timestamp; if (msg.value < withdrawalFee ) { revert InsufficientPayment(withdrawalFee, 1, msg.value); @@ -306,7 +261,7 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee}(validator.validatorPubkey); - emit ValidatorExitRequest(validator.stakingModuleId, validator.nodeOperatorId, validator.validatorIndex, validator.validatorPubkey, timestamp); + emit ValidatorExitRequest(validator.stakingModuleId, validator.nodeOperatorId, validator.validatorIndex, validator.validatorPubkey, _getTimestamp()); uint256 refund = msg.value - withdrawalFee; @@ -371,13 +326,13 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab /// Internal functions - - function _processExitRequestsList(bytes calldata data) internal { + function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { uint256 offset; uint256 offsetPastEnd; + assembly { - offset := data.offset - offsetPastEnd := add(offset, data.length) + offset := add(data.offset, mul(startIndex, PACKED_REQUEST_LENGTH)) + offsetPastEnd := add(offset, mul(count, PACKED_REQUEST_LENGTH)) } bytes calldata pubkey; @@ -417,6 +372,7 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab return block.timestamp; // solhint-disable-line not-rely-on-time } + // this method function _storeExitRequestHash( bytes32 exitRequestHash, uint256 totalItemsCount, @@ -427,9 +383,7 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); RequestStatus storage request = hashes[exitRequestHash]; - if (request.contractVersion != 0) { - return; - } + require(request.contractVersion == 0, "Hash already exists"); request.totalItemsCount = totalItemsCount; request.deliveredItemsCount = deliveredItemsCount; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 0d0b835c8f..4eabde0328 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.9; import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; -import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; import { Math256 } from "../../common/lib/Math256.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; @@ -27,7 +26,6 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { error AdminCannotBeZero(); error SenderNotAllowed(); error UnexpectedRequestsDataLength(); - error InvalidRequestsDataSortOrder(); error ArgumentOutOfBounds(); event WarnDataIncompleteProcessing( @@ -244,24 +242,26 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) .checkExitBusOracleReport(data.requestsCount); - // TODO: move this logic in separate method + // Check VEB common limit ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (exitRequestLimitData.isExitReportLimitSet()) { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); - if (limit < data.requestsCount) { + if (data.requestsCount > limit) { revert ExitRequestsLimit(); } - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - data.requestsCount)); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitRequestsLimit(limit - data.requestsCount) + ); } if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); } - _processExitRequestsList(data.data); + _processExitRequestsList(data.data, 0, data.requestsCount); _storageDataProcessingState().value = DataProcessingState({ refSlot: data.refSlot.toUint64(), @@ -283,7 +283,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(block.timestamp, requestsCount - 1)); + _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(requestsCount - 1, block.timestamp)); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index 0bc7d66049..2add9e2064 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -125,7 +125,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { it("Emit ValidatorExit event", async () => { const emitTx = await oracle.emitExitEvents(exitRequest, 2); - const block = await emitTx.getBlock(); + const timestamp = await oracle.getTime(); await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") @@ -134,7 +134,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[0].nodeOpId, exitRequests[0].valIndex, exitRequests[0].valPubkey, - block?.timestamp, + timestamp, ); await expect(emitTx) @@ -144,7 +144,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[1].nodeOpId, exitRequests[1].valIndex, exitRequests[1].valPubkey, - block?.timestamp, + timestamp, ); await expect(emitTx) @@ -154,7 +154,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[2].nodeOpId, exitRequests[2].valIndex, exitRequests[2].valPubkey, - block?.timestamp, + timestamp, ); await expect(emitTx) @@ -164,7 +164,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[3].nodeOpId, exitRequests[3].valIndex, exitRequests[3].valPubkey, - block?.timestamp, + timestamp, ); }); @@ -216,7 +216,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const receipt = await emitTx.wait(); expect(receipt?.logs.length).to.eq(2); - const block = await emitTx.getBlock(); + const timestamp = await oracle.getTime(); await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") @@ -225,7 +225,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[0].nodeOpId, exitRequests[0].valIndex, exitRequests[0].valPubkey, - block?.timestamp, + timestamp, ); await expect(emitTx) @@ -235,21 +235,18 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[1].nodeOpId, exitRequests[1].valIndex, exitRequests[1].valPubkey, - block?.timestamp, + timestamp, ); const history1 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history1.length).to.eq(2); - expect(history1[0].lastDeliveredKeyIndex).to.eq(0); - expect(history1[1].lastDeliveredKeyIndex).to.eq(1); + expect(history1.length).to.eq(1); + expect(history1[0].lastDeliveredKeyIndex).to.eq(1); const emitTx2 = await oracle.emitExitEvents(exitRequest, 2); const receipt2 = await emitTx2.wait(); expect(receipt2?.logs.length).to.eq(1); - const block2 = await emitTx2.getBlock(); - await expect(emitTx2) .to.emit(oracle, "ValidatorExitRequest") .withArgs( @@ -257,22 +254,18 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[2].nodeOpId, exitRequests[2].valIndex, exitRequests[2].valPubkey, - block2?.timestamp, + timestamp, ); const history2 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history2.length).to.eq(3); - expect(history2[0].lastDeliveredKeyIndex).to.eq(0); - expect(history2[1].lastDeliveredKeyIndex).to.eq(1); - expect(history2[2].lastDeliveredKeyIndex).to.eq(2); + expect(history2.length).to.eq(2); + expect(history2[1].lastDeliveredKeyIndex).to.eq(2); const emitTx3 = await oracle.emitExitEvents(exitRequest, 2); const receipt3 = await emitTx2.wait(); expect(receipt3?.logs.length).to.eq(1); - const block3 = await emitTx3.getBlock(); - await expect(emitTx3) .to.emit(oracle, "ValidatorExitRequest") .withArgs( @@ -280,23 +273,18 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[3].nodeOpId, exitRequests[3].valIndex, exitRequests[3].valPubkey, - block3?.timestamp, + timestamp, ); const history3 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history3.length).to.eq(4); - expect(history3[0].lastDeliveredKeyIndex).to.eq(0); - expect(history3[1].lastDeliveredKeyIndex).to.eq(1); - expect(history3[2].lastDeliveredKeyIndex).to.eq(2); - expect(history3[3].lastDeliveredKeyIndex).to.eq(3); + expect(history3.length).to.eq(3); + expect(history3[2].lastDeliveredKeyIndex).to.eq(3); const emitTx4 = await oracle.emitExitEvents(exitRequest, 2); const receipt4 = await emitTx2.wait(); expect(receipt4?.logs.length).to.eq(1); - const block4 = await emitTx4.getBlock(); - await expect(emitTx4) .to.emit(oracle, "ValidatorExitRequest") .withArgs( @@ -304,16 +292,12 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequests[4].nodeOpId, exitRequests[4].valIndex, exitRequests[4].valPubkey, - block4?.timestamp, + timestamp, ); const history4 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history4.length).to.eq(5); - expect(history4[0].lastDeliveredKeyIndex).to.eq(0); - expect(history4[1].lastDeliveredKeyIndex).to.eq(1); - expect(history4[2].lastDeliveredKeyIndex).to.eq(2); - expect(history4[3].lastDeliveredKeyIndex).to.eq(3); - expect(history4[4].lastDeliveredKeyIndex).to.eq(4); + expect(history4.length).to.eq(4); + expect(history4[3].lastDeliveredKeyIndex).to.eq(4); await expect(oracle.emitExitEvents(exitRequest, 2)).to.be.revertedWithCustomError( oracle, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index aad853c120..469e2ff3df 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -645,27 +645,16 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }; const emitTx = await oracle.emitExitEvents(exitRequest, 2); - const block = await emitTx.getBlock(); + + const timestamp = await oracle.getTime(); await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - requests[0].moduleId, - requests[0].nodeOpId, - requests[0].valIndex, - requests[0].valPubkey, - block?.timestamp, - ); + .withArgs(requests[0].moduleId, requests[0].nodeOpId, requests[0].valIndex, requests[0].valPubkey, timestamp); await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - requests[1].moduleId, - requests[1].nodeOpId, - requests[1].valIndex, - requests[1].valPubkey, - block?.timestamp, - ); + .withArgs(requests[1].moduleId, requests[1].nodeOpId, requests[1].valIndex, requests[1].valPubkey, timestamp); }); it("emits ValidatorExitRequest events", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index 38a3d2fe5d..2956525b23 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -90,7 +90,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(validatorExitData, { value: 2, }); - const block = await tx.getBlock(); + const timestamp = await oracle.getTime(); await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(PUBKEYS[0]); await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); @@ -101,7 +101,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { validatorExitData.nodeOperatorId, validatorExitData.validatorIndex, validatorExitData.validatorPubkey, - block?.timestamp, + timestamp, ); }); }); From 8edf693563c1c91f1ec8d5c6c3257fd7a6b83350 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 10 Apr 2025 13:56:45 +0400 Subject: [PATCH 065/405] fix: refactoring --- contracts/0.8.9/lib/ExitRequestUtils.sol | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 contracts/0.8.9/lib/ExitRequestUtils.sol diff --git a/contracts/0.8.9/lib/ExitRequestUtils.sol b/contracts/0.8.9/lib/ExitRequestUtils.sol deleted file mode 100644 index 9fd5c284e1..0000000000 --- a/contracts/0.8.9/lib/ExitRequestUtils.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.9; - - -library ExitRequestUtils { - /// Method - -} \ No newline at end of file From 019672d63434cab982973e3adfd575e66b1eb098 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 10 Apr 2025 14:31:51 +0400 Subject: [PATCH 066/405] fix: refactoring --- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- .../oracle/validator-exit-bus-oracle.happyPath.test.ts | 8 -------- .../validator-exit-bus-oracle.submitReportData.test.ts | 4 ---- ...alidator-exit-bus-oracle.triggerExitHashVerify.test.ts | 8 -------- 4 files changed, 1 insertion(+), 21 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 4eabde0328..61728a527f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -283,7 +283,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(requestsCount - 1, block.timestamp)); + _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(requestsCount - 1, _getTimestamp())); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 122c47b43f..5da082d184 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -241,14 +241,6 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { expect(procState.requestsSubmitted).to.equal(exitRequests.length); }); - // it("last requested validator indices are updated", async () => { - // const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n]); - // const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n]); - - // expect([...indices1]).to.have.ordered.members([2n, -1n, -1n]); - // expect([...indices2]).to.have.ordered.members([1n, -1n, -1n]); - // }); - it("no data can be submitted for the same reference slot again", async () => { await expect(oracle.connect(member2).submitReportData(reportFields, oracleVersion)).to.be.revertedWithCustomError( oracle, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 469e2ff3df..30984dffda 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -103,10 +103,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { return { reportData, reportHash }; }; - // async function getLastRequestedValidatorIndex(moduleId: number, nodeOpId: number) { - // return (await oracle.getLastRequestedValidatorIndices(moduleId, [nodeOpId]))[0]; - // } - const deploy = async () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index cd6218cde0..f4af02a33c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -210,14 +210,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect(procState.requestsSubmitted).to.equal(exitRequests.length); }); - // it("last requested validator indices are updated", async () => { - // const indices1 = await oracle.getLastRequestedValidatorIndices(1n, [0n, 1n, 2n, 3n]); - // const indices2 = await oracle.getLastRequestedValidatorIndices(2n, [0n, 1n, 2n, 3n]); - - // expect([...indices1]).to.have.ordered.members([2n, -1n, -1n, -1n]); - // expect([...indices2]).to.have.ordered.members([3n, -1n, -1n, -1n]); - // }); - it("someone submitted exit report data and triggered exit", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, From 78b0a2863372e221543d3f8a428bc515326bc566 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 10 Apr 2025 18:01:27 +0200 Subject: [PATCH 067/405] refactor: remove automatic fee calculation and redundant fee validation --- .../common/lib/TriggerableWithdrawals.sol | 27 ----- .../triggerableWithdrawals.test.ts | 107 ++---------------- 2 files changed, 12 insertions(+), 122 deletions(-) diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol index 0547065e8e..b20b7a2bf5 100644 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ b/contracts/common/lib/TriggerableWithdrawals.sol @@ -20,9 +20,6 @@ library TriggerableWithdrawals { error WithdrawalFeeInvalidData(); error WithdrawalRequestAdditionFailed(bytes callData); - error InsufficientWithdrawalFee(uint256 feePerRequest, uint256 minFeePerRequest); - error TotalWithdrawalFeeExceededBalance(uint256 balance, uint256 totalWithdrawalFee); - error NoWithdrawalRequests(); error MalformedPubkeysArray(); error PartialWithdrawalRequired(uint256 index); @@ -37,7 +34,6 @@ library TriggerableWithdrawals { * * @param feePerRequest The withdrawal fee for each withdrawal request. * - Must be greater than or equal to the current minimal withdrawal fee. - * - If set to zero, the current minimal withdrawal fee will be used automatically. * * @notice Reverts if: * - Validation of the public keys fails. @@ -46,7 +42,6 @@ library TriggerableWithdrawals { */ function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) internal { uint256 keysCount = _validateAndCountPubkeys(pubkeys); - feePerRequest = _validateAndAdjustFee(feePerRequest, keysCount); bytes memory callData = new bytes(56); @@ -75,7 +70,6 @@ library TriggerableWithdrawals { * * @param feePerRequest The withdrawal fee for each withdrawal request. * - Must be greater than or equal to the current minimal withdrawal fee. - * - If set to zero, the current minimal withdrawal fee will be used automatically. * * @notice Reverts if: * - Validation of the public keys fails. @@ -116,7 +110,6 @@ library TriggerableWithdrawals { * * @param feePerRequest The withdrawal fee for each withdrawal request. * - Must be greater than or equal to the current minimal withdrawal fee. - * - If set to zero, the current minimal withdrawal fee will be used automatically. * * @notice Reverts if: * - Validation of the public keys fails. @@ -131,8 +124,6 @@ library TriggerableWithdrawals { revert MismatchedArrayLengths(keysCount, amounts.length); } - feePerRequest = _validateAndAdjustFee(feePerRequest, keysCount); - bytes memory callData = new bytes(56); for (uint256 i = 0; i < keysCount; i++) { _copyPubkeyToMemory(pubkeys, callData, i); @@ -188,22 +179,4 @@ library TriggerableWithdrawals { return keysCount; } - - function _validateAndAdjustFee(uint256 feePerRequest, uint256 keysCount) private view returns (uint256) { - uint256 minFeePerRequest = getWithdrawalRequestFee(); - - if (feePerRequest == 0) { - feePerRequest = minFeePerRequest; - } - - if (feePerRequest < minFeePerRequest) { - revert InsufficientWithdrawalFee(feePerRequest, minFeePerRequest); - } - - if (address(this).balance < feePerRequest * keysCount) { - revert TotalWithdrawalFeeExceededBalance(address(this).balance, feePerRequest * keysCount); - } - - return feePerRequest; - } } diff --git a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts index ff0f267f49..86b51a8ba7 100644 --- a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts @@ -139,17 +139,17 @@ describe("TriggerableWithdrawals.sol", () => { // 2. Should revert if fee is less than required const insufficientFee = 2n; - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, insufficientFee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientWithdrawalFee") - .withArgs(2n, 3n); + await expect( + triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, insufficientFee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, insufficientFee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientWithdrawalFee") - .withArgs(2n, 3n); + await expect( + triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, insufficientFee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, insufficientFee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "InsufficientWithdrawalFee") - .withArgs(2n, 3n); + await expect( + triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, insufficientFee), + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); }); it("Should revert if pubkey is not 48 bytes", async function () { @@ -231,7 +231,6 @@ describe("TriggerableWithdrawals.sol", () => { const keysCount = 2; const fee = 10n; const balance = 19n; - const expectedMinimalBalance = 20n; const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(keysCount); @@ -239,95 +238,17 @@ describe("TriggerableWithdrawals.sol", () => { await withdrawalsPredeployed.mock__setFee(fee); await setBalance(await triggerableWithdrawals.getAddress(), balance); - await expect(triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "TotalWithdrawalFeeExceededBalance") - .withArgs(balance, expectedMinimalBalance); - - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "TotalWithdrawalFeeExceededBalance") - .withArgs(balance, expectedMinimalBalance); - - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "TotalWithdrawalFeeExceededBalance") - .withArgs(balance, expectedMinimalBalance); - }); - - it("Should revert when fee read fails", async function () { - await withdrawalsPredeployed.mock__setFailOnGetFee(true); - - const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(2); - const fee = 10n; - await expect( triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); await expect( triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); await expect( triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeReadFailed"); - }); - - ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { - it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); - - const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(2); - const fee = 10n; - - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeInvalidData"); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeInvalidData"); - - await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalFeeInvalidData"); - }); - }); - - it("Should accept withdrawal requests with minimal possible fee when fee not provided", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - const fee_not_provided = 0n; - await withdrawalsPredeployed.mock__setFee(fee); - - await testEIP7002Mock( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee_not_provided), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => - triggerableWithdrawals.addPartialWithdrawalRequests( - pubkeysHexString, - partialWithdrawalAmounts, - fee_not_provided, - ), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee_not_provided), - pubkeys, - mixedWithdrawalAmounts, - fee, - ); + ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); }); it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { @@ -576,15 +497,11 @@ describe("TriggerableWithdrawals.sol", () => { } const testCasesForWithdrawalRequests = [ - { requestCount: 1, fee: 0n }, { requestCount: 1, fee: 100n }, { requestCount: 1, fee: 100_000_000_000n }, - { requestCount: 3, fee: 0n }, { requestCount: 3, fee: 1n }, { requestCount: 7, fee: 3n }, - { requestCount: 10, fee: 0n }, { requestCount: 10, fee: 100_000_000_000n }, - { requestCount: 100, fee: 0n }, ]; testCasesForWithdrawalRequests.forEach(({ requestCount, fee }) => { From dff57bb80bd127984d91e2083fb9e7fc701b5ea3 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Apr 2025 19:24:37 +0200 Subject: [PATCH 068/405] wip: initial TW processing in NOR --- .../0.4.24/nos/NodeOperatorExitManager.sol | 449 ++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 contracts/0.4.24/nos/NodeOperatorExitManager.sol diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol new file mode 100644 index 0000000000..9faaf9410d --- /dev/null +++ b/contracts/0.4.24/nos/NodeOperatorExitManager.sol @@ -0,0 +1,449 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.4.24; + +import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; +import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; + +interface IStETH { + function getSharesByPooledEth(uint256 _ethAmount) external view returns (uint256); + function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); +} + +/** + * @title NodeOperatorExitManager + * @notice Base contract for handling triggerable withdrawals and penalties for validators + */ +contract NodeOperatorExitManager { + using SafeMath for uint256; + using UnstructuredStorage for bytes32; + + // Events + event ValidatorExitStatusUpdated( + uint256 indexed nodeOperatorId, + bytes publicKey, + uint256 eligibleToExitInSec, + uint256 proofSlotTimestamp + ); + event TriggerableExitFeeSet( + uint256 indexed nodeOperatorId, + bytes publicKey, + uint256 withdrawalRequestPaidFee, + uint256 exitType + ); + event PenaltyApplied( + uint256 indexed nodeOperatorId, + bytes publicKey, + uint256 penaltyAmount, + string penaltyType + ); + event ExitDeadlineThresholdChanged(uint256 threshold); + + // Storage positions + bytes32 internal constant EXIT_DEADLINE_THRESHOLD_POSITION = keccak256("lido.NodeOperatorExitManager.exitDeadlineThreshold"); + + // Struct to store exit-related data for each validator + struct ValidatorExitRecord { + uint256 eligibleToExitInSec; + uint256 pinalizedFee; + uint256 triggerableExitFee; + uint256 lastUpdatedTimestamp; + bool isPenalized; + bool isExited; + } + + // Mapping from operator ID to mapping from validator public key hash to exit record + mapping(uint256 => mapping(bytes32 => ValidatorExitRecord)) internal validatorExitRecords; + + // Mapping to store all validator key hashes for each operator + mapping(uint256 => bytes32[]) internal operatorWatchableValidatorKeys; + + /** + * @notice Initialize the contract with a default exit deadline threshold + * @param _exitDeadlineThreshold The number of seconds after which a validator is considered late + */ + function _initializeNodeOperatorExitManager(uint256 _exitDeadlineThreshold) internal { + EXIT_DEADLINE_THRESHOLD_POSITION.setStorageUint256(_exitDeadlineThreshold); + emit ExitDeadlineThresholdChanged(_exitDeadlineThreshold); + } + + /** + * @notice Set the exit deadline threshold + * @param _threshold New threshold in seconds + */ + function _setExitDeadlineThreshold(uint256 _threshold) internal { + EXIT_DEADLINE_THRESHOLD_POSITION.setStorageUint256(_threshold); + emit ExitDeadlineThresholdChanged(_threshold); + } + + /** + * @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window + * @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered + * @param _proofSlotTimestamp The timestamp when the validator was last known to be in an active ongoing state + * @param _publicKey The public key of the validator being reported + * @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit + */ + function _handleActiveValidatorsExitingStatus( + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes _publicKey, + uint256 _eligibleToExitInSec + ) internal { + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + + // Hash the public key to use as a mapping key + bytes32 publicKeyHash = keccak256(_publicKey); + + // Track this validator key if it's new + _ensureValidatorKeyTracked(_nodeOperatorId, publicKeyHash, _publicKey); + + // Get or initialize the validator exit record + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; + + // Update the record with the new data + record.eligibleToExitInSec = _eligibleToExitInSec; + record.lastUpdatedTimestamp = _proofSlotTimestamp; + + // Calculate penalty if the validator has exceeded the exit deadline + if (_eligibleToExitInSec > _exitDeadlineThreshold(_nodeOperatorId)) { + // Calculate penalty based on the excess time + uint256 excessTime = _eligibleToExitInSec.sub(_exitDeadlineThreshold(_nodeOperatorId)); + uint256 penaltyAmount = _calculatePenalty(excessTime); + + // Add to the penalized fee + record.pinalizedFee = record.pinalizedFee.add(penaltyAmount); + + emit PenaltyApplied(_nodeOperatorId, _publicKey, penaltyAmount, "EXCESS_EXIT_TIME"); + } + + emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); + } + + /** + * @notice Handles the triggerable exit event for a validator + * @param _nodeOperatorId The ID of the node operator + * @param _publicKey The public key of the validator being reported + * @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the EL + * @param _exitType The type of exit being performed + */ + function _onTriggerableExit( + uint256 _nodeOperatorId, + bytes _publicKey, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType + ) internal { + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + + // Hash the public key to use as a mapping key + bytes32 publicKeyHash = keccak256(_publicKey); + + // Track this validator key if it's new + _ensureValidatorKeyTracked(_nodeOperatorId, publicKeyHash, _publicKey); + + // Get or initialize the validator exit record + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; + + // Set the triggerable exit fee + record.triggerableExitFee = _withdrawalRequestPaidFee; + + emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); + } + + /** + * @notice Ensures a validator key is tracked in the operatorWatchableValidatorKeys array + * @param _nodeOperatorId The node operator ID + * @param _publicKeyHash Hash of the validator public key + * @param _publicKey Original public key (for events) + */ + function _ensureValidatorKeyTracked( + uint256 _nodeOperatorId, + bytes32 _publicKeyHash, + bytes _publicKey + ) internal { + // Only add to tracking if this is a new record + if (validatorExitRecords[_nodeOperatorId][_publicKeyHash].lastUpdatedTimestamp == 0) { + operatorWatchableValidatorKeys[_nodeOperatorId].push(_publicKeyHash); + } + } + + /** + * @notice Returns the number of seconds after which a validator is considered late + * @param _nodeOperatorId The ID of the node operator + * @return The exit deadline threshold in seconds + */ + function _exitDeadlineThreshold(uint256 _nodeOperatorId) public view returns (uint256) { + // Currently using a global threshold, but could be extended to support per-operator thresholds + return EXIT_DEADLINE_THRESHOLD_POSITION.getStorageUint256(); + } + + /** + * @notice Determines whether a validator's exit status should be updated + * @param _nodeOperatorId The ID of the node operator + * @param _proofSlotTimestamp The timestamp when the validator was last known to be active + * @param _publicKey The public key of the validator + * @param _eligibleToExitInSec The number of seconds the validator was eligible to exit + * @return bool Returns true if the contract should receive the updated status + */ + function _shouldValidatorBePenalized( + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes _publicKey, + uint256 _eligibleToExitInSec + ) internal view returns (bool) { + bytes32 publicKeyHash = keccak256(_publicKey); + + // Check if record exists, otherwise it's a new record and should be updated + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; + bool recordExists = record.lastUpdatedTimestamp > 0; + + if (!recordExists) { + return true; + } + + // If the validator has exceeded the exit deadline, it should be penalized + if (_eligibleToExitInSec > _exitDeadlineThreshold(_nodeOperatorId)) { + return true; + } + + // If the validator's exit status has changed, it should be updated + if (_eligibleToExitInSec != record.eligibleToExitInSec) { + return true; + } + + // If proof timestamp is newer than last updated, it should be updated + if (_proofSlotTimestamp > record.lastUpdatedTimestamp) { + return true; + } + + return false; + } + + /** + * @notice Get the exit record for a validator using its public key hash + * @param _nodeOperatorId The ID of the node operator + * @param _publicKeyHash Hash of the validator's public key + * @return Record data: eligibleToExitInSec, pinalizedFee, triggerableExitFee, lastUpdatedTimestamp + */ + function _getValidatorExitRecordByHash( + uint256 _nodeOperatorId, + bytes32 _publicKeyHash + ) internal view returns (uint256, uint256, uint256, uint256) { + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; + require(record.lastUpdatedTimestamp > 0, "VALIDATOR_RECORD_NOT_FOUND"); + + return ( + record.eligibleToExitInSec, + record.pinalizedFee, + record.triggerableExitFee, + record.lastUpdatedTimestamp + ); + } + + /** + * @notice Get the exit record for a validator using its public key + * @param _nodeOperatorId The ID of the node operator + * @param _publicKey The public key of the validator + * @return Record data: eligibleToExitInSec, pinalizedFee, triggerableExitFee, lastUpdatedTimestamp + */ + function _getValidatorExitRecord( + uint256 _nodeOperatorId, + bytes _publicKey + ) internal view returns (uint256, uint256, uint256, uint256) { + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + bytes32 publicKeyHash = keccak256(_publicKey); + + return _getValidatorExitRecordByHash(_nodeOperatorId, publicKeyHash); + } + + /** + * @notice Helper function to calculate penalty based on excess time + * @param _excessTime Time in seconds beyond the exit deadline + * @return Penalty amount in ETH + */ + function _calculatePenalty(uint256 _excessTime) internal pure returns (uint256) { + // TODO: get the penalty rate from analytics team + return _excessTime.mul(1 ether).div(86400); + } + + /** + * @notice Apply penalties to an operator's rewards using public key hash + * @param _nodeOperatorId The ID of the node operator + * @param _publicKeyHash Hash of the validator's public key + * @param _stETH Interface to the stETH token + * @param _shares Amount of shares being distributed to the operator + * @return Adjusted shares after penalties + */ + function _applyPenaltiesByHash( + uint256 _nodeOperatorId, + bytes32 _publicKeyHash, + IStETH _stETH, + uint256 _shares + ) internal returns (uint256) { + // Check if record exists before attempting to apply penalties + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; + if (record.lastUpdatedTimestamp == 0) { + return _shares; + } + + // If there are no penalties, return the original shares + if (record.pinalizedFee == 0 && record.triggerableExitFee == 0) { + return _shares; + } + + uint256 remainingShares = _shares; + + // Apply penalties for exceeding exit deadline + if (record.pinalizedFee > 0) { + uint256 pinalizedShares = _stETH.getSharesByPooledEth(record.pinalizedFee); + + if (pinalizedShares >= remainingShares) { + // Not enough shares to cover the full penalty + record.pinalizedFee = record.pinalizedFee.sub( + _stETH.getPooledEthByShares(remainingShares) + ); + remainingShares = 0; + } else { + // Enough shares to cover the penalty + remainingShares = remainingShares.sub(pinalizedShares); + record.pinalizedFee = 0; + record.isPenalized = true; + } + } + + + // Apply penalties for triggerable exit fees + if (remainingShares > 0 && record.triggerableExitFee > 0) { + uint256 triggerableShares = _stETH.getSharesByPooledEth(record.triggerableExitFee); + + if (triggerableShares >= remainingShares) { + // Not enough shares to cover the full fee + record.triggerableExitFee = record.triggerableExitFee.sub( + _stETH.getPooledEthByShares(remainingShares) + ); + remainingShares = 0; + } else { + // Enough shares to cover the fee + remainingShares = remainingShares.sub(triggerableShares); + record.triggerableExitFee = 0; + record.isExited = true; + } + } + + return remainingShares; + } + + /** + * @notice Apply penalties to an operator's rewards using public key + * @param _nodeOperatorId The ID of the node operator + * @param _publicKey The public key of the validator + * @param _stETH Interface to the stETH token + * @param _shares Amount of shares being distributed to the operator + * @return Adjusted shares after penalties + */ + function _applyPenalties( + uint256 _nodeOperatorId, + bytes _publicKey, + IStETH _stETH, + uint256 _shares + ) internal returns (uint256) { + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + bytes32 publicKeyHash = keccak256(_publicKey); + + return _applyPenaltiesByHash(_nodeOperatorId, publicKeyHash, _stETH, _shares); + } + + /** + * @notice Apply penalties to all validators of an operator + * @param _nodeOperatorId The ID of the node operator + * @param _stETH Interface to the stETH token + * @param _shares Amount of shares being distributed to the operator + * @return Adjusted shares after penalties + */ + function _applyAllPenalties( + uint256 _nodeOperatorId, + IStETH _stETH, + uint256 _shares + ) internal returns (uint256) { + uint256 remainingShares = _shares; + bytes32[] storage validatorKeys = operatorValidatorKeys[_nodeOperatorId]; + + // Iterate through all validator keys for this operator + for (uint256 i = 0; i < validatorKeys.length; i++) { + remainingShares = _applyPenaltiesByHash( + _nodeOperatorId, + validatorKeys[i], + _stETH, + remainingShares + ); + + if (remainingShares == 0) break; + } + // Clean up completed validators from the watchable keys array + // TODO: combine with _applyPenaltiesByHash to avoid double iteration + _cleanupCompletedValidators(_nodeOperatorId); + + return remainingShares; + } + + /** + * @notice Clean up validators that have completed processing from the watchable keys array + * @param _nodeOperatorId The ID of the node operator + */ + function _cleanupCompletedValidators(uint256 _nodeOperatorId) internal { + bytes32[] storage watchableKeys = operatorWatchableValidatorKeys[_nodeOperatorId]; + uint256 i = 0; + + while (i < watchableKeys.length) { + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][watchableKeys[i]]; + + if (record.isPenalized && record.isExited) { + // If both conditions are met, remove from watchable keys by swapping with the last element + watchableKeys[i] = watchableKeys[watchableKeys.length - 1]; + watchableKeys.length--; + // Don't increment i as we need to process the swapped element + } else { + // Move to next key + i++; + } + } + } + + /** + * @notice Check if a validator exit record exists using key hash + * @param _nodeOperatorId The ID of the node operator + * @param _publicKeyHash Hash of the validator public key + * @return True if record exists + */ + function _validatorExitRecordExistsByHash( + uint256 _nodeOperatorId, + bytes32 _publicKeyHash + ) internal view returns (bool) { + return validatorExitRecords[_nodeOperatorId][_publicKeyHash].lastUpdatedTimestamp > 0; + } + + /** + * @notice Check if a validator exit record exists + * @param _nodeOperatorId The ID of the node operator + * @param _publicKey The public key of the validator + * @return True if record exists + */ + function _validatorExitRecordExists( + uint256 _nodeOperatorId, + bytes _publicKey + ) internal view returns (bool) { + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + bytes32 publicKeyHash = keccak256(_publicKey); + + return _validatorExitRecordExistsByHash(_nodeOperatorId, publicKeyHash); + } + + /** + * @notice Get the count of validators with exit records for a node operator + * @param _nodeOperatorId The ID of the node operator + * @return Count of validators with exit records + */ + function _getValidatorExitRecordCount(uint256 _nodeOperatorId) internal view returns (uint256) { + return operatorWatchableValidatorKeys[_nodeOperatorId].length; + } +} From 26340ab1e9c34c1e032ad29603e64e99e6067197 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Apr 2025 19:44:18 +0200 Subject: [PATCH 069/405] wip: enhance validator exit management with improved exit time validation and cleanup of unused functions --- .../0.4.24/nos/NodeOperatorExitManager.sol | 92 +++---------------- 1 file changed, 12 insertions(+), 80 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol index 9faaf9410d..390c599aff 100644 --- a/contracts/0.4.24/nos/NodeOperatorExitManager.sol +++ b/contracts/0.4.24/nos/NodeOperatorExitManager.sol @@ -90,13 +90,16 @@ contract NodeOperatorExitManager { bytes _publicKey, uint256 _eligibleToExitInSec ) internal { + require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "INVALID_EXIT_TIME"); require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); // Hash the public key to use as a mapping key bytes32 publicKeyHash = keccak256(_publicKey); // Track this validator key if it's new - _ensureValidatorKeyTracked(_nodeOperatorId, publicKeyHash, _publicKey); + if (validatorExitRecords[_nodeOperatorId][_publicKeyHash].lastUpdatedTimestamp == 0) { + operatorWatchableValidatorKeys[_nodeOperatorId].push(_publicKeyHash); + } // Get or initialize the validator exit record ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; @@ -106,9 +109,9 @@ contract NodeOperatorExitManager { record.lastUpdatedTimestamp = _proofSlotTimestamp; // Calculate penalty if the validator has exceeded the exit deadline - if (_eligibleToExitInSec > _exitDeadlineThreshold(_nodeOperatorId)) { + if (record.pinalizedFee != 0) { // Calculate penalty based on the excess time - uint256 excessTime = _eligibleToExitInSec.sub(_exitDeadlineThreshold(_nodeOperatorId)); + uint256 excessTime = _eligibleToExitInSec.sub(_exitDeadlineThreshold()); uint256 penaltyAmount = _calculatePenalty(excessTime); // Add to the penalized fee @@ -138,11 +141,8 @@ contract NodeOperatorExitManager { // Hash the public key to use as a mapping key bytes32 publicKeyHash = keccak256(_publicKey); - // Track this validator key if it's new - _ensureValidatorKeyTracked(_nodeOperatorId, publicKeyHash, _publicKey); - // Get or initialize the validator exit record - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; + ValidatorExitRecord storage record = _getValidatorExitRecordByHash(_nodeOperatorId, publicKeyHash); // Set the triggerable exit fee record.triggerableExitFee = _withdrawalRequestPaidFee; @@ -150,30 +150,11 @@ contract NodeOperatorExitManager { emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } - /** - * @notice Ensures a validator key is tracked in the operatorWatchableValidatorKeys array - * @param _nodeOperatorId The node operator ID - * @param _publicKeyHash Hash of the validator public key - * @param _publicKey Original public key (for events) - */ - function _ensureValidatorKeyTracked( - uint256 _nodeOperatorId, - bytes32 _publicKeyHash, - bytes _publicKey - ) internal { - // Only add to tracking if this is a new record - if (validatorExitRecords[_nodeOperatorId][_publicKeyHash].lastUpdatedTimestamp == 0) { - operatorWatchableValidatorKeys[_nodeOperatorId].push(_publicKeyHash); - } - } - /** * @notice Returns the number of seconds after which a validator is considered late - * @param _nodeOperatorId The ID of the node operator * @return The exit deadline threshold in seconds */ - function _exitDeadlineThreshold(uint256 _nodeOperatorId) public view returns (uint256) { - // Currently using a global threshold, but could be extended to support per-operator thresholds + function _exitDeadlineThreshold() public view returns (uint256) { return EXIT_DEADLINE_THRESHOLD_POSITION.getStorageUint256(); } @@ -186,33 +167,13 @@ contract NodeOperatorExitManager { * @return bool Returns true if the contract should receive the updated status */ function _shouldValidatorBePenalized( - uint256 _nodeOperatorId, - uint256 _proofSlotTimestamp, - bytes _publicKey, + uint256, // _nodeOperatorId, + uint256, // _proofSlotTimestamp, + bytes, // _publicKey, uint256 _eligibleToExitInSec ) internal view returns (bool) { - bytes32 publicKeyHash = keccak256(_publicKey); - - // Check if record exists, otherwise it's a new record and should be updated - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; - bool recordExists = record.lastUpdatedTimestamp > 0; - - if (!recordExists) { - return true; - } - // If the validator has exceeded the exit deadline, it should be penalized - if (_eligibleToExitInSec > _exitDeadlineThreshold(_nodeOperatorId)) { - return true; - } - - // If the validator's exit status has changed, it should be updated - if (_eligibleToExitInSec != record.eligibleToExitInSec) { - return true; - } - - // If proof timestamp is newer than last updated, it should be updated - if (_proofSlotTimestamp > record.lastUpdatedTimestamp) { + if (_eligibleToExitInSec >= _exitDeadlineThreshold()) { return true; } @@ -409,35 +370,6 @@ contract NodeOperatorExitManager { } } - /** - * @notice Check if a validator exit record exists using key hash - * @param _nodeOperatorId The ID of the node operator - * @param _publicKeyHash Hash of the validator public key - * @return True if record exists - */ - function _validatorExitRecordExistsByHash( - uint256 _nodeOperatorId, - bytes32 _publicKeyHash - ) internal view returns (bool) { - return validatorExitRecords[_nodeOperatorId][_publicKeyHash].lastUpdatedTimestamp > 0; - } - - /** - * @notice Check if a validator exit record exists - * @param _nodeOperatorId The ID of the node operator - * @param _publicKey The public key of the validator - * @return True if record exists - */ - function _validatorExitRecordExists( - uint256 _nodeOperatorId, - bytes _publicKey - ) internal view returns (bool) { - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - bytes32 publicKeyHash = keccak256(_publicKey); - - return _validatorExitRecordExistsByHash(_nodeOperatorId, publicKeyHash); - } - /** * @notice Get the count of validators with exit records for a node operator * @param _nodeOperatorId The ID of the node operator From 4baba69009000072c54dda17f66ff46e7f628ca3 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Apr 2025 20:10:17 +0200 Subject: [PATCH 070/405] wip: implement initial structure for validator exit management --- .../0.4.24/nos/NodeOperatorExitManager.sol | 171 +++++------------- 1 file changed, 44 insertions(+), 127 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol index 390c599aff..8d9bf66f6c 100644 --- a/contracts/0.4.24/nos/NodeOperatorExitManager.sol +++ b/contracts/0.4.24/nos/NodeOperatorExitManager.sol @@ -6,11 +6,6 @@ pragma solidity 0.4.24; import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; -interface IStETH { - function getSharesByPooledEth(uint256 _ethAmount) external view returns (uint256); - function getPooledEthByShares(uint256 _sharesAmount) external view returns (uint256); -} - /** * @title NodeOperatorExitManager * @notice Base contract for handling triggerable withdrawals and penalties for validators @@ -46,7 +41,7 @@ contract NodeOperatorExitManager { // Struct to store exit-related data for each validator struct ValidatorExitRecord { uint256 eligibleToExitInSec; - uint256 pinalizedFee; + uint256 penalizedFee; uint256 triggerableExitFee; uint256 lastUpdatedTimestamp; bool isPenalized; @@ -61,11 +56,11 @@ contract NodeOperatorExitManager { /** * @notice Initialize the contract with a default exit deadline threshold - * @param _exitDeadlineThreshold The number of seconds after which a validator is considered late + * @param _getExitDeadlineThreshold The number of seconds after which a validator is considered late */ - function _initializeNodeOperatorExitManager(uint256 _exitDeadlineThreshold) internal { - EXIT_DEADLINE_THRESHOLD_POSITION.setStorageUint256(_exitDeadlineThreshold); - emit ExitDeadlineThresholdChanged(_exitDeadlineThreshold); + function _initializeNodeOperatorExitManager(uint256 _getExitDeadlineThreshold) internal { + EXIT_DEADLINE_THRESHOLD_POSITION.setStorageUint256(_getExitDeadlineThreshold); + emit ExitDeadlineThresholdChanged(_getExitDeadlineThreshold); } /** @@ -90,7 +85,7 @@ contract NodeOperatorExitManager { bytes _publicKey, uint256 _eligibleToExitInSec ) internal { - require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "INVALID_EXIT_TIME"); + require(_eligibleToExitInSec >= _getExitDeadlineThreshold(), "INVALID_EXIT_TIME"); require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); // Hash the public key to use as a mapping key @@ -109,13 +104,11 @@ contract NodeOperatorExitManager { record.lastUpdatedTimestamp = _proofSlotTimestamp; // Calculate penalty if the validator has exceeded the exit deadline - if (record.pinalizedFee != 0) { - // Calculate penalty based on the excess time - uint256 excessTime = _eligibleToExitInSec.sub(_exitDeadlineThreshold()); - uint256 penaltyAmount = _calculatePenalty(excessTime); + if (record.penalizedFee == 0) { + uint256 penaltyAmount = _getPenalty(); // Add to the penalized fee - record.pinalizedFee = record.pinalizedFee.add(penaltyAmount); + record.penalizedFee = record.penalizedFee.add(penaltyAmount); emit PenaltyApplied(_nodeOperatorId, _publicKey, penaltyAmount, "EXCESS_EXIT_TIME"); } @@ -139,10 +132,11 @@ contract NodeOperatorExitManager { require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); // Hash the public key to use as a mapping key - bytes32 publicKeyHash = keccak256(_publicKey); + bytes32 _publicKeyHash = keccak256(_publicKey); // Get or initialize the validator exit record - ValidatorExitRecord storage record = _getValidatorExitRecordByHash(_nodeOperatorId, publicKeyHash); + ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; + require(record.lastUpdatedTimestamp > 0, "VALIDATOR_RECORD_NOT_FOUND"); // Set the triggerable exit fee record.triggerableExitFee = _withdrawalRequestPaidFee; @@ -154,7 +148,7 @@ contract NodeOperatorExitManager { * @notice Returns the number of seconds after which a validator is considered late * @return The exit deadline threshold in seconds */ - function _exitDeadlineThreshold() public view returns (uint256) { + function _getExitDeadlineThreshold() public view returns (uint256) { return EXIT_DEADLINE_THRESHOLD_POSITION.getStorageUint256(); } @@ -173,178 +167,110 @@ contract NodeOperatorExitManager { uint256 _eligibleToExitInSec ) internal view returns (bool) { // If the validator has exceeded the exit deadline, it should be penalized - if (_eligibleToExitInSec >= _exitDeadlineThreshold()) { + if (_eligibleToExitInSec >= _getExitDeadlineThreshold()) { return true; } return false; } - /** - * @notice Get the exit record for a validator using its public key hash - * @param _nodeOperatorId The ID of the node operator - * @param _publicKeyHash Hash of the validator's public key - * @return Record data: eligibleToExitInSec, pinalizedFee, triggerableExitFee, lastUpdatedTimestamp - */ - function _getValidatorExitRecordByHash( - uint256 _nodeOperatorId, - bytes32 _publicKeyHash - ) internal view returns (uint256, uint256, uint256, uint256) { - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; - require(record.lastUpdatedTimestamp > 0, "VALIDATOR_RECORD_NOT_FOUND"); - - return ( - record.eligibleToExitInSec, - record.pinalizedFee, - record.triggerableExitFee, - record.lastUpdatedTimestamp - ); - } - - /** - * @notice Get the exit record for a validator using its public key - * @param _nodeOperatorId The ID of the node operator - * @param _publicKey The public key of the validator - * @return Record data: eligibleToExitInSec, pinalizedFee, triggerableExitFee, lastUpdatedTimestamp - */ - function _getValidatorExitRecord( - uint256 _nodeOperatorId, - bytes _publicKey - ) internal view returns (uint256, uint256, uint256, uint256) { - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - bytes32 publicKeyHash = keccak256(_publicKey); - - return _getValidatorExitRecordByHash(_nodeOperatorId, publicKeyHash); - } - /** * @notice Helper function to calculate penalty based on excess time - * @param _excessTime Time in seconds beyond the exit deadline - * @return Penalty amount in ETH + * @return Penalty amount in stETH */ - function _calculatePenalty(uint256 _excessTime) internal pure returns (uint256) { + function _getPenalty() internal pure returns (uint256) { // TODO: get the penalty rate from analytics team - return _excessTime.mul(1 ether).div(86400); + return 1 ether; } /** * @notice Apply penalties to an operator's rewards using public key hash * @param _nodeOperatorId The ID of the node operator * @param _publicKeyHash Hash of the validator's public key - * @param _stETH Interface to the stETH token - * @param _shares Amount of shares being distributed to the operator + * @param _sharesInStETH Amount of shares being distributed to the operator in stETH * @return Adjusted shares after penalties */ function _applyPenaltiesByHash( uint256 _nodeOperatorId, bytes32 _publicKeyHash, - IStETH _stETH, - uint256 _shares + uint256 _sharesInStETH ) internal returns (uint256) { // Check if record exists before attempting to apply penalties ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; if (record.lastUpdatedTimestamp == 0) { - return _shares; + return _sharesInStETH; } // If there are no penalties, return the original shares - if (record.pinalizedFee == 0 && record.triggerableExitFee == 0) { - return _shares; + if (record.penalizedFee == 0 && record.triggerableExitFee == 0) { + return _sharesInStETH; } - uint256 remainingShares = _shares; + uint256 remainingSharesInStETH = _sharesInStETH; // Apply penalties for exceeding exit deadline - if (record.pinalizedFee > 0) { - uint256 pinalizedShares = _stETH.getSharesByPooledEth(record.pinalizedFee); - - if (pinalizedShares >= remainingShares) { + if (record.penalizedFee > 0) { + if (record.penalizedFee >= remainingSharesInStETH) { // Not enough shares to cover the full penalty - record.pinalizedFee = record.pinalizedFee.sub( - _stETH.getPooledEthByShares(remainingShares) + record.penalizedFee = record.penalizedFee.sub( + remainingSharesInStETH ); - remainingShares = 0; + remainingSharesInStETH = 0; } else { // Enough shares to cover the penalty - remainingShares = remainingShares.sub(pinalizedShares); - record.pinalizedFee = 0; + remainingSharesInStETH = remainingSharesInStETH.sub(record.penalizedFee); + record.penalizedFee = 0; record.isPenalized = true; } } - // Apply penalties for triggerable exit fees - if (remainingShares > 0 && record.triggerableExitFee > 0) { - uint256 triggerableShares = _stETH.getSharesByPooledEth(record.triggerableExitFee); - - if (triggerableShares >= remainingShares) { + if (remainingSharesInStETH > 0 && record.triggerableExitFee > 0) { + if (record.triggerableExitFee >= remainingSharesInStETH) { // Not enough shares to cover the full fee record.triggerableExitFee = record.triggerableExitFee.sub( - _stETH.getPooledEthByShares(remainingShares) + remainingSharesInStETH ); - remainingShares = 0; + remainingSharesInStETH = 0; } else { // Enough shares to cover the fee - remainingShares = remainingShares.sub(triggerableShares); + remainingSharesInStETH = remainingSharesInStETH.sub(record.triggerableExitFee); record.triggerableExitFee = 0; record.isExited = true; } } - return remainingShares; - } - - /** - * @notice Apply penalties to an operator's rewards using public key - * @param _nodeOperatorId The ID of the node operator - * @param _publicKey The public key of the validator - * @param _stETH Interface to the stETH token - * @param _shares Amount of shares being distributed to the operator - * @return Adjusted shares after penalties - */ - function _applyPenalties( - uint256 _nodeOperatorId, - bytes _publicKey, - IStETH _stETH, - uint256 _shares - ) internal returns (uint256) { - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - bytes32 publicKeyHash = keccak256(_publicKey); - - return _applyPenaltiesByHash(_nodeOperatorId, publicKeyHash, _stETH, _shares); + return remainingSharesInStETH; } /** * @notice Apply penalties to all validators of an operator * @param _nodeOperatorId The ID of the node operator - * @param _stETH Interface to the stETH token - * @param _shares Amount of shares being distributed to the operator + * @param _sharesInStETH Amount of shares being distributed to the operator * @return Adjusted shares after penalties */ function _applyAllPenalties( uint256 _nodeOperatorId, - IStETH _stETH, - uint256 _shares + uint256 _sharesInStETH ) internal returns (uint256) { - uint256 remainingShares = _shares; - bytes32[] storage validatorKeys = operatorValidatorKeys[_nodeOperatorId]; + uint256 remainingSharesInStETH = _sharesInStETH; + bytes32[] storage validatorKeys = operatorWatchableValidatorKeys[_nodeOperatorId]; // Iterate through all validator keys for this operator for (uint256 i = 0; i < validatorKeys.length; i++) { - remainingShares = _applyPenaltiesByHash( + remainingSharesInStETH = _applyPenaltiesByHash( _nodeOperatorId, validatorKeys[i], - _stETH, - remainingShares + remainingSharesInStETH ); - if (remainingShares == 0) break; + if (remainingSharesInStETH == 0) break; } // Clean up completed validators from the watchable keys array // TODO: combine with _applyPenaltiesByHash to avoid double iteration _cleanupCompletedValidators(_nodeOperatorId); - return remainingShares; + return remainingSharesInStETH; } /** @@ -369,13 +295,4 @@ contract NodeOperatorExitManager { } } } - - /** - * @notice Get the count of validators with exit records for a node operator - * @param _nodeOperatorId The ID of the node operator - * @return Count of validators with exit records - */ - function _getValidatorExitRecordCount(uint256 _nodeOperatorId) internal view returns (uint256) { - return operatorWatchableValidatorKeys[_nodeOperatorId].length; - } } From 636865912bbdd4e25eed362612bb54e0cc386c98 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Apr 2025 20:38:45 +0200 Subject: [PATCH 071/405] refactor: streamline penalty application logic --- .../0.4.24/nos/NodeOperatorExitManager.sol | 100 ++++++------------ 1 file changed, 31 insertions(+), 69 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol index 8d9bf66f6c..ed80bd0fd3 100644 --- a/contracts/0.4.24/nos/NodeOperatorExitManager.sol +++ b/contracts/0.4.24/nos/NodeOperatorExitManager.sol @@ -27,16 +27,12 @@ contract NodeOperatorExitManager { uint256 withdrawalRequestPaidFee, uint256 exitType ); - event PenaltyApplied( - uint256 indexed nodeOperatorId, - bytes publicKey, - uint256 penaltyAmount, - string penaltyType - ); + event PenaltyApplied(uint256 indexed nodeOperatorId, bytes publicKey, uint256 penaltyAmount, string penaltyType); event ExitDeadlineThresholdChanged(uint256 threshold); // Storage positions - bytes32 internal constant EXIT_DEADLINE_THRESHOLD_POSITION = keccak256("lido.NodeOperatorExitManager.exitDeadlineThreshold"); + bytes32 internal constant EXIT_DEADLINE_THRESHOLD_POSITION = + keccak256("lido.NodeOperatorExitManager.exitDeadlineThreshold"); // Struct to store exit-related data for each validator struct ValidatorExitRecord { @@ -139,6 +135,7 @@ contract NodeOperatorExitManager { require(record.lastUpdatedTimestamp > 0, "VALIDATOR_RECORD_NOT_FOUND"); // Set the triggerable exit fee + // TODO: validation? record.triggerableExitFee = _withdrawalRequestPaidFee; emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); @@ -167,11 +164,7 @@ contract NodeOperatorExitManager { uint256 _eligibleToExitInSec ) internal view returns (bool) { // If the validator has exceeded the exit deadline, it should be penalized - if (_eligibleToExitInSec >= _getExitDeadlineThreshold()) { - return true; - } - - return false; + return _eligibleToExitInSec >= _getExitDeadlineThreshold(); } /** @@ -194,53 +187,39 @@ contract NodeOperatorExitManager { uint256 _nodeOperatorId, bytes32 _publicKeyHash, uint256 _sharesInStETH - ) internal returns (uint256) { - // Check if record exists before attempting to apply penalties + ) internal returns (uint256, bool) { ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; - if (record.lastUpdatedTimestamp == 0) { - return _sharesInStETH; - } - // If there are no penalties, return the original shares - if (record.penalizedFee == 0 && record.triggerableExitFee == 0) { - return _sharesInStETH; + if (record.lastUpdatedTimestamp == 0) { + return (_sharesInStETH, false); } uint256 remainingSharesInStETH = _sharesInStETH; - // Apply penalties for exceeding exit deadline if (record.penalizedFee > 0) { - if (record.penalizedFee >= remainingSharesInStETH) { - // Not enough shares to cover the full penalty - record.penalizedFee = record.penalizedFee.sub( - remainingSharesInStETH - ); + if (record.penalizedFee > remainingSharesInStETH) { + record.penalizedFee = record.penalizedFee.sub(remainingSharesInStETH); remainingSharesInStETH = 0; } else { - // Enough shares to cover the penalty remainingSharesInStETH = remainingSharesInStETH.sub(record.penalizedFee); record.penalizedFee = 0; record.isPenalized = true; } } - // Apply penalties for triggerable exit fees if (remainingSharesInStETH > 0 && record.triggerableExitFee > 0) { - if (record.triggerableExitFee >= remainingSharesInStETH) { - // Not enough shares to cover the full fee - record.triggerableExitFee = record.triggerableExitFee.sub( - remainingSharesInStETH - ); + if (record.triggerableExitFee > remainingSharesInStETH) { + record.triggerableExitFee = record.triggerableExitFee.sub(remainingSharesInStETH); remainingSharesInStETH = 0; } else { - // Enough shares to cover the fee remainingSharesInStETH = remainingSharesInStETH.sub(record.triggerableExitFee); record.triggerableExitFee = 0; record.isExited = true; } } - return remainingSharesInStETH; + bool completed = record.isPenalized && record.isExited; + return (remainingSharesInStETH, completed); } /** @@ -249,50 +228,33 @@ contract NodeOperatorExitManager { * @param _sharesInStETH Amount of shares being distributed to the operator * @return Adjusted shares after penalties */ - function _applyAllPenalties( - uint256 _nodeOperatorId, - uint256 _sharesInStETH - ) internal returns (uint256) { + function _applyAllPenalties(uint256 _nodeOperatorId, uint256 _sharesInStETH) internal returns (uint256) { uint256 remainingSharesInStETH = _sharesInStETH; bytes32[] storage validatorKeys = operatorWatchableValidatorKeys[_nodeOperatorId]; + uint256 i = 0; + + while (i < validatorKeys.length) { + bytes32 key = validatorKeys[i]; - // Iterate through all validator keys for this operator - for (uint256 i = 0; i < validatorKeys.length; i++) { - remainingSharesInStETH = _applyPenaltiesByHash( + (uint256 updatedShares, bool completed) = _applyPenaltiesByHash( _nodeOperatorId, - validatorKeys[i], + key, remainingSharesInStETH ); + remainingSharesInStETH = updatedShares; - if (remainingSharesInStETH == 0) break; - } - // Clean up completed validators from the watchable keys array - // TODO: combine with _applyPenaltiesByHash to avoid double iteration - _cleanupCompletedValidators(_nodeOperatorId); - - return remainingSharesInStETH; - } - - /** - * @notice Clean up validators that have completed processing from the watchable keys array - * @param _nodeOperatorId The ID of the node operator - */ - function _cleanupCompletedValidators(uint256 _nodeOperatorId) internal { - bytes32[] storage watchableKeys = operatorWatchableValidatorKeys[_nodeOperatorId]; - uint256 i = 0; - - while (i < watchableKeys.length) { - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][watchableKeys[i]]; - - if (record.isPenalized && record.isExited) { - // If both conditions are met, remove from watchable keys by swapping with the last element - watchableKeys[i] = watchableKeys[watchableKeys.length - 1]; - watchableKeys.length--; - // Don't increment i as we need to process the swapped element + if (completed) { + validatorKeys[i] = validatorKeys[validatorKeys.length - 1]; + validatorKeys.length--; } else { - // Move to next key i++; } + + if (remainingSharesInStETH == 0) { + break; + } } + + return remainingSharesInStETH; } } From 9c5f0b289eb8a5dae66501d25e70dbe3dcb9466e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 14 Apr 2025 20:06:17 +0400 Subject: [PATCH 072/405] fix: fix helpers test --- test/0.8.9/contracts/ValidatorsExitBus__Harness.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index af33a90331..56bcdff561 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -52,7 +52,7 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { totalItemsCount, deliveredItemsCount, contractVersion, - lastDeliveredKeyIndex + DeliveryHistory(lastDeliveredKeyIndex, block.timestamp) ); } } From 4403b51f5b22e623c8a20fa8e0b03ae0b72a18a8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 14 Apr 2025 20:07:29 +0400 Subject: [PATCH 073/405] fix: locator use fix --- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 3 --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index ee0871f83c..c2b3ffd560 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -51,9 +51,6 @@ library ReportExitLimitUtilsStorage { } library ReportExitLimitUtils { - - error Debug(uint256 limit, uint256 block, uint256 prev); - /** * @notice Calculate exit requests limit * @dev using `_constGasMin` to make gas consumption independent of the current block number diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index b040f06423..1d9596f4da 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -181,9 +181,7 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab if (requestStatus.contractVersion == 0) { revert ExitHashWasNotSubmitted(); } - - address locatorAddr = address(LOCATOR); - address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); + address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); if (msg.value < keyIndexes.length * withdrawalFee ) { @@ -240,8 +238,7 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab function triggerExitsDirectly(ValidatorExitData calldata validator) external payable whenResumed onlyRole(DIRECT_EXIT_HASH_ROLE) returns (uint256) { uint256 prevBalance = address(this).balance - msg.value; - address locatorAddr = address(LOCATOR); - address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault(); + address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); if (msg.value < withdrawalFee ) { From 6b4f952738e5236aea2a00a11fbb4e2522c4903c Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 15 Apr 2025 12:53:04 +0200 Subject: [PATCH 074/405] feat: add partial withdrawal requests support --- contracts/0.8.9/WithdrawalVault.sol | 66 +--- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 123 +++++++ test/0.8.9/contracts/RefundFailureTester.sol | 8 + test/0.8.9/withdrawalVault.test.ts | 328 ++++++++++++++++--- 4 files changed, 422 insertions(+), 103 deletions(-) create mode 100644 contracts/0.8.9/WithdrawalVaultEIP7685.sol diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 9df5e186f0..4b0eb9b4d5 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -10,8 +10,9 @@ import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; -import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; + import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; +import {WithdrawalVaultEIP7685} from "./WithdrawalVaultEIP7685.sol"; interface ILido { /** @@ -25,14 +26,12 @@ interface ILido { /** * @title A vault for temporary storage of withdrawals */ -contract WithdrawalVault is AccessControlEnumerable, Versioned { +contract WithdrawalVault is AccessControlEnumerable, Versioned, WithdrawalVaultEIP7685 { using SafeERC20 for IERC20; ILido public immutable LIDO; address public immutable TREASURY; - bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); - // Events /** * Emitted when the ERC20 `token` recovered (i.e. transferred) @@ -51,12 +50,6 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { error NotLido(); error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); - error InsufficientTriggerableWithdrawalFee( - uint256 providedTotalFee, - uint256 requiredTotalFee, - uint256 requestCount - ); - error TriggerableWithdrawalRefundFailed(); /** * @param _lido the Lido token (stETH) address @@ -142,59 +135,6 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned { _token.transferFrom(address(this), TREASURY, _tokenId); } - /** - * @dev Submits EIP-7002 full withdrawal requests for the specified public keys. - * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. - * Refunds any excess fee to the caller after deducting the total fees, - * which are calculated based on the number of public keys and the current minimum fee per withdrawal request. - * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @notice Reverts if: - * - The caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE`. - * - Validation of any of the provided public keys fails. - * - The provided total withdrawal fee is insufficient to cover all requests. - * - Refund of the excess fee fails. - */ - function addFullWithdrawalRequests( - bytes calldata pubkeys - ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) { - uint256 prevBalance = address(this).balance - msg.value; - - uint256 minFeePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); - uint256 totalFee = (pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH) * minFeePerRequest; - - if (totalFee > msg.value) { - revert InsufficientTriggerableWithdrawalFee( - msg.value, - totalFee, - pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH - ); - } - - TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, minFeePerRequest); - - uint256 refund = msg.value - totalFee; - if (refund > 0) { - (bool success, ) = msg.sender.call{value: refund}(""); - - if (!success) { - revert TriggerableWithdrawalRefundFailed(); - } - } - - assert(address(this).balance == prevBalance); - } - - /** - * @dev Retrieves the current EIP-7002 withdrawal fee. - * @return The minimum fee required per withdrawal request. - */ - function getWithdrawalRequestFee() external view returns (uint256) { - return TriggerableWithdrawals.getWithdrawalRequestFee(); - } - function _onlyNonZeroAddress(address _address) internal pure { if (_address == address(0)) revert ZeroAddress(); } diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol new file mode 100644 index 0000000000..6fde4be879 --- /dev/null +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +pragma solidity 0.8.9; + +import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; + +import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; + +/** + * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests + * @dev This contract enables validators to submit EIP-7002 withdrawal requests + * and manages the associated fees. + */ +abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { + bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); + bytes32 public constant ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE"); + + uint256 internal constant PUBLIC_KEY_LENGTH = 48; + + error InsufficientFee(uint256 providedFee, uint256 requiredFee); + error ExcessFeeRefundFailed(); + + /// @dev Ensures the contract’s ETH balance is unchanged. + modifier preservesEthBalance() { + uint256 balanceBeforeCall = address(this).balance - msg.value; + _; + assert(address(this).balance == balanceBeforeCall); + } + + /** + * @dev Submits EIP-7002 full withdrawal requests for the specified public keys. + * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. + * Refunds any excess fee to the caller after deducting the total fees, + * which are calculated based on the number of public keys and the current minimum fee per withdrawal request. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @notice Reverts if: + * - The caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE`. + * - The provided public key array is empty. + * - Validation of any of the provided public keys fails. + * - The provided total withdrawal fee is insufficient to cover all requests. + * - Refund of the excess fee fails. + */ + function addFullWithdrawalRequests( + bytes calldata pubkeys + ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + uint256 feePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); + uint256 totalFee = _countPubkeys(pubkeys) * feePerRequest; + + _requireSufficientFee(totalFee); + + TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); + + _refundExcessFee(totalFee); + } + + /** + * @dev Submits EIP-7002 partial withdrawal requests for the specified public keys with corresponding amounts. + * Each request instructs a validator to withdraw a specified amount of ETH via their execution layer (0x01) withdrawal credentials. + * Refunds any excess fee to the caller after deducting the total fees, + * which are calculated based on the number of public keys and the current minimum fee per withdrawal request. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting partial withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @param amounts An array of 8-byte unsigned integers representing the amounts to be withdrawn for each corresponding public key. + * + * @notice Reverts if: + * - The caller does not have the `ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE`. + * - The provided public key array is empty. + * - The provided public key and amount arrays are not of equal length. + * - Full withdrawal requested for any pubkeys (withdrawal amount = 0). + * - Validation of any of the provided public keys fails. + * - The provided total withdrawal fee is insufficient to cover all requests. + * - Refund of the excess fee fails. + */ + function addPartialWithdrawalRequests( + bytes calldata pubkeys, + uint64[] calldata amounts + ) external payable onlyRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + uint256 feePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); + uint256 totalFee = _countPubkeys(pubkeys) * feePerRequest; + + _requireSufficientFee(totalFee); + + TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, feePerRequest); + + _refundExcessFee(totalFee); + } + + /** + * @dev Retrieves the current EIP-7002 withdrawal fee. + * @return The minimum fee required per withdrawal request. + */ + function getWithdrawalRequestFee() external view returns (uint256) { + return TriggerableWithdrawals.getWithdrawalRequestFee(); + } + + function _countPubkeys(bytes calldata pubkeys) internal pure returns (uint256) { + return (pubkeys.length / PUBLIC_KEY_LENGTH); + } + + function _requireSufficientFee(uint256 requiredFee) internal view { + if (requiredFee > msg.value) { + revert InsufficientFee(msg.value, requiredFee); + } + } + + function _refundExcessFee(uint256 fee) internal { + uint256 refund = msg.value - fee; + if (refund > 0) { + (bool success, ) = msg.sender.call{value: refund}(""); + + if (!success) { + revert ExcessFeeRefundFailed(); + } + } + } +} diff --git a/test/0.8.9/contracts/RefundFailureTester.sol b/test/0.8.9/contracts/RefundFailureTester.sol index 0363e87cf5..3d054cb9cd 100644 --- a/test/0.8.9/contracts/RefundFailureTester.sol +++ b/test/0.8.9/contracts/RefundFailureTester.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.9; interface IWithdrawalVault { function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; + function addPartialWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; function getWithdrawalRequestFee() external view returns (uint256); } @@ -28,4 +29,11 @@ contract RefundFailureTester { // withdrawal vault should fail to refund withdrawalVault.addFullWithdrawalRequests{value: msg.value}(pubkeys); } + + function addPartialWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable { + require(msg.value > withdrawalVault.getWithdrawalRequestFee(), "Not enough eth for Refund"); + + // withdrawal vault should fail to refund + withdrawalVault.addPartialWithdrawalRequests{value: msg.value}(pubkeys, amounts); + } } diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts index 438ff8ed10..8d8646cff5 100644 --- a/test/0.8.9/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { ZeroAddress } from "ethers"; +import { ContractTransactionResponse, ZeroAddress } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; @@ -23,6 +23,7 @@ import { Snapshot } from "test/suite"; const PETRIFIED_VERSION = MAX_UINT256; const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); +const ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE"); describe("WithdrawalVault.sol", () => { let owner: HardhatEthersSigner; @@ -309,6 +310,7 @@ describe("WithdrawalVault.sol", () => { beforeEach(async () => { await vault.initialize(owner); await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + await vault.connect(owner).grantRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); }); it("Should revert if the caller is not Validator Exit Bus", async () => { @@ -316,31 +318,89 @@ describe("WithdrawalVault.sol", () => { stranger.address, ADD_FULL_WITHDRAWAL_REQUEST_ROLE, ); + + await expect( + vault.connect(stranger).addPartialWithdrawalRequests("0x1234", [1n]), + ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE); }); it("Should revert if empty arrays are provided", async function () { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests("0x", { value: 1n }), ).to.be.revertedWithCustomError(vault, "NoWithdrawalRequests"); + + await expect( + vault.connect(validatorsExitBus).addPartialWithdrawalRequests("0x", [], { value: 1n }), + ).to.be.revertedWithCustomError(vault, "NoWithdrawalRequests"); + }); + + it("Should revert if array lengths do not match", async function () { + const requestCount = 2; + const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); + const amounts = [1n]; + + const totalWithdrawalFee = (await getFee()) * BigInt(requestCount); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, amounts, { value: totalWithdrawalFee }), + ) + .to.be.revertedWithCustomError(vault, "MismatchedArrayLengths") + .withArgs(requestCount, amounts.length); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, [], { value: totalWithdrawalFee }), + ) + .to.be.revertedWithCustomError(vault, "MismatchedArrayLengths") + .withArgs(requestCount, 0); + }); + + it("Should revert when a full withdrawal amount is included in 'addPartialWithdrawalRequests'", async function () { + const { pubkeysHexString } = generateWithdrawalRequestPayload(2); + const amounts = [1n, 0n]; // Partial and Full withdrawal + const totalWithdrawalFee = (await getFee()) * BigInt(pubkeysHexString.length); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, amounts, { value: totalWithdrawalFee }), + ).to.be.revertedWithCustomError(vault, "PartialWithdrawalRequired"); }); it("Should revert if not enough fee is sent", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(1); await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString)) - .to.be.revertedWithCustomError(vault, "InsufficientTriggerableWithdrawalFee") - .withArgs(0, 3n, 1); + .to.be.revertedWithCustomError(vault, "InsufficientFee") + .withArgs(0, 3n); + + await expect( + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts), + ) + .to.be.revertedWithCustomError(vault, "InsufficientFee") + .withArgs(0, 3n); // 2. Should revert if fee is less than required const insufficientFee = 2n; await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: insufficientFee }), ) - .to.be.revertedWithCustomError(vault, "InsufficientTriggerableWithdrawalFee") - .withArgs(2n, 3n, 1); + .to.be.revertedWithCustomError(vault, "InsufficientFee") + .withArgs(2n, 3n); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: insufficientFee }), + ) + .to.be.revertedWithCustomError(vault, "InsufficientFee") + .withArgs(2n, 3n); }); it("Should revert if pubkey is not 48 bytes", async function () { @@ -352,6 +412,10 @@ describe("WithdrawalVault.sol", () => { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(invalidPubkeyHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); + + await expect( + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(invalidPubkeyHexString, [1n], { value: fee }), + ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); }); it("Should revert if last pubkey not 48 bytes", async function () { @@ -365,10 +429,14 @@ describe("WithdrawalVault.sol", () => { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); + + await expect( + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, [1n, 2n], { value: fee }), + ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); }); it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(1); + const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(1); const fee = await getFee(); // Set mock to fail on add @@ -377,29 +445,47 @@ describe("WithdrawalVault.sol", () => { await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "WithdrawalRequestAdditionFailed"); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: fee }), + ).to.be.revertedWithCustomError(vault, "WithdrawalRequestAdditionFailed"); }); it("Should revert when fee read fails", async function () { await withdrawalsPredeployed.mock__setFailOnGetFee(true); - const { pubkeysHexString } = generateWithdrawalRequestPayload(2); + const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(2); const fee = 10n; await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: fee }), + ).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); }); ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { - it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + it(`Should revert if unexpected fee value ${unexpectedFee} is returned`, async function () { await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); - const { pubkeysHexString } = generateWithdrawalRequestPayload(2); + const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(2); const fee = 10n; await expect( vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), ).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); + + await expect( + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: fee }), + ).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); }); }); @@ -410,9 +496,10 @@ describe("WithdrawalVault.sol", () => { const refundFailureTesterAddress = await refundFailureTester.getAddress(); await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, refundFailureTesterAddress); + await vault.connect(owner).grantRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE, refundFailureTesterAddress); const requestCount = 3; - const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); @@ -422,18 +509,31 @@ describe("WithdrawalVault.sol", () => { refundFailureTester .connect(stranger) .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + 1n }), - ).to.be.revertedWithCustomError(vault, "TriggerableWithdrawalRefundFailed"); + ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); + + await expect( + refundFailureTester.connect(stranger).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee + 1n, + }), + ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); await expect( refundFailureTester .connect(stranger) .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + ethers.parseEther("1") }), - ).to.be.revertedWithCustomError(vault, "TriggerableWithdrawalRefundFailed"); + ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); + + await expect( + refundFailureTester.connect(stranger).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee + ethers.parseEther("1"), + }), + ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); }); it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(3n); @@ -449,6 +549,16 @@ describe("WithdrawalVault.sol", () => { fee, ); + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + // Check extremely high fee const highFee = ethers.parseEther("10"); await withdrawalsPredeployed.mock__setFee(highFee); @@ -463,11 +573,22 @@ describe("WithdrawalVault.sol", () => { fullWithdrawalAmounts, highFee, ); + + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedLargeTotalWithdrawalFee, + }), + pubkeys, + partialWithdrawalAmounts, + highFee, + ); }); it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); @@ -480,6 +601,16 @@ describe("WithdrawalVault.sol", () => { fee, ); + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: withdrawalFee }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + // Check when the provided fee extremely exceeds the required amount const largeWithdrawalFee = ethers.parseEther("10"); @@ -490,11 +621,22 @@ describe("WithdrawalVault.sol", () => { fullWithdrawalAmounts, fee, ); + + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: largeWithdrawalFee }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); }); it("Should not affect contract balance", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); @@ -513,6 +655,17 @@ describe("WithdrawalVault.sol", () => { ); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + const excessTotalWithdrawalFee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei await testEIP7002Mock( @@ -526,20 +679,33 @@ describe("WithdrawalVault.sol", () => { ); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: excessTotalWithdrawalFee, + }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); }); it("Should refund excess fee", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei const excessFee = 1n; - const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + let vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - const { receipt } = await testEIP7002Mock( + const { receipt: fullWithdrawalReceipt } = await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -550,13 +716,32 @@ describe("WithdrawalVault.sol", () => { ); expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( - vebInitialBalance - expectedTotalWithdrawalFee - receipt.gasUsed * receipt.gasPrice, + vebInitialBalance - expectedTotalWithdrawalFee - fullWithdrawalReceipt.gasUsed * fullWithdrawalReceipt.gasPrice, + ); + + vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + + const { receipt: partialWithdrawalReceipt } = await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee + excessFee, + }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + + expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( + vebInitialBalance - + expectedTotalWithdrawalFee - + partialWithdrawalReceipt.gasUsed * partialWithdrawalReceipt.gasPrice, ); }); it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(3n); @@ -577,6 +762,20 @@ describe("WithdrawalVault.sol", () => { expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); + initialBalance = await getWithdrawalsPredeployedContractBalance(); + + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); + initialBalance = await getWithdrawalsPredeployedContractBalance(); await testEIP7002Mock( () => @@ -587,33 +786,61 @@ describe("WithdrawalVault.sol", () => { fullWithdrawalAmounts, fee, ); - // Only the expected fee should be transferred + + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); + + initialBalance = await getWithdrawalsPredeployedContractBalance(); + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: excessTotalWithdrawalFee, + }), + pubkeys, + partialWithdrawalAmounts, + fee, + ); + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); }); it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { const requestCount = 16; - const { pubkeysHexString, pubkeys } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, partialWithdrawalAmounts, fullWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const totalWithdrawalFee = 333n; - const tx = await vault + const testEncoding = async ( + tx: ContractTransactionResponse, + expectedPubkeys: string[], + expectedAmounts: bigint[], + ) => { + const receipt = await tx.wait(); + + const events = findEIP7002MockEvents(receipt!); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + const encodedRequest = events[i].args[0]; + // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters + expect(encodedRequest.length).to.equal(114); + + expect(encodedRequest.slice(0, 2)).to.equal("0x"); + expect(encodedRequest.slice(2, 98)).to.equal(expectedPubkeys[i]); + expect(encodedRequest.slice(98, 114)).to.equal(expectedAmounts[i].toString(16).padStart(16, "0")); + } + }; + + const txFullWithdrawal = await vault .connect(validatorsExitBus) .addFullWithdrawalRequests(pubkeysHexString, { value: totalWithdrawalFee }); - const receipt = await tx.wait(); - - const events = findEIP7002MockEvents(receipt!); - expect(events.length).to.equal(requestCount); + await testEncoding(txFullWithdrawal, pubkeys, fullWithdrawalAmounts); - for (let i = 0; i < requestCount; i++) { - const encodedRequest = events[i].args[0]; - // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters - expect(encodedRequest.length).to.equal(114); + const txPartialWithdrawal = await vault + .connect(validatorsExitBus) + .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: totalWithdrawalFee }); - expect(encodedRequest.slice(0, 2)).to.equal("0x"); - expect(encodedRequest.slice(2, 98)).to.equal(pubkeys[i]); - expect(encodedRequest.slice(98, 114)).to.equal("0".repeat(16)); // Amount is 0 - } + await testEncoding(txPartialWithdrawal, pubkeys, partialWithdrawalAmounts); }); const testCasesForWithdrawalRequests = [ @@ -630,14 +857,15 @@ describe("WithdrawalVault.sol", () => { testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { it(`Should successfully add ${requestCount} requests with extra fee ${extraFee}`, async () => { - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = + generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); const initialBalance = await getWithdrawalCredentialsContractBalance(); - const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + let vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - const { receipt } = await testEIP7002Mock( + const { receipt: receiptFullWithdrawal } = await testEIP7002Mock( () => vault .connect(validatorsExitBus) @@ -649,7 +877,27 @@ describe("WithdrawalVault.sol", () => { expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( - vebInitialBalance - expectedTotalWithdrawalFee - receipt.gasUsed * receipt.gasPrice, + vebInitialBalance - + expectedTotalWithdrawalFee - + receiptFullWithdrawal.gasUsed * receiptFullWithdrawal.gasPrice, + ); + + vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + const { receipt: receiptPartialWithdrawal } = await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { + value: expectedTotalWithdrawalFee + extraFee, + }), + pubkeys, + partialWithdrawalAmounts, + expectedFee, + ); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( + vebInitialBalance - + expectedTotalWithdrawalFee - + receiptPartialWithdrawal.gasUsed * receiptPartialWithdrawal.gasPrice, ); }); }); From 1d469518e049a89270fbc51f728d7078a2aea2b5 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 15 Apr 2025 16:37:05 +0200 Subject: [PATCH 075/405] fix: correct variable name for public key hash in validator exit record --- contracts/0.4.24/nos/NodeOperatorExitManager.sol | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol index ed80bd0fd3..2ea962b253 100644 --- a/contracts/0.4.24/nos/NodeOperatorExitManager.sol +++ b/contracts/0.4.24/nos/NodeOperatorExitManager.sol @@ -88,8 +88,8 @@ contract NodeOperatorExitManager { bytes32 publicKeyHash = keccak256(_publicKey); // Track this validator key if it's new - if (validatorExitRecords[_nodeOperatorId][_publicKeyHash].lastUpdatedTimestamp == 0) { - operatorWatchableValidatorKeys[_nodeOperatorId].push(_publicKeyHash); + if (validatorExitRecords[_nodeOperatorId][publicKeyHash].lastUpdatedTimestamp == 0) { + operatorWatchableValidatorKeys[_nodeOperatorId].push(publicKeyHash); } // Get or initialize the validator exit record @@ -151,9 +151,6 @@ contract NodeOperatorExitManager { /** * @notice Determines whether a validator's exit status should be updated - * @param _nodeOperatorId The ID of the node operator - * @param _proofSlotTimestamp The timestamp when the validator was last known to be active - * @param _publicKey The public key of the validator * @param _eligibleToExitInSec The number of seconds the validator was eligible to exit * @return bool Returns true if the contract should receive the updated status */ From 2e2ea0885aef09f0331d6beec6fa8e2f91ffe9ae Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 15 Apr 2025 19:41:56 +0400 Subject: [PATCH 076/405] fix: moved unpackExitRequest in veb & add batching in triggerExitsDirectly --- contracts/0.8.25/ValidatorExitVerifier.sol | 8 +- ...itBusOracle.sol => IValidatorsExitBus.sol} | 2 +- .../0.8.9/interfaces/IValidatorExitBus.sol | 17 ++- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 103 ++++++++++++++---- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 66 ----------- .../ValidatorsExitBusOracle_Mock.sol | 4 +- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 43 +++----- ...it-bus-oracle.triggerExitsDirectly.test.ts | 42 +++---- 8 files changed, 141 insertions(+), 144 deletions(-) rename contracts/0.8.25/interfaces/{IValidatorsExitBusOracle.sol => IValidatorsExitBus.sol} (94%) diff --git a/contracts/0.8.25/ValidatorExitVerifier.sol b/contracts/0.8.25/ValidatorExitVerifier.sol index 3f5196c6d6..3d356a72fb 100644 --- a/contracts/0.8.25/ValidatorExitVerifier.sol +++ b/contracts/0.8.25/ValidatorExitVerifier.sol @@ -7,7 +7,7 @@ import {BeaconBlockHeader, Validator} from "./lib/BeaconTypes.sol"; import {GIndex} from "./lib/GIndex.sol"; import {SSZ} from "./lib/SSZ.sol"; import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; -import {IValidatorsExitBusOracle, DeliveryHistory} from "./interfaces/IValidatorsExitBusOracle.sol"; +import {IValidatorsExitBus, DeliveryHistory} from "./interfaces/IValidatorsExitBus.sol"; import {IStakingRouter} from "./interfaces/IStakingRouter.sol"; struct ExitRequestData { @@ -174,7 +174,7 @@ contract ValidatorExitVerifier { ) external { _verifyBeaconBlockRoot(beaconBlock); - IValidatorsExitBusOracle vebo = IValidatorsExitBusOracle(LOCATOR.validatorsExitBusOracle()); + IValidatorsExitBus vebo = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); @@ -225,7 +225,7 @@ contract ValidatorExitVerifier { _verifyBeaconBlockRoot(beaconBlock); _verifyHistoricalBeaconBlockRoot(beaconBlock, oldBlock); - IValidatorsExitBusOracle vebo = IValidatorsExitBusOracle(LOCATOR.validatorsExitBusOracle()); + IValidatorsExitBus vebo = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); @@ -367,7 +367,7 @@ contract ValidatorExitVerifier { } function _getExitRequestDeliveryHistory( - IValidatorsExitBusOracle vebo, + IValidatorsExitBus vebo, ExitRequestData calldata exitRequests ) internal view returns (ExitRequestsDeliveryHistory memory) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol similarity index 94% rename from contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol rename to contracts/0.8.25/interfaces/IValidatorsExitBus.sol index 3b0ec9daf7..907deea590 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -10,7 +10,7 @@ struct DeliveryHistory { uint256 lastDeliveredKeyIndex; } -interface IValidatorsExitBusOracle { +interface IValidatorsExitBus { function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history); diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol index 72d1f95e5b..a5e97d1188 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -8,11 +8,10 @@ interface IValidatorsExitBus { uint256 dataFormat; } - struct ValidatorExitData { + struct DirectExitData { uint256 stakingModuleId; uint256 nodeOperatorId; - uint256 validatorIndex; - bytes validatorPubkey; + bytes validatorsPubkeys; } struct DeliveryHistory { @@ -26,11 +25,19 @@ interface IValidatorsExitBus { function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable; - function triggerExitsDirectly(ValidatorExitData calldata validator) external payable returns (uint256); + function triggerExitsDirectly(DirectExitData calldata exitData) external payable returns (uint256); function setExitReportLimit(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock) external; - function getDeliveryHistory(bytes32 exitReportHash) external view returns (DeliveryHistory[] memory); + function getExitRequestsDeliveryHistory( + bytes32 exitRequestsHash + ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history); + + function unpackExitRequest( + bytes calldata exitRequests, + uint256 dataFormat, + uint256 index + ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); function resume() external; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 1d9596f4da..496b25275e 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -16,7 +16,7 @@ interface IWithdrawalVault { function getWithdrawalRequestFee() external view returns (uint256); } -abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { +contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ReportExitLimitUtilsStorage for bytes32; using ReportExitLimitUtils for ExitRequestLimitData; @@ -33,6 +33,8 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab error InvalidRequestsData(); error RequestsAlreadyDelivered(); error ExitRequestsLimit(); + error InvalidPubkeysArray(); + error NoExitRequestProvided(); /// @dev Events event MadeRefund( @@ -57,6 +59,13 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab uint256 _exitRequestsLimitIncreasePerBlock ); + event DirectExitRequest( + uint256 indexed stakingModuleId, + uint256 indexed nodeOperatorId, + bytes validatorsPubkeys, + uint256 timestamp + ); + struct RequestStatus { // Total items count in report (by default type(uint32).max, update on first report delivery) uint256 totalItemsCount; @@ -236,31 +245,28 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab assert(address(this).balance == prevBalance); } - function triggerExitsDirectly(ValidatorExitData calldata validator) external payable whenResumed onlyRole(DIRECT_EXIT_HASH_ROLE) returns (uint256) { + function triggerExitsDirectly(DirectExitData calldata exitData) external payable whenResumed onlyRole(DIRECT_EXIT_HASH_ROLE) returns (uint256) { uint256 prevBalance = address(this).balance - msg.value; address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - if (msg.value < withdrawalFee ) { - revert InsufficientPayment(withdrawalFee, 1, msg.value); + if (exitData.validatorsPubkeys.length == 0) { + revert NoExitRequestProvided(); } - ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - - if (exitRequestLimitData.isExitReportLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); - if (limit == 0) { - revert ExitRequestsLimit(); - } + if ( exitData.validatorsPubkeys.length % PUBLIC_KEY_LENGTH != 0) { + revert InvalidPubkeysArray(); + } - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - 1)); + if (msg.value < withdrawalFee * (exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH )) { + revert InsufficientPayment(withdrawalFee,(exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH ), msg.value); } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee}(validator.validatorPubkey); + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * (exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH )}(exitData.validatorsPubkeys); - emit ValidatorExitRequest(validator.stakingModuleId, validator.nodeOperatorId, validator.validatorIndex, validator.validatorPubkey, _getTimestamp()); + emit DirectExitRequest(exitData.stakingModuleId, exitData.nodeOperatorId, exitData.validatorsPubkeys, _getTimestamp()); - uint256 refund = msg.value - withdrawalFee; + uint256 refund = msg.value - withdrawalFee * (exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH ); if (refund > 0) { (bool success, ) = msg.sender.call{value: refund}(""); @@ -285,11 +291,70 @@ abstract contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerab emit ExitRequestsLimitSet(_maxExitRequestsLimit, _exitRequestsLimitIncreasePerBlock); } - function getDeliveryHistory(bytes32 exitReportHash) external view returns (DeliveryHistory[] memory) { - mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); - RequestStatus storage request = hashes[exitReportHash]; + function getExitRequestsDeliveryHistory( + bytes32 exitRequestsHash + ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { + RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; + + if (requestStatus.contractVersion == 0) { + revert ExitHashWasNotSubmitted(); + } + + return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); + } + + function unpackExitRequest( + bytes calldata exitRequests, + uint256 dataFormat, + uint256 index + ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { + if (dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(dataFormat); + } + + if (exitRequests.length % PACKED_REQUEST_LENGTH != 0) { + revert InvalidRequestsDataLength(); + } + + if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { + revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); + } + + uint256 itemOffset; + uint256 dataWithoutPubkey; + + assembly { + // Compute the start of this packed request (item) + itemOffset := add(exitRequests.offset, mul(PACKED_REQUEST_LENGTH, index)) + + // Load the first 16 bytes which contain moduleId (24 bits), + // nodeOpId (40 bits), and valIndex (64 bits). + dataWithoutPubkey := shr(128, calldataload(itemOffset)) + } + + // dataWithoutPubkey format (128 bits total): + // MSB <-------------------- 128 bits --------------------> LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + + valIndex = uint64(dataWithoutPubkey); + nodeOpId = uint40(dataWithoutPubkey >> 64); + moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + // Allocate a new bytes array in memory for the pubkey + pubkey = new bytes(PUBLIC_KEY_LENGTH); + + assembly { + // Starting offset in calldata for the pubkey part + let pubkeyCalldataOffset := add(itemOffset, 16) + + // Memory location of the 'pubkey' bytes array data + let pubkeyMemPtr := add(pubkey, 32) + + // Copy the 48 bytes of the pubkey from calldata into memory + calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + } - return request.deliverHistory; + return (pubkey, nodeOpId, moduleId, valIndex); } /// @notice Resume accepting validator exit requests diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index bd09223f91..61728a527f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -204,72 +204,6 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { result.requestsSubmitted = procState.requestsProcessed; } - function unpackExitRequest( - bytes calldata exitRequests, - uint256 dataFormat, - uint256 index - ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { - if (dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(dataFormat); - } - - if (exitRequests.length % PACKED_REQUEST_LENGTH != 0) { - revert InvalidRequestsDataLength(); - } - - if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { - revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); - } - - uint256 itemOffset; - uint256 dataWithoutPubkey; - - assembly { - // Compute the start of this packed request (item) - itemOffset := add(exitRequests.offset, mul(PACKED_REQUEST_LENGTH, index)) - - // Load the first 16 bytes which contain moduleId (24 bits), - // nodeOpId (40 bits), and valIndex (64 bits). - dataWithoutPubkey := shr(128, calldataload(itemOffset)) - } - - // dataWithoutPubkey format (128 bits total): - // MSB <-------------------- 128 bits --------------------> LSB - // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - - valIndex = uint64(dataWithoutPubkey); - nodeOpId = uint40(dataWithoutPubkey >> 64); - moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - // Allocate a new bytes array in memory for the pubkey - pubkey = new bytes(PUBLIC_KEY_LENGTH); - - assembly { - // Starting offset in calldata for the pubkey part - let pubkeyCalldataOffset := add(itemOffset, 16) - - // Memory location of the 'pubkey' bytes array data - let pubkeyMemPtr := add(pubkey, 32) - - // Copy the 48 bytes of the pubkey from calldata into memory - calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) - } - - return (pubkey, nodeOpId, moduleId, valIndex); - } - - function getExitRequestsDeliveryHistory( - bytes32 exitRequestsHash - ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { - RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; - - if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); - } - - return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); - } - /// /// Implementation & helpers /// diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 84c4724967..d335d4a830 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IValidatorsExitBusOracle, DeliveryHistory} from "contracts/0.8.25/interfaces/IValidatorsExitBusOracle.sol"; +import {IValidatorsExitBus, DeliveryHistory} from "contracts/0.8.25/interfaces/IValidatorsExitBus.sol"; struct MockExitRequestDeliveryHistory { uint256 totalItemsCount; @@ -15,7 +15,7 @@ struct MockExitRequestData { uint256 moduleId; uint256 valIndex; } -contract ValidatorsExitBusOracle_Mock is IValidatorsExitBusOracle { +contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { bytes32 _hash; MockExitRequestDeliveryHistory private _history; MockExitRequestData[] private _data; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index 2add9e2064..92c6f7ea77 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -83,10 +83,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, ]; - exitRequest = { - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }; + exitRequest = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; await expect(oracle.emitExitEvents(exitRequest, 2)) .to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted") @@ -173,10 +170,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], request)); const submitTx = await oracle.connect(authorizedEntity).submitReportHash(hash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(hash); - exitRequest = { - dataFormat: 2, - data: encodeExitRequestsDataList(exitRequests), - }; + exitRequest = { dataFormat: 2, data: encodeExitRequestsDataList(exitRequests) }; await expect(oracle.emitExitEvents(exitRequest, 2)) .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") .withArgs(2); @@ -196,17 +190,14 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, ]; - exitRequest = { - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }; + exitRequest = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; exitRequestHash = ethers.keccak256( ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), ); - const history0 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history0.length).to.eq(0); + // const history0 = await oracle.getDeliveryHistory(exitRequestHash); + // expect(history0.length).to.eq(0); const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash"); @@ -238,9 +229,9 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { timestamp, ); - const history1 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history1.length).to.eq(1); - expect(history1[0].lastDeliveredKeyIndex).to.eq(1); + // const history1 = await oracle.getDeliveryHistory(exitRequestHash); + // expect(history1.length).to.eq(1); + // expect(history1[0].lastDeliveredKeyIndex).to.eq(1); const emitTx2 = await oracle.emitExitEvents(exitRequest, 2); @@ -257,9 +248,9 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { timestamp, ); - const history2 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history2.length).to.eq(2); - expect(history2[1].lastDeliveredKeyIndex).to.eq(2); + // const history2 = await oracle.getDeliveryHistory(exitRequestHash); + // expect(history2.length).to.eq(2); + // expect(history2[1].lastDeliveredKeyIndex).to.eq(2); const emitTx3 = await oracle.emitExitEvents(exitRequest, 2); @@ -276,9 +267,9 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { timestamp, ); - const history3 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history3.length).to.eq(3); - expect(history3[2].lastDeliveredKeyIndex).to.eq(3); + // const history3 = await oracle.getDeliveryHistory(exitRequestHash); + // expect(history3.length).to.eq(3); + // expect(history3[2].lastDeliveredKeyIndex).to.eq(3); const emitTx4 = await oracle.emitExitEvents(exitRequest, 2); @@ -295,9 +286,9 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { timestamp, ); - const history4 = await oracle.getDeliveryHistory(exitRequestHash); - expect(history4.length).to.eq(4); - expect(history4[3].lastDeliveredKeyIndex).to.eq(4); + // const history4 = await oracle.getDeliveryHistory(exitRequestHash); + // expect(history4.length).to.eq(4); + // expect(history4[3].lastDeliveredKeyIndex).to.eq(4); await expect(oracle.emitExitEvents(exitRequest, 2)).to.be.revertedWithCustomError( oracle, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index 2956525b23..55e7a914b8 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -16,7 +16,7 @@ const PUBKEYS = [ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ]; -describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { +describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; @@ -24,15 +24,14 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { let authorizedEntity: HardhatEthersSigner; let stranger: HardhatEthersSigner; - let validatorExitData: ValidatorExitData; + let exitData: DirectExitData; const LAST_PROCESSING_REF_SLOT = 1; - interface ValidatorExitData { + interface DirectExitData { stakingModuleId: number; nodeOperatorId: number; - validatorIndex: number; - validatorPubkey: string; + validatorsPubkeys: string; } const deploy = async () => { @@ -58,16 +57,18 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { }); it("Should revert without DIRECT_EXIT_HASH_ROLE role", async () => { - validatorExitData = { + const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; + const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); + + exitData = { stakingModuleId: 1, nodeOperatorId: 0, - validatorIndex: 0, - validatorPubkey: PUBKEYS[0], + validatorsPubkeys: "0x" + concatenatedPubKeys }; await expect( - oracle.connect(stranger).triggerExitsDirectly(validatorExitData, { - value: 2, + oracle.connect(stranger).triggerExitsDirectly(exitData, { + value: 4, }), ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_HASH_ROLE()); }); @@ -78,29 +79,28 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { await oracle.grantRole(role, authorizedEntity); await expect( - oracle.connect(authorizedEntity).triggerExitsDirectly(validatorExitData, { - value: 0, + oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + value: 2, }), ) .to.be.revertedWithCustomError(oracle, "InsufficientPayment") - .withArgs(1, 1, 0); + .withArgs(1, 3, 2); }); it("Emit ValidatorExit event and should trigger withdrawals", async () => { - const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(validatorExitData, { - value: 2, + const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + value: 4, }); const timestamp = await oracle.getTime(); - await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(PUBKEYS[0]); + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); await expect(tx) - .to.emit(oracle, "ValidatorExitRequest") + .to.emit(oracle, "DirectExitRequest") .withArgs( - validatorExitData.stakingModuleId, - validatorExitData.nodeOperatorId, - validatorExitData.validatorIndex, - validatorExitData.validatorPubkey, + exitData.stakingModuleId, + exitData.nodeOperatorId, + exitData.validatorsPubkeys, timestamp, ); }); From 14ec02813295a85ee8152370d9783b054520ec7c Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 15 Apr 2025 19:57:57 +0400 Subject: [PATCH 077/405] fix: request count computation --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 496b25275e..ce4551e906 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -65,7 +65,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorsPubkeys, uint256 timestamp ); - struct RequestStatus { // Total items count in report (by default type(uint32).max, update on first report delivery) uint256 totalItemsCount; @@ -258,15 +257,18 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InvalidPubkeysArray(); } - if (msg.value < withdrawalFee * (exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH )) { - revert InsufficientPayment(withdrawalFee,(exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH ), msg.value); + // TODO: maybe add requestCount in DirectExitData + uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; + + if (msg.value < withdrawalFee * requestsCount ) { + revert InsufficientPayment(withdrawalFee, requestsCount , msg.value); } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * (exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH )}(exitData.validatorsPubkeys); + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}(exitData.validatorsPubkeys); emit DirectExitRequest(exitData.stakingModuleId, exitData.nodeOperatorId, exitData.validatorsPubkeys, _getTimestamp()); - uint256 refund = msg.value - withdrawalFee * (exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH ); + uint256 refund = msg.value - withdrawalFee * requestsCount; if (refund > 0) { (bool success, ) = msg.sender.call{value: refund}(""); From 188852036a99522a5f599ea65f5ed72587660374 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 15 Apr 2025 19:07:11 +0200 Subject: [PATCH 078/405] wip: temporary NOR implementation for TW --- .../0.4.24/nos/NodeOperatorExitManager.sol | 187 +------------ .../0.4.24/nos/NodeOperatorsRegistry.sol | 48 +++- contracts/0.8.9/StakingRouter.sol | 12 +- contracts/0.8.9/interfaces/IStakingModule.sol | 2 +- test/0.4.24/nor/nor.exit.manager.test.ts | 246 ++++++++++++++++++ .../0.4.24/nor/nor.initialize.upgrade.test.ts | 2 +- .../StakingModule__MockForStakingRouter.sol | 20 +- .../stakingRouter.module-sync.test.ts | 37 +-- 8 files changed, 322 insertions(+), 232 deletions(-) create mode 100644 test/0.4.24/nor/nor.exit.manager.test.ts diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol index 2ea962b253..7fab102dfa 100644 --- a/contracts/0.4.24/nos/NodeOperatorExitManager.sol +++ b/contracts/0.4.24/nos/NodeOperatorExitManager.sol @@ -3,18 +3,12 @@ pragma solidity 0.4.24; -import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; -import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; - /** * @title NodeOperatorExitManager - * @notice Base contract for handling triggerable withdrawals and penalties for validators + * @notice Mock version: only event interfaces and signatures, logic removed */ contract NodeOperatorExitManager { - using SafeMath for uint256; - using UnstructuredStorage for bytes32; - // Events event ValidatorExitStatusUpdated( uint256 indexed nodeOperatorId, bytes publicKey, @@ -30,11 +24,6 @@ contract NodeOperatorExitManager { event PenaltyApplied(uint256 indexed nodeOperatorId, bytes publicKey, uint256 penaltyAmount, string penaltyType); event ExitDeadlineThresholdChanged(uint256 threshold); - // Storage positions - bytes32 internal constant EXIT_DEADLINE_THRESHOLD_POSITION = - keccak256("lido.NodeOperatorExitManager.exitDeadlineThreshold"); - - // Struct to store exit-related data for each validator struct ValidatorExitRecord { uint256 eligibleToExitInSec; uint256 penalizedFee; @@ -44,81 +33,27 @@ contract NodeOperatorExitManager { bool isExited; } - // Mapping from operator ID to mapping from validator public key hash to exit record - mapping(uint256 => mapping(bytes32 => ValidatorExitRecord)) internal validatorExitRecords; - - // Mapping to store all validator key hashes for each operator - mapping(uint256 => bytes32[]) internal operatorWatchableValidatorKeys; - - /** - * @notice Initialize the contract with a default exit deadline threshold - * @param _getExitDeadlineThreshold The number of seconds after which a validator is considered late - */ function _initializeNodeOperatorExitManager(uint256 _getExitDeadlineThreshold) internal { - EXIT_DEADLINE_THRESHOLD_POSITION.setStorageUint256(_getExitDeadlineThreshold); emit ExitDeadlineThresholdChanged(_getExitDeadlineThreshold); } - /** - * @notice Set the exit deadline threshold - * @param _threshold New threshold in seconds - */ function _setExitDeadlineThreshold(uint256 _threshold) internal { - EXIT_DEADLINE_THRESHOLD_POSITION.setStorageUint256(_threshold); emit ExitDeadlineThresholdChanged(_threshold); } - /** - * @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window - * @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered - * @param _proofSlotTimestamp The timestamp when the validator was last known to be in an active ongoing state - * @param _publicKey The public key of the validator being reported - * @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit - */ function _handleActiveValidatorsExitingStatus( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes _publicKey, uint256 _eligibleToExitInSec ) internal { - require(_eligibleToExitInSec >= _getExitDeadlineThreshold(), "INVALID_EXIT_TIME"); + require(_eligibleToExitInSec >= 0, "INVALID_EXIT_TIME"); // placeholder check require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - // Hash the public key to use as a mapping key - bytes32 publicKeyHash = keccak256(_publicKey); - - // Track this validator key if it's new - if (validatorExitRecords[_nodeOperatorId][publicKeyHash].lastUpdatedTimestamp == 0) { - operatorWatchableValidatorKeys[_nodeOperatorId].push(publicKeyHash); - } - - // Get or initialize the validator exit record - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][publicKeyHash]; - - // Update the record with the new data - record.eligibleToExitInSec = _eligibleToExitInSec; - record.lastUpdatedTimestamp = _proofSlotTimestamp; - - // Calculate penalty if the validator has exceeded the exit deadline - if (record.penalizedFee == 0) { - uint256 penaltyAmount = _getPenalty(); - - // Add to the penalized fee - record.penalizedFee = record.penalizedFee.add(penaltyAmount); - - emit PenaltyApplied(_nodeOperatorId, _publicKey, penaltyAmount, "EXCESS_EXIT_TIME"); - } - + emit PenaltyApplied(_nodeOperatorId, _publicKey, 1 ether, "EXCESS_EXIT_TIME"); emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } - /** - * @notice Handles the triggerable exit event for a validator - * @param _nodeOperatorId The ID of the node operator - * @param _publicKey The public key of the validator being reported - * @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the EL - * @param _exitType The type of exit being performed - */ function _onTriggerableExit( uint256 _nodeOperatorId, bytes _publicKey, @@ -127,131 +62,23 @@ contract NodeOperatorExitManager { ) internal { require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - // Hash the public key to use as a mapping key - bytes32 _publicKeyHash = keccak256(_publicKey); - - // Get or initialize the validator exit record - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; - require(record.lastUpdatedTimestamp > 0, "VALIDATOR_RECORD_NOT_FOUND"); - - // Set the triggerable exit fee - // TODO: validation? - record.triggerableExitFee = _withdrawalRequestPaidFee; - emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } - /** - * @notice Returns the number of seconds after which a validator is considered late - * @return The exit deadline threshold in seconds - */ function _getExitDeadlineThreshold() public view returns (uint256) { - return EXIT_DEADLINE_THRESHOLD_POSITION.getStorageUint256(); + return 60 * 60 * 24 * 2; // 2 days } - /** - * @notice Determines whether a validator's exit status should be updated - * @param _eligibleToExitInSec The number of seconds the validator was eligible to exit - * @return bool Returns true if the contract should receive the updated status - */ function _shouldValidatorBePenalized( - uint256, // _nodeOperatorId, - uint256, // _proofSlotTimestamp, - bytes, // _publicKey, + uint256, // _nodeOperatorId + uint256, // _proofSlotTimestamp + bytes, // _publicKey uint256 _eligibleToExitInSec ) internal view returns (bool) { - // If the validator has exceeded the exit deadline, it should be penalized return _eligibleToExitInSec >= _getExitDeadlineThreshold(); } - /** - * @notice Helper function to calculate penalty based on excess time - * @return Penalty amount in stETH - */ function _getPenalty() internal pure returns (uint256) { - // TODO: get the penalty rate from analytics team return 1 ether; } - - /** - * @notice Apply penalties to an operator's rewards using public key hash - * @param _nodeOperatorId The ID of the node operator - * @param _publicKeyHash Hash of the validator's public key - * @param _sharesInStETH Amount of shares being distributed to the operator in stETH - * @return Adjusted shares after penalties - */ - function _applyPenaltiesByHash( - uint256 _nodeOperatorId, - bytes32 _publicKeyHash, - uint256 _sharesInStETH - ) internal returns (uint256, bool) { - ValidatorExitRecord storage record = validatorExitRecords[_nodeOperatorId][_publicKeyHash]; - - if (record.lastUpdatedTimestamp == 0) { - return (_sharesInStETH, false); - } - - uint256 remainingSharesInStETH = _sharesInStETH; - - if (record.penalizedFee > 0) { - if (record.penalizedFee > remainingSharesInStETH) { - record.penalizedFee = record.penalizedFee.sub(remainingSharesInStETH); - remainingSharesInStETH = 0; - } else { - remainingSharesInStETH = remainingSharesInStETH.sub(record.penalizedFee); - record.penalizedFee = 0; - record.isPenalized = true; - } - } - - if (remainingSharesInStETH > 0 && record.triggerableExitFee > 0) { - if (record.triggerableExitFee > remainingSharesInStETH) { - record.triggerableExitFee = record.triggerableExitFee.sub(remainingSharesInStETH); - remainingSharesInStETH = 0; - } else { - remainingSharesInStETH = remainingSharesInStETH.sub(record.triggerableExitFee); - record.triggerableExitFee = 0; - record.isExited = true; - } - } - - bool completed = record.isPenalized && record.isExited; - return (remainingSharesInStETH, completed); - } - - /** - * @notice Apply penalties to all validators of an operator - * @param _nodeOperatorId The ID of the node operator - * @param _sharesInStETH Amount of shares being distributed to the operator - * @return Adjusted shares after penalties - */ - function _applyAllPenalties(uint256 _nodeOperatorId, uint256 _sharesInStETH) internal returns (uint256) { - uint256 remainingSharesInStETH = _sharesInStETH; - bytes32[] storage validatorKeys = operatorWatchableValidatorKeys[_nodeOperatorId]; - uint256 i = 0; - - while (i < validatorKeys.length) { - bytes32 key = validatorKeys[i]; - - (uint256 updatedShares, bool completed) = _applyPenaltiesByHash( - _nodeOperatorId, - key, - remainingSharesInStETH - ); - remainingSharesInStETH = updatedShares; - - if (completed) { - validatorKeys[i] = validatorKeys[validatorKeys.length - 1]; - validatorKeys.length--; - } else { - i++; - } - - if (remainingSharesInStETH == 0) { - break; - } - } - - return remainingSharesInStETH; - } } diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 1e5d30f3a9..df4d13c010 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -8,6 +8,8 @@ import {AragonApp} from "@aragon/os/contracts/apps/AragonApp.sol"; import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; +import {NodeOperatorExitManager} from "./NodeOperatorExitManager.sol"; + import {Math256} from "../../common/lib/Math256.sol"; import {MinFirstAllocationStrategy} from "../../common/lib/MinFirstAllocationStrategy.sol"; import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; @@ -27,7 +29,7 @@ interface IStETH { /// @dev Must implement the full version of IStakingModule interface, not only the one declared locally. /// It's also responsible for distributing rewards to node operators. /// NOTE: the code below assumes moderate amount of node operators, i.e. up to `MAX_NODE_OPERATORS_COUNT`. -contract NodeOperatorsRegistry is AragonApp, Versioned { +contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager { using SafeMath for uint256; using UnstructuredStorage for bytes32; using SigningKeys for bytes32; @@ -222,6 +224,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Initializations for v2 --> v3 _initialize_v3(); + _initialize_v4(); + initialized(); } @@ -311,6 +315,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.Distributed); } + function _initialize_v4() internal { + _setContractVersion(4); + // TODO: after devnet-1 set correct value + _initializeNodeOperatorExitManager(60 * 60 * 24 * 2); + } + /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators /// @param _name Human-readable name /// @param _rewardAddress Ethereum 1 address which receives stETH rewards for this operator @@ -1184,6 +1194,41 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } + function handleActiveValidatorsExitingStatus( + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes _publicKey, + uint256 _eligibleToExitInSec + ) external { + _auth(STAKING_ROUTER_ROLE); + return _handleActiveValidatorsExitingStatus( + _nodeOperatorId, _proofSlotTimestamp, _publicKey, _eligibleToExitInSec + ); + } + + function onTriggerableExit( + uint256 _nodeOperatorId, + bytes _publicKey, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType + ) external { + _auth(STAKING_ROUTER_ROLE); + return _onTriggerableExit(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); + } + + function exitDeadlineThreshold(uint256 /* _nodeOperatorId */) external view returns (uint256) { + return _getExitDeadlineThreshold(); + } + + function shouldValidatorBePenalized( + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes _publicKey, + uint256 _eligibleToExitInSec + ) external view returns (bool) { + return _shouldValidatorBePenalized(_nodeOperatorId, _proofSlotTimestamp, _publicKey, _eligibleToExitInSec); + } + function _removeUnusedSigningKeys(uint256 _nodeOperatorId, uint256 _fromIndex, uint256 _keysCount) internal { _onlyExistedNodeOperator(_nodeOperatorId); _onlyNodeOperatorManager(msg.sender, _nodeOperatorId); @@ -1435,6 +1480,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { toBurn = toBurn.add(shares[idx]); emit NodeOperatorPenalized(recipients[idx], shares[idx]); } + // TODO: apply penalty to the operator stETH.transferShares(recipients[idx], shares[idx]); distributed = distributed.add(shares[idx]); emit RewardsDistributed(recipients[idx], shares[idx]); diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 4e8878168c..eb17181509 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -455,7 +455,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// 4. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the stuck /// and exited validator counts per node operator for the current reporting frame, the oracle calls /// `StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished` which, in turn, calls - /// `IStakingModule.onExitedValidatorsCountsUpdated` on all modules. + /// `IStakingModule.onExitedAndStuckValidatorsCountsUpdated` on all modules. /// /// @dev The function is restricted to the `REPORT_EXITED_VALIDATORS_ROLE` role. function updateExitedValidatorsCountByStakingModule( @@ -554,7 +554,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// /// @param _stakingModuleId Id of the staking module. /// @param _nodeOperatorId Id of the node operator. - /// @param _triggerUpdateFinish Whether to call `onExitedValidatorsCountsUpdated` on the module + /// @param _triggerUpdateFinish Whether to call `onExitedAndStuckValidatorsCountsUpdated` on the module /// after applying the corrections. /// @param _correction See the docs for the `ValidatorsCountsCorrection` struct. /// @@ -621,7 +621,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ); } - stakingModule.onExitedValidatorsCountsUpdated(); + stakingModule.onExitedAndStuckValidatorsCountsUpdated(); } } @@ -649,13 +649,13 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version (uint256 exitedValidatorsCount, , ) = _getStakingModuleSummary(moduleContract); if (exitedValidatorsCount == stakingModule.exitedValidatorsCount) { // oracle finished updating exited validators for all node ops - try moduleContract.onExitedValidatorsCountsUpdated() {} + try moduleContract.onExitedAndStuckValidatorsCountsUpdated() {} catch (bytes memory lowLevelRevertData) { /// @dev This check is required to prevent incorrect gas estimation of the method. /// Without it, Ethereum nodes that use binary search for gas estimation may - /// return an invalid value when the onExitedValidatorsCountsUpdated() + /// return an invalid value when the onExitedAndStuckValidatorsCountsUpdated() /// reverts because of the "out of gas" error. Here we assume that the - /// onExitedValidatorsCountsUpdated() method doesn't have reverts with + /// onExitedAndStuckValidatorsCountsUpdated() method doesn't have reverts with /// empty error data except "out of gas". if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); emit ExitedAndStuckValidatorsCountsUpdateFailed( diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index cbe5fd509a..1fc079d7e7 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -206,7 +206,7 @@ interface IStakingModule { /// /// @dev IMPORTANT: this method SHOULD revert with empty error data ONLY because of "out of gas". /// Details about error data: https://docs.soliditylang.org/en/v0.8.9/control-structures.html#error-handling-assert-require-revert-and-exceptions - function onExitedValidatorsCountsUpdated() external; + function onExitedAndStuckValidatorsCountsUpdated() external; /// @notice Called by StakingRouter when withdrawal credentials are changed. /// @dev This method MUST discard all StakingModule's unused deposit data cause they become diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts new file mode 100644 index 0000000000..fb7ba4b85f --- /dev/null +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -0,0 +1,246 @@ +import { expect } from "chai"; +import { encodeBytes32String } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { + ACL, + Kernel, + Lido, + LidoLocator, + LidoLocator__factory, + MinFirstAllocationStrategy__factory, + NodeOperatorsRegistry__Harness, + NodeOperatorsRegistry__Harness__factory, +} from "typechain-types"; +import { NodeOperatorsRegistryLibraryAddresses } from "typechain-types/factories/contracts/0.4.24/nos/NodeOperatorsRegistry.sol/NodeOperatorsRegistry__factory"; + +import { addNodeOperator, certainAddress, NodeOperatorConfig, RewardDistributionState } from "lib"; + +import { addAragonApp, deployLidoDao } from "test/deploy"; +import { Snapshot } from "test/suite"; + +describe("NodeOperatorsRegistry.sol:ExitManager", () => { + let deployer: HardhatEthersSigner; + let user: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + let nodeOperatorsManager: HardhatEthersSigner; + let signingKeysManager: HardhatEthersSigner; + let stakingRouter: HardhatEthersSigner; + let lido: Lido; + let dao: Kernel; + let acl: ACL; + let locator: LidoLocator; + + let impl: NodeOperatorsRegistry__Harness; + let nor: NodeOperatorsRegistry__Harness; + + let originalState: string; + + const firstNodeOperatorId = 0; + + const NODE_OPERATORS: NodeOperatorConfig[] = [ + { + name: "testOperator", + rewardAddress: certainAddress("node-operator-1"), + totalSigningKeysCount: 10n, + depositedSigningKeysCount: 5n, + exitedSigningKeysCount: 1n, + vettedSigningKeysCount: 6n, + stuckValidatorsCount: 0n, + refundedValidatorsCount: 0n, + stuckPenaltyEndAt: 0n, + } + ]; + + const moduleType = encodeBytes32String("curated-onchain-v1"); + const penaltyDelay = 86400n; + + const testPublicKey = "0x123456"; + const eligibleToExitInSec = 172800n; // 2 days + const proofSlotTimestamp = 1234567890n; + const withdrawalRequestPaidFee = 100000n; + const exitType = 1n; + + before(async () => { + [deployer, user, stakingRouter, nodeOperatorsManager, signingKeysManager, stranger] = + await ethers.getSigners(); + + ({ lido, dao, acl } = await deployLidoDao({ + rootAccount: deployer, + initialized: true, + locatorConfig: { + stakingRouter, + }, + })); + + const allocLib = await new MinFirstAllocationStrategy__factory(deployer).deploy(); + const allocLibAddr: NodeOperatorsRegistryLibraryAddresses = { + ["__contracts/common/lib/MinFirstAllocat__"]: await allocLib.getAddress(), + }; + + impl = await new NodeOperatorsRegistry__Harness__factory(allocLibAddr, deployer).deploy(); + const appProxy = await addAragonApp({ + dao, + name: "node-operators-registry", + impl, + rootAccount: deployer, + }); + + nor = NodeOperatorsRegistry__Harness__factory.connect(appProxy, deployer); + + await acl.createPermission(user, lido, await lido.RESUME_ROLE(), deployer); + + await acl.createPermission(stakingRouter, nor, await nor.STAKING_ROUTER_ROLE(), deployer); + await acl.createPermission(signingKeysManager, nor, await nor.MANAGE_SIGNING_KEYS(), deployer); + await acl.createPermission(nodeOperatorsManager, nor, await nor.MANAGE_NODE_OPERATOR_ROLE(), deployer); + + // grant role to nor itself cause it uses solidity's call method to itself + // inside the harness__requestValidatorsKeysForDeposits() method + await acl.grantPermission(nor, nor, await nor.STAKING_ROUTER_ROLE()); + + locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); + + // Initialize the nor's proxy + await expect(nor.initialize(locator, moduleType, penaltyDelay)) + .to.emit(nor, "RewardDistributionStateChanged") + .withArgs(RewardDistributionState.Distributed); + + // Add a node operator for testing + expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.be.equal( + firstNodeOperatorId, + ); + + nor = nor.connect(user); + originalState = await Snapshot.take(); + }); + + afterEach(async () => (originalState = await Snapshot.refresh(originalState))); + + context("handleActiveValidatorsExitingStatus", () => { + it("reverts when called by sender without STAKING_ROUTER_ROLE", async () => { + expect(await acl["hasPermission(address,address,bytes32)"](stranger, nor, await nor.STAKING_ROUTER_ROLE())).to.be + .false; + + await expect( + nor.connect(stranger).handleActiveValidatorsExitingStatus( + firstNodeOperatorId, + proofSlotTimestamp, + testPublicKey, + eligibleToExitInSec + ) + ).to.be.revertedWith("APP_AUTH_FAILED"); + }); + + it("emits events when called by sender with STAKING_ROUTER_ROLE", async () => { + expect(await acl["hasPermission(address,address,bytes32)"](stakingRouter, nor, await nor.STAKING_ROUTER_ROLE())) + .to.be.true; + + await expect( + nor.connect(stakingRouter).handleActiveValidatorsExitingStatus( + firstNodeOperatorId, + proofSlotTimestamp, + testPublicKey, + eligibleToExitInSec + ) + ) + .to.emit(nor, "PenaltyApplied") + .withArgs(firstNodeOperatorId, testPublicKey, ethers.parseEther("1"), "EXCESS_EXIT_TIME") + .and.to.emit(nor, "ValidatorExitStatusUpdated") + .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); + }); + + it("reverts when public key is empty", async () => { + await expect( + nor.connect(stakingRouter).handleActiveValidatorsExitingStatus( + firstNodeOperatorId, + proofSlotTimestamp, + "0x", + eligibleToExitInSec + ) + ).to.be.revertedWith("INVALID_PUBLIC_KEY"); + }); + }); + + context("onTriggerableExit", () => { + it("reverts when called by sender without STAKING_ROUTER_ROLE", async () => { + expect(await acl["hasPermission(address,address,bytes32)"](stranger, nor, await nor.STAKING_ROUTER_ROLE())).to.be + .false; + + await expect( + nor.connect(stranger).onTriggerableExit( + firstNodeOperatorId, + testPublicKey, + withdrawalRequestPaidFee, + exitType + ) + ).to.be.revertedWith("APP_AUTH_FAILED"); + }); + + it("emits an event when called by sender with STAKING_ROUTER_ROLE", async () => { + expect(await acl["hasPermission(address,address,bytes32)"](stakingRouter, nor, await nor.STAKING_ROUTER_ROLE())) + .to.be.true; + + await expect( + nor.connect(stakingRouter).onTriggerableExit( + firstNodeOperatorId, + testPublicKey, + withdrawalRequestPaidFee, + exitType + ) + ) + .to.emit(nor, "TriggerableExitFeeSet") + .withArgs(firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, exitType); + }); + + it("reverts when public key is empty", async () => { + await expect( + nor.connect(stakingRouter).onTriggerableExit( + firstNodeOperatorId, + "0x", + withdrawalRequestPaidFee, + exitType + ) + ).to.be.revertedWith("INVALID_PUBLIC_KEY"); + }); + }); + + context("exitDeadlineThreshold", () => { + it("returns the expected value", async () => { + const threshold = await nor.exitDeadlineThreshold(firstNodeOperatorId); + expect(threshold).to.equal(172800n); // 2 days in seconds + }); + }); + + context("shouldValidatorBePenalized", () => { + it("returns true when eligible to exit time exceeds the threshold", async () => { + const shouldPenalize = await nor.shouldValidatorBePenalized( + firstNodeOperatorId, + proofSlotTimestamp, + testPublicKey, + 172800n // Equal to the threshold + ); + expect(shouldPenalize).to.be.true; + + const shouldPenalizeMore = await nor.shouldValidatorBePenalized( + firstNodeOperatorId, + proofSlotTimestamp, + testPublicKey, + 172801n // Greater than the threshold + ); + expect(shouldPenalizeMore).to.be.true; + }); + + it("returns false when eligible to exit time is less than the threshold", async () => { + const shouldPenalize = await nor.shouldValidatorBePenalized( + firstNodeOperatorId, + proofSlotTimestamp, + testPublicKey, + 172799n // Less than the threshold + ); + expect(shouldPenalize).to.be.false; + }); + }); +}); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 02d71a306e..2e802b4475 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -201,7 +201,7 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { expect(await nor.getInitializationBlock()).to.equal(latestBlock + 1n); expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(MaxUint256); expect(await nor.getStuckPenaltyDelay()).to.equal(86400n); - expect(await nor.getContractVersion()).to.equal(3); + expect(await nor.getContractVersion()).to.equal(4); expect(await nor.getType()).to.equal(moduleType); }); }); diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index 28cbd74ece..66a1c41696 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -226,24 +226,24 @@ contract StakingModule__MockForStakingRouter is IStakingModule { signatures = new bytes(96 * _depositsCount); } - event Mock__onExitedValidatorsCountsUpdated(); + event Mock__onExitedAndStuckValidatorsCountsUpdated(); - bool private onExitedValidatorsCountsUpdatedShouldRevert = false; - bool private onExitedValidatorsCountsUpdatedShouldRunOutGas = false; + bool private onExitedAndStuckValidatorsCountsUpdatedShouldRevert = false; + bool private onExitedAndStuckValidatorsCountsUpdatedShouldRunOutGas = false; - function onExitedValidatorsCountsUpdated() external { - require(!onExitedValidatorsCountsUpdatedShouldRevert, "revert reason"); + function onExitedAndStuckValidatorsCountsUpdated() external { + require(!onExitedAndStuckValidatorsCountsUpdatedShouldRevert, "revert reason"); - if (onExitedValidatorsCountsUpdatedShouldRunOutGas) { + if (onExitedAndStuckValidatorsCountsUpdatedShouldRunOutGas) { revert(); } - emit Mock__onExitedValidatorsCountsUpdated(); + emit Mock__onExitedAndStuckValidatorsCountsUpdated(); } - function mock__onExitedValidatorsCountsUpdated(bool shouldRevert, bool shouldRunOutGas) external { - onExitedValidatorsCountsUpdatedShouldRevert = shouldRevert; - onExitedValidatorsCountsUpdatedShouldRunOutGas = shouldRunOutGas; + function mock__onExitedAndStuckValidatorsCountsUpdated(bool shouldRevert, bool shouldRunOutGas) external { + onExitedAndStuckValidatorsCountsUpdatedShouldRevert = shouldRevert; + onExitedAndStuckValidatorsCountsUpdatedShouldRunOutGas = shouldRunOutGas; } event Mock__WithdrawalCredentialsChanged(); diff --git a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts index 0f19594f56..09946a41f7 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts @@ -126,7 +126,7 @@ describe("StakingRouter.sol:module-sync", () => { const nodeOperatorSummary: Parameters = [ 1, // targetLimitMode 100n, // targetValidatorsCount - 1n, // stuckValidatorsCount + 0n, // stuckValidatorsCount 5n, // refundedValidatorsCount 0n, // stuckPenaltyEndTimestamp 50, // totalExitedValidators @@ -651,10 +651,8 @@ describe("StakingRouter.sol:module-sync", () => { const correction: StakingRouter.ValidatorsCountsCorrectionStruct = { currentModuleExitedValidatorsCount: moduleSummary.totalExitedValidators, currentNodeOperatorExitedValidatorsCount: operatorSummary.totalExitedValidators, - currentNodeOperatorStuckValidatorsCount: operatorSummary.stuckValidatorsCount, newModuleExitedValidatorsCount: moduleSummary.totalExitedValidators, newNodeOperatorExitedValidatorsCount: operatorSummary.totalExitedValidators + 1n, - newNodeOperatorStuckValidatorsCount: operatorSummary.stuckValidatorsCount + 1n, }; beforeEach(async () => { @@ -694,11 +692,7 @@ describe("StakingRouter.sol:module-sync", () => { }), ) .to.be.revertedWithCustomError(stakingRouter, "UnexpectedCurrentValidatorsCount") - .withArgs( - correction.currentModuleExitedValidatorsCount, - correction.currentNodeOperatorExitedValidatorsCount, - correction.currentNodeOperatorStuckValidatorsCount, - ); + .withArgs(correction.currentModuleExitedValidatorsCount, correction.currentNodeOperatorExitedValidatorsCount); }); it("Reverts if the number of exited validators of the operator does not match what is stored on the contract", async () => { @@ -709,26 +703,7 @@ describe("StakingRouter.sol:module-sync", () => { }), ) .to.be.revertedWithCustomError(stakingRouter, "UnexpectedCurrentValidatorsCount") - .withArgs( - correction.currentModuleExitedValidatorsCount, - correction.currentNodeOperatorExitedValidatorsCount, - correction.currentNodeOperatorStuckValidatorsCount, - ); - }); - - it("Reverts if the number of stuck validators of the operator does not match what is stored on the contract", async () => { - await expect( - stakingRouter.unsafeSetExitedValidatorsCount(moduleId, nodeOperatorId, true, { - ...correction, - currentNodeOperatorStuckValidatorsCount: 1n, - }), - ) - .to.be.revertedWithCustomError(stakingRouter, "UnexpectedCurrentValidatorsCount") - .withArgs( - correction.currentModuleExitedValidatorsCount, - correction.currentNodeOperatorExitedValidatorsCount, - correction.currentNodeOperatorStuckValidatorsCount, - ); + .withArgs(correction.currentModuleExitedValidatorsCount, correction.currentNodeOperatorExitedValidatorsCount); }); it("Reverts if the total exited validators exceed the module's deposited validators", async () => { @@ -760,11 +735,7 @@ describe("StakingRouter.sol:module-sync", () => { it("Update unsafely the number of exited validators on the staking module with finalization hook triggering", async () => { await expect(stakingRouter.unsafeSetExitedValidatorsCount(moduleId, nodeOperatorId, true, correction)) .to.be.emit(stakingModule, "Mock__ValidatorsCountUnsafelyUpdated") - .withArgs( - moduleId, - correction.newNodeOperatorExitedValidatorsCount, - correction.newNodeOperatorStuckValidatorsCount, - ) + .withArgs(moduleId, correction.newNodeOperatorExitedValidatorsCount) .and.to.emit(stakingModule, "Mock__onExitedAndStuckValidatorsCountsUpdated"); }); From fef4f6dcc0339aceb83717bab78a62a969b9cc17 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 15 Apr 2025 20:04:13 +0200 Subject: [PATCH 079/405] refactor: reduce NOR size --- .../0.4.24/nos/NodeOperatorExitManager.sol | 84 ------------------- .../0.4.24/nos/NodeOperatorsRegistry.sol | 47 +++++++++-- 2 files changed, 38 insertions(+), 93 deletions(-) delete mode 100644 contracts/0.4.24/nos/NodeOperatorExitManager.sol diff --git a/contracts/0.4.24/nos/NodeOperatorExitManager.sol b/contracts/0.4.24/nos/NodeOperatorExitManager.sol deleted file mode 100644 index 7fab102dfa..0000000000 --- a/contracts/0.4.24/nos/NodeOperatorExitManager.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.4.24; - -/** - * @title NodeOperatorExitManager - * @notice Mock version: only event interfaces and signatures, logic removed - */ -contract NodeOperatorExitManager { - - event ValidatorExitStatusUpdated( - uint256 indexed nodeOperatorId, - bytes publicKey, - uint256 eligibleToExitInSec, - uint256 proofSlotTimestamp - ); - event TriggerableExitFeeSet( - uint256 indexed nodeOperatorId, - bytes publicKey, - uint256 withdrawalRequestPaidFee, - uint256 exitType - ); - event PenaltyApplied(uint256 indexed nodeOperatorId, bytes publicKey, uint256 penaltyAmount, string penaltyType); - event ExitDeadlineThresholdChanged(uint256 threshold); - - struct ValidatorExitRecord { - uint256 eligibleToExitInSec; - uint256 penalizedFee; - uint256 triggerableExitFee; - uint256 lastUpdatedTimestamp; - bool isPenalized; - bool isExited; - } - - function _initializeNodeOperatorExitManager(uint256 _getExitDeadlineThreshold) internal { - emit ExitDeadlineThresholdChanged(_getExitDeadlineThreshold); - } - - function _setExitDeadlineThreshold(uint256 _threshold) internal { - emit ExitDeadlineThresholdChanged(_threshold); - } - - function _handleActiveValidatorsExitingStatus( - uint256 _nodeOperatorId, - uint256 _proofSlotTimestamp, - bytes _publicKey, - uint256 _eligibleToExitInSec - ) internal { - require(_eligibleToExitInSec >= 0, "INVALID_EXIT_TIME"); // placeholder check - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - - emit PenaltyApplied(_nodeOperatorId, _publicKey, 1 ether, "EXCESS_EXIT_TIME"); - emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); - } - - function _onTriggerableExit( - uint256 _nodeOperatorId, - bytes _publicKey, - uint256 _withdrawalRequestPaidFee, - uint256 _exitType - ) internal { - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - - emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); - } - - function _getExitDeadlineThreshold() public view returns (uint256) { - return 60 * 60 * 24 * 2; // 2 days - } - - function _shouldValidatorBePenalized( - uint256, // _nodeOperatorId - uint256, // _proofSlotTimestamp - bytes, // _publicKey - uint256 _eligibleToExitInSec - ) internal view returns (bool) { - return _eligibleToExitInSec >= _getExitDeadlineThreshold(); - } - - function _getPenalty() internal pure returns (uint256) { - return 1 ether; - } -} diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index df4d13c010..7985c715b6 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -8,8 +8,6 @@ import {AragonApp} from "@aragon/os/contracts/apps/AragonApp.sol"; import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; -import {NodeOperatorExitManager} from "./NodeOperatorExitManager.sol"; - import {Math256} from "../../common/lib/Math256.sol"; import {MinFirstAllocationStrategy} from "../../common/lib/MinFirstAllocationStrategy.sol"; import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; @@ -29,7 +27,7 @@ interface IStETH { /// @dev Must implement the full version of IStakingModule interface, not only the one declared locally. /// It's also responsible for distributing rewards to node operators. /// NOTE: the code below assumes moderate amount of node operators, i.e. up to `MAX_NODE_OPERATORS_COUNT`. -contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager { +contract NodeOperatorsRegistry is AragonApp, Versioned { using SafeMath for uint256; using UnstructuredStorage for bytes32; using SigningKeys for bytes32; @@ -65,6 +63,21 @@ contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager event NodeOperatorPenalized(address indexed recipientAddress, uint256 sharesPenalizedAmount); event NodeOperatorPenaltyCleared(uint256 indexed nodeOperatorId); + event ValidatorExitStatusUpdated( + uint256 indexed nodeOperatorId, + bytes publicKey, + uint256 eligibleToExitInSec, + uint256 proofSlotTimestamp + ); + event TriggerableExitFeeSet( + uint256 indexed nodeOperatorId, + bytes publicKey, + uint256 withdrawalRequestPaidFee, + uint256 exitType + ); + event PenaltyApplied(uint256 indexed nodeOperatorId, bytes publicKey, uint256 penaltyAmount, string penaltyType); + event ExitDeadlineThresholdChanged(uint256 threshold); + // Enum to represent the state of the reward distribution process enum RewardDistributionState { TransferredToModule, // New reward portion minted and transferred to the module @@ -317,8 +330,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager function _initialize_v4() internal { _setContractVersion(4); - // TODO: after devnet-1 set correct value - _initializeNodeOperatorExitManager(60 * 60 * 24 * 2); + // TODO: after devnet-1 set correct logic } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators @@ -1194,6 +1206,19 @@ contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } + function _getExitDeadlineThreshold() public view returns (uint256) { + return 60 * 60 * 24 * 2; // 2 days + } + + function _shouldValidatorBePenalized( + uint256, // _nodeOperatorId + uint256, // _proofSlotTimestamp + bytes, // _publicKey + uint256 _eligibleToExitInSec + ) internal view returns (bool) { + return _eligibleToExitInSec >= _getExitDeadlineThreshold(); + } + function handleActiveValidatorsExitingStatus( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, @@ -1201,9 +1226,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager uint256 _eligibleToExitInSec ) external { _auth(STAKING_ROUTER_ROLE); - return _handleActiveValidatorsExitingStatus( - _nodeOperatorId, _proofSlotTimestamp, _publicKey, _eligibleToExitInSec - ); + require(_eligibleToExitInSec >= 0, "INVALID_EXIT_TIME"); // placeholder check + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + + emit PenaltyApplied(_nodeOperatorId, _publicKey, 1 ether, "EXCESS_EXIT_TIME"); + emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } function onTriggerableExit( @@ -1213,7 +1240,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned, NodeOperatorExitManager uint256 _exitType ) external { _auth(STAKING_ROUTER_ROLE); - return _onTriggerableExit(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); + require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + + emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } function exitDeadlineThreshold(uint256 /* _nodeOperatorId */) external view returns (uint256) { From 8429eaf33ada2692abe182c9c3abd60c1e60fa11 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 15 Apr 2025 20:26:30 +0200 Subject: [PATCH 080/405] wip: reduce NOR size --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 64 ---- .../0.4.24/nor/nor.initialize.upgrade.test.ts | 316 +----------------- 2 files changed, 1 insertion(+), 379 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 7985c715b6..c5c7c50311 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -242,59 +242,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { initialized(); } - /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once - /// For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md - function finalizeUpgrade_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) external { - require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); - _checkContractVersion(0); - _initialize_v2(_locator, _type, _stuckPenaltyDelay); - - uint256 totalOperators = getNodeOperatorsCount(); - Packed64x4.Packed memory signingKeysStats; - Packed64x4.Packed memory operatorTargetStats; - Packed64x4.Packed memory summarySigningKeysStats = Packed64x4.Packed(0); - uint256 vettedSigningKeysCountBefore; - uint256 totalSigningKeysCount; - uint256 depositedSigningKeysCount; - for (uint256 nodeOperatorId; nodeOperatorId < totalOperators; ++nodeOperatorId) { - signingKeysStats = _loadOperatorSigningKeysStats(nodeOperatorId); - vettedSigningKeysCountBefore = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); - totalSigningKeysCount = signingKeysStats.get(TOTAL_KEYS_COUNT_OFFSET); - depositedSigningKeysCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); - - uint256 vettedSigningKeysCountAfter; - if (!_nodeOperators[nodeOperatorId].active) { - // trim vetted signing keys count when node operator is not active - vettedSigningKeysCountAfter = depositedSigningKeysCount; - } else { - vettedSigningKeysCountAfter = Math256.min( - totalSigningKeysCount, - Math256.max(depositedSigningKeysCount, vettedSigningKeysCountBefore) - ); - } - - if (vettedSigningKeysCountBefore != vettedSigningKeysCountAfter) { - signingKeysStats.set(TOTAL_VETTED_KEYS_COUNT_OFFSET, vettedSigningKeysCountAfter); - _saveOperatorSigningKeysStats(nodeOperatorId, signingKeysStats); - emit VettedSigningKeysCountChanged(nodeOperatorId, vettedSigningKeysCountAfter); - } - - operatorTargetStats = _loadOperatorTargetValidatorsStats(nodeOperatorId); - operatorTargetStats.set(MAX_VALIDATORS_COUNT_OFFSET, vettedSigningKeysCountAfter); - _saveOperatorTargetValidatorsStats(nodeOperatorId, operatorTargetStats); - - summarySigningKeysStats.add(SUMMARY_MAX_VALIDATORS_COUNT_OFFSET, vettedSigningKeysCountAfter); - summarySigningKeysStats.add(SUMMARY_DEPOSITED_KEYS_COUNT_OFFSET, depositedSigningKeysCount); - summarySigningKeysStats.add( - SUMMARY_EXITED_KEYS_COUNT_OFFSET, - signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET) - ); - } - - _saveSummarySigningKeysStats(summarySigningKeysStats); - _increaseValidatorsKeysNonce(); - } - function _initialize_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) internal { _onlyNonZeroAddress(_locator); LIDO_LOCATOR_POSITION.setStorageAddress(_locator); @@ -312,17 +259,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit StakingModuleTypeSet(_type); } - function finalizeUpgrade_v3() external { - require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); - _checkContractVersion(2); - _initialize_v3(); - - // clear deprecated total keys count storage - Packed64x4.Packed memory summarySigningKeysStats = _loadSummarySigningKeysStats(); - summarySigningKeysStats.set(SUMMARY_TOTAL_KEYS_COUNT_OFFSET, 0); - _saveSummarySigningKeysStats(summarySigningKeysStats); - } - function _initialize_v3() internal { _setContractVersion(3); _updateRewardDistributionState(RewardDistributionState.Distributed); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 2e802b4475..88f6def9a5 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -168,18 +168,7 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { ); }); - it("Reverts if has been upgraded to v2 before", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - - await nor.harness__initialize(0n); - await nor.finalizeUpgrade_v2(locator, encodeBytes32String("curated-onchain-v1"), MAX_STUCK_PENALTY_DELAY); - - await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( - "INIT_ALREADY_INITIALIZED", - ); - }); - - it("Makes the contract initialized to v3", async () => { + it("Makes the contract initialized to v4", async () => { const burnerAddress = await locator.burner(); const latestBlock = BigInt(await time.latestBlock()); @@ -205,307 +194,4 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { expect(await nor.getType()).to.equal(moduleType); }); }); - - context("finalizeUpgrade_v2", () => { - let burnerAddress: string; - let preInitState: string; - - beforeEach(async () => { - locator = await deployLidoLocator({ lido: lido }); - burnerAddress = await locator.burner(); - - preInitState = await Snapshot.take(); - await nor.harness__initialize(0n); - }); - - it("Reverts if Locator is zero address", async () => { - await expect(nor.finalizeUpgrade_v2(ZeroAddress, moduleType, 43200n)).to.be.reverted; - }); - - it("Reverts if stuck penalty delay exceeds MAX_STUCK_PENALTY_DELAY", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await expect(nor.finalizeUpgrade_v2(locator, "curated-onchain-v1", MAX_STUCK_PENALTY_DELAY + 1n)); - }); - - it("Reverts if hasn't been initialized yet", async () => { - await Snapshot.restore(preInitState); - - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await expect(nor.finalizeUpgrade_v2(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( - "CONTRACT_NOT_INITIALIZED", - ); - }); - - it("Reverts if already initialized to v3", async () => { - await Snapshot.restore(preInitState); - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), MAX_STUCK_PENALTY_DELAY); - - await expect(nor.finalizeUpgrade_v2(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( - "UNEXPECTED_CONTRACT_VERSION", - ); - }); - - it("Reverts if already upgraded to v2", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await nor.finalizeUpgrade_v2(locator, encodeBytes32String("curated-onchain-v1"), MAX_STUCK_PENALTY_DELAY); - - await expect(nor.finalizeUpgrade_v2(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( - "UNEXPECTED_CONTRACT_VERSION", - ); - }); - - it("Makes the contract upgraded to v2", async () => { - const latestBlock = BigInt(await time.latestBlock()); - - await expect(nor.finalizeUpgrade_v2(locator, moduleType, 86400n)) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV2) - .and.to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(86400n) - .and.to.emit(nor, "LocatorContractSet") - .withArgs(await locator.getAddress()) - .and.to.emit(nor, "StakingModuleTypeSet") - .withArgs(moduleType); - - expect(await nor.getLocator()).to.equal(await locator.getAddress()); - expect(await nor.getInitializationBlock()).to.equal(latestBlock); - expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(MaxUint256); - expect(await nor.getStuckPenaltyDelay()).to.equal(86400n); - expect(await nor.getContractVersion()).to.equal(contractVersionV2); - expect(await nor.getType()).to.equal(moduleType); - }); - - it("Migrates the contract storage from v1 to v2", async () => { - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.equal( - secondNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[thirdNodeOperatorId])).to.equal( - thirdNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[fourthNodeOperatorId])).to.equal( - fourthNodeOperatorId, - ); - - await nor.harness__unsafeResetModuleSummary(); - const resetSummary = await nor.getStakingModuleSummary(); - expect(resetSummary.totalExitedValidators).to.equal(0n); - expect(resetSummary.totalDepositedValidators).to.equal(0n); - expect(resetSummary.depositableValidatorsCount).to.equal(0n); - - await nor.harness__unsafeSetVettedKeys( - firstNodeOperatorId, - NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount - 1n, - ); - await nor.harness__unsafeSetVettedKeys( - secondNodeOperatorId, - NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount + 1n, - ); - await nor.harness__unsafeSetVettedKeys( - thirdNodeOperatorId, - NODE_OPERATORS[thirdNodeOperatorId].totalSigningKeysCount, - ); - - await expect(nor.finalizeUpgrade_v2(locator, moduleType, 86400n)) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV2) - .and.to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(86400n) - .and.to.emit(nor, "LocatorContractSet") - .withArgs(await locator.getAddress()) - .and.to.emit(nor, "StakingModuleTypeSet") - .withArgs(moduleType); - - const summary = await nor.getStakingModuleSummary(); - expect(summary.totalExitedValidators).to.equal(1n + 0n + 0n + 1n); - expect(summary.totalDepositedValidators).to.equal(5n + 7n + 0n + 2n); - expect(summary.depositableValidatorsCount).to.equal(0n + 8n + 0n + 0n); - - const firstNoInfo = await nor.getNodeOperator(firstNodeOperatorId, true); - expect(firstNoInfo.totalVettedValidators).to.equal(NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount); - - const secondNoInfo = await nor.getNodeOperator(secondNodeOperatorId, true); - expect(secondNoInfo.totalVettedValidators).to.equal(NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount); - - const thirdNoInfo = await nor.getNodeOperator(thirdNodeOperatorId, true); - expect(thirdNoInfo.totalVettedValidators).to.equal(NODE_OPERATORS[thirdNodeOperatorId].depositedSigningKeysCount); - - const fourthNoInfo = await nor.getNodeOperator(fourthNodeOperatorId, true); - expect(fourthNoInfo.totalVettedValidators).to.equal(NODE_OPERATORS[fourthNodeOperatorId].vettedSigningKeysCount); - }); - }); - - context("finalizeUpgrade_v3()", () => { - let preInitState: string; - beforeEach(async () => { - locator = await deployLidoLocator({ lido: lido }); - preInitState = await Snapshot.take(); - await nor.harness__initialize(2n); - }); - - it("fails with CONTRACT_NOT_INITIALIZED error when called on implementation", async () => { - await expect(impl.finalizeUpgrade_v3()).to.be.revertedWith("CONTRACT_NOT_INITIALIZED"); - }); - - it("fails with CONTRACT_NOT_INITIALIZED error when nor instance not initialized yet", async () => { - const appProxy = await addAragonApp({ - dao, - name: "new-node-operators-registry", - impl, - rootAccount: deployer, - }); - const registry = await ethers.getContractAt("NodeOperatorsRegistry__Harness", appProxy, deployer); - await expect(registry.finalizeUpgrade_v3()).to.be.revertedWith("CONTRACT_NOT_INITIALIZED"); - }); - - it("sets correct contract version and reward distribution state", async () => { - await expect(nor.finalizeUpgrade_v3()) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV3) - .to.emit(nor, "RewardDistributionStateChanged") - .withArgs(RewardDistributionState.Distributed); - - expect(await nor.getContractVersion()).to.be.equal(3); - expect(await nor.getRewardDistributionState()).to.be.equal(RewardDistributionState.Distributed); - }); - - it("reverts with error UNEXPECTED_CONTRACT_VERSION when called on already upgraded contract", async () => { - await nor.finalizeUpgrade_v3(); - expect(await nor.getContractVersion()).to.be.equal(3); - await expect(nor.finalizeUpgrade_v3()).to.be.revertedWith("UNEXPECTED_CONTRACT_VERSION"); - }); - - it("Migrates the contract storage from v1 to v3", async () => { - preInitState = await Snapshot.refresh(preInitState); - - await nor.harness__initialize(0n); - - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.be.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.be.equal( - secondNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[thirdNodeOperatorId])).to.be.equal( - thirdNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[fourthNodeOperatorId])).to.be.equal( - fourthNodeOperatorId, - ); - - await nor.harness__unsafeResetModuleSummary(); - const resetSummary = await nor.getStakingModuleSummary(); - expect(resetSummary.totalExitedValidators).to.be.equal(0n); - expect(resetSummary.totalDepositedValidators).to.be.equal(0n); - expect(resetSummary.depositableValidatorsCount).to.be.equal(0n); - - await nor.harness__unsafeSetVettedKeys( - firstNodeOperatorId, - NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount - 1n, - ); - await nor.harness__unsafeSetVettedKeys( - secondNodeOperatorId, - NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount + 1n, - ); - await nor.harness__unsafeSetVettedKeys( - thirdNodeOperatorId, - NODE_OPERATORS[thirdNodeOperatorId].totalSigningKeysCount, - ); - - const checkStorage = async () => { - const summary = await nor.getStakingModuleSummary(); - expect(summary.totalExitedValidators).to.be.equal(1n + 0n + 0n + 1n); - expect(summary.totalDepositedValidators).to.be.equal(5n + 7n + 0n + 2n); - expect(summary.depositableValidatorsCount).to.be.equal(0n + 8n + 0n + 0n); - - const firstNoInfo = await nor.getNodeOperator(firstNodeOperatorId, true); - expect(firstNoInfo.totalVettedValidators).to.be.equal( - NODE_OPERATORS[firstNodeOperatorId].depositedSigningKeysCount, - ); - - const secondNoInfo = await nor.getNodeOperator(secondNodeOperatorId, true); - expect(secondNoInfo.totalVettedValidators).to.be.equal( - NODE_OPERATORS[secondNodeOperatorId].totalSigningKeysCount, - ); - - const thirdNoInfo = await nor.getNodeOperator(thirdNodeOperatorId, true); - expect(thirdNoInfo.totalVettedValidators).to.be.equal( - NODE_OPERATORS[thirdNodeOperatorId].depositedSigningKeysCount, - ); - - const fourthNoInfo = await nor.getNodeOperator(fourthNodeOperatorId, true); - expect(fourthNoInfo.totalVettedValidators).to.be.equal( - NODE_OPERATORS[fourthNodeOperatorId].vettedSigningKeysCount, - ); - }; - - await expect(nor.finalizeUpgrade_v2(locator, moduleType, 86400n)) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV2) - .and.to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(86400n) - .and.to.emit(nor, "LocatorContractSet") - .withArgs(await locator.getAddress()) - .and.to.emit(nor, "StakingModuleTypeSet") - .withArgs(moduleType); - - await checkStorage(); - - await expect(nor.finalizeUpgrade_v3()) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV3) - .to.emit(nor, "RewardDistributionStateChanged") - .withArgs(RewardDistributionState.Distributed); - - await checkStorage(); - }); - - it("Calling finalizeUpgrade_v3 on v1 version", async () => { - preInitState = await Snapshot.refresh(preInitState); - await nor.harness__initialize(0n); - await expect(nor.finalizeUpgrade_v3()).to.be.revertedWith("UNEXPECTED_CONTRACT_VERSION"); - }); - - it("Happy path test for update from v1: finalizeUpgrade_v2 -> finalizeUpgrade_v3", async () => { - preInitState = await Snapshot.refresh(preInitState); - - await nor.harness__initialize(0n); - - const latestBlock = BigInt(await time.latestBlock()); - const burnerAddress = await locator.burner(); - - await expect(nor.finalizeUpgrade_v2(locator, moduleType, 86400n)) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV2) - .and.to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(86400n) - .and.to.emit(nor, "LocatorContractSet") - .withArgs(await locator.getAddress()) - .and.to.emit(nor, "StakingModuleTypeSet") - .withArgs(moduleType); - - expect(await nor.getLocator()).to.equal(await locator.getAddress()); - expect(await nor.getInitializationBlock()).to.equal(latestBlock); - expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(MaxUint256); - expect(await nor.getStuckPenaltyDelay()).to.equal(86400n); - expect(await nor.getContractVersion()).to.equal(contractVersionV2); - expect(await nor.getType()).to.equal(moduleType); - - await expect(nor.finalizeUpgrade_v3()) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV3) - .to.emit(nor, "RewardDistributionStateChanged") - .withArgs(RewardDistributionState.Distributed); - - expect(await nor.getLocator()).to.equal(await locator.getAddress()); - expect(await nor.getInitializationBlock()).to.equal(latestBlock); - expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(MaxUint256); - expect(await nor.getStuckPenaltyDelay()).to.equal(86400n); - expect(await nor.getContractVersion()).to.equal(contractVersionV3); - expect(await nor.getType()).to.equal(moduleType); - }); - }); }); From 0aff0c8828fc4187aea0ac790d3799caf2bbc8c7 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 15 Apr 2025 20:35:23 +0200 Subject: [PATCH 081/405] refactor: remove unused node operator configurations from tests --- .../0.4.24/nor/nor.initialize.upgrade.test.ts | 55 +------------------ 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 88f6def9a5..449330fd87 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -7,7 +7,7 @@ import { time } from "@nomicfoundation/hardhat-network-helpers"; import { ACL, Kernel, Lido, LidoLocator, NodeOperatorsRegistry__Harness } from "typechain-types"; -import { addNodeOperator, certainAddress, NodeOperatorConfig, RewardDistributionState } from "lib"; +import { RewardDistributionState } from "lib"; import { addAragonApp, deployLidoDao, deployLidoLocator } from "test/deploy"; import { Snapshot } from "test/suite"; @@ -30,59 +30,6 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { let originalState: string; - const firstNodeOperatorId = 0; - const secondNodeOperatorId = 1; - const thirdNodeOperatorId = 2; - const fourthNodeOperatorId = 3; - - const NODE_OPERATORS: NodeOperatorConfig[] = [ - { - name: "foo", - rewardAddress: certainAddress("node-operator-1"), - totalSigningKeysCount: 10n, - depositedSigningKeysCount: 5n, - exitedSigningKeysCount: 1n, - vettedSigningKeysCount: 6n, - stuckValidatorsCount: 0n, - refundedValidatorsCount: 0n, - stuckPenaltyEndAt: 0n, - }, - { - name: "bar", - rewardAddress: certainAddress("node-operator-2"), - totalSigningKeysCount: 15n, - depositedSigningKeysCount: 7n, - exitedSigningKeysCount: 0n, - vettedSigningKeysCount: 10n, - stuckValidatorsCount: 0n, - refundedValidatorsCount: 0n, - stuckPenaltyEndAt: 0n, - }, - { - name: "deactivated", - isActive: false, - rewardAddress: certainAddress("node-operator-3"), - totalSigningKeysCount: 10n, - depositedSigningKeysCount: 0n, - exitedSigningKeysCount: 0n, - vettedSigningKeysCount: 5n, - stuckValidatorsCount: 0n, - refundedValidatorsCount: 0n, - stuckPenaltyEndAt: 0n, - }, - { - name: "extra-no", - rewardAddress: certainAddress("node-operator-4"), - totalSigningKeysCount: 3n, - depositedSigningKeysCount: 2n, - exitedSigningKeysCount: 1n, - vettedSigningKeysCount: 2n, - stuckValidatorsCount: 1n, - refundedValidatorsCount: 0n, - stuckPenaltyEndAt: 0n, - }, - ]; - const moduleType = encodeBytes32String("curated-onchain-v1"); const contractVersionV2 = 2n; const contractVersionV3 = 3n; From 966241ee144871c0f663a1b0700deed462ce370e Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 16 Apr 2025 11:12:34 +0200 Subject: [PATCH 082/405] refactor: rename IStakingModule methods --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 10 ++++---- contracts/0.8.9/StakingRouter.sol | 8 +++---- contracts/0.8.9/interfaces/IStakingModule.sol | 6 ++--- test/0.4.24/nor/nor.exit.manager.test.ts | 24 +++++++++---------- .../StakingModule__MockForStakingRouter.sol | 16 ++++++------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c5c7c50311..5dd288fb0a 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1146,7 +1146,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return 60 * 60 * 24 * 2; // 2 days } - function _shouldValidatorBePenalized( + function _isValidatorExitDelayPenaltyApplicable( uint256, // _nodeOperatorId uint256, // _proofSlotTimestamp bytes, // _publicKey @@ -1155,7 +1155,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return _eligibleToExitInSec >= _getExitDeadlineThreshold(); } - function handleActiveValidatorsExitingStatus( + function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes _publicKey, @@ -1169,7 +1169,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } - function onTriggerableExit( + function onValidatorExitTriggered( uint256 _nodeOperatorId, bytes _publicKey, uint256 _withdrawalRequestPaidFee, @@ -1185,13 +1185,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return _getExitDeadlineThreshold(); } - function shouldValidatorBePenalized( + function isValidatorExitDelayPenaltyApplicable( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool) { - return _shouldValidatorBePenalized(_nodeOperatorId, _proofSlotTimestamp, _publicKey, _eligibleToExitInSec); + return _isValidatorExitDelayPenaltyApplicable(_nodeOperatorId, _proofSlotTimestamp, _publicKey, _eligibleToExitInSec); } function _removeUnusedSigningKeys(uint256 _nodeOperatorId, uint256 _fromIndex, uint256 _keysCount) internal { diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index eb17181509..8774660e42 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1477,7 +1477,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. /// @param _publicKey The public key of the validator being reported. /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. - function handleActiveValidatorsExitingStatus( + function reportValidatorExitDelay( uint256 _stakingModuleId, uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, @@ -1487,7 +1487,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version external onlyRole(REPORT_EXITED_VALIDATORS_STATUS_ROLE) { - _getIStakingModuleById(_stakingModuleId).handleActiveValidatorsExitingStatus( + _getIStakingModuleById(_stakingModuleId).reportValidatorExitDelay( _nodeOperatorId, _proofSlotTimestamp, _publicKey, @@ -1503,7 +1503,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the Execution Layer (EL). /// @param _exitType The type of exit being performed. /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. - function onTriggerableExit( + function onValidatorExitTriggered( uint256 _stakingModuleId, uint256 _nodeOperatorId, bytes calldata _publicKey, @@ -1513,7 +1513,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version external onlyRole(REPORT_EXITED_VALIDATORS_ROLE) { - _getIStakingModuleById(_stakingModuleId).onTriggerableExit( + _getIStakingModuleById(_stakingModuleId).onValidatorExitTriggered( _nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 1fc079d7e7..1291e16982 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -23,7 +23,7 @@ interface IStakingModule { /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. /// @param _publicKey The public key of the validator being reported. /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. - function handleActiveValidatorsExitingStatus( + function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, @@ -38,7 +38,7 @@ interface IStakingModule { /// @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the Execution Layer (EL). /// @param _exitType The type of exit being performed. /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. - function onTriggerableExit( + function onValidatorExitTriggered( uint256 _nodeOperatorId, bytes calldata _publicKey, uint256 _withdrawalRequestPaidFee, @@ -51,7 +51,7 @@ interface IStakingModule { /// @param _publicKey The public key of the validator. /// @param _eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. /// @return bool Returns true if the contract should receive the updated status of the validator. - function shouldValidatorBePenalized( + function isValidatorExitDelayPenaltyApplicable( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index fb7ba4b85f..99ee01bd63 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -119,13 +119,13 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { afterEach(async () => (originalState = await Snapshot.refresh(originalState))); - context("handleActiveValidatorsExitingStatus", () => { + context("reportValidatorExitDelay", () => { it("reverts when called by sender without STAKING_ROUTER_ROLE", async () => { expect(await acl["hasPermission(address,address,bytes32)"](stranger, nor, await nor.STAKING_ROUTER_ROLE())).to.be .false; await expect( - nor.connect(stranger).handleActiveValidatorsExitingStatus( + nor.connect(stranger).reportValidatorExitDelay( firstNodeOperatorId, proofSlotTimestamp, testPublicKey, @@ -139,7 +139,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .to.be.true; await expect( - nor.connect(stakingRouter).handleActiveValidatorsExitingStatus( + nor.connect(stakingRouter).reportValidatorExitDelay( firstNodeOperatorId, proofSlotTimestamp, testPublicKey, @@ -154,7 +154,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("reverts when public key is empty", async () => { await expect( - nor.connect(stakingRouter).handleActiveValidatorsExitingStatus( + nor.connect(stakingRouter).reportValidatorExitDelay( firstNodeOperatorId, proofSlotTimestamp, "0x", @@ -164,13 +164,13 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { }); }); - context("onTriggerableExit", () => { + context("onValidatorExitTriggered", () => { it("reverts when called by sender without STAKING_ROUTER_ROLE", async () => { expect(await acl["hasPermission(address,address,bytes32)"](stranger, nor, await nor.STAKING_ROUTER_ROLE())).to.be .false; await expect( - nor.connect(stranger).onTriggerableExit( + nor.connect(stranger).onValidatorExitTriggered( firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, @@ -184,7 +184,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .to.be.true; await expect( - nor.connect(stakingRouter).onTriggerableExit( + nor.connect(stakingRouter).onValidatorExitTriggered( firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, @@ -197,7 +197,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("reverts when public key is empty", async () => { await expect( - nor.connect(stakingRouter).onTriggerableExit( + nor.connect(stakingRouter).onValidatorExitTriggered( firstNodeOperatorId, "0x", withdrawalRequestPaidFee, @@ -214,9 +214,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { }); }); - context("shouldValidatorBePenalized", () => { + context("isValidatorExitDelayPenaltyApplicable", () => { it("returns true when eligible to exit time exceeds the threshold", async () => { - const shouldPenalize = await nor.shouldValidatorBePenalized( + const shouldPenalize = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, proofSlotTimestamp, testPublicKey, @@ -224,7 +224,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { ); expect(shouldPenalize).to.be.true; - const shouldPenalizeMore = await nor.shouldValidatorBePenalized( + const shouldPenalizeMore = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, proofSlotTimestamp, testPublicKey, @@ -234,7 +234,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { }); it("returns false when eligible to exit time is less than the threshold", async () => { - const shouldPenalize = await nor.shouldValidatorBePenalized( + const shouldPenalize = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, proofSlotTimestamp, testPublicKey, diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index 66a1c41696..f03accccb8 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -11,14 +11,14 @@ contract StakingModule__MockForStakingRouter is IStakingModule { event Mock__OnRewardsMinted(uint256 _totalShares); event Mock__ExitedValidatorsCountUpdated(bytes _nodeOperatorIds, bytes _stuckValidatorsCounts); - event Mock__HandleActiveValidatorsExitingStatus( + event Mock__reportValidatorExitDelay( uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKeys, bytes eligibleToExitInSec ); - event Mock__OnTriggerableExit( + event Mock__onValidatorExitTriggered( uint256 _nodeOperatorId, bytes publicKeys, uint256 withdrawalRequestPaidFee, @@ -268,13 +268,13 @@ contract StakingModule__MockForStakingRouter is IStakingModule { bool private shouldBePenalized__mocked; - function handleActiveValidatorsExitingStatus( + function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKeys, bytes calldata _eligibleToExitInSec ) external { - emit Mock__HandleActiveValidatorsExitingStatus( + emit Mock__reportValidatorExitDelay( _nodeOperatorId, _proofSlotTimestamp, _publicKeys, @@ -282,13 +282,13 @@ contract StakingModule__MockForStakingRouter is IStakingModule { ); } - function onTriggerableExit( + function onValidatorExitTriggered( uint256 _nodeOperatorId, bytes calldata _publicKeys, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external { - emit Mock__OnTriggerableExit( + emit Mock__onValidatorExitTriggered( _nodeOperatorId, _publicKeys, _withdrawalRequestPaidFee, @@ -296,7 +296,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { ); } - function shouldValidatorBePenalized( + function isValidatorExitDelayPenaltyApplicable( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, @@ -305,7 +305,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { return shouldBePenalized__mocked; } - function mock__shouldValidatorBePenalized(bool _shouldBePenalized) external { + function mock__isValidatorExitDelayPenaltyApplicable(bool _shouldBePenalized) external { shouldBePenalized__mocked = _shouldBePenalized; } From 9a14e9d8b97271e33583aa2e604dc56a127f55ea Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 16 Apr 2025 21:39:40 +0400 Subject: [PATCH 083/405] fix: formatting & refactoring --- .../0.8.9/interfaces/IValidatorExitBus.sol | 1 + contracts/0.8.9/oracle/ValidatorsExitBus.sol | 216 +++++++++--------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 33 +-- ...it-bus-oracle.triggerExitsDirectly.test.ts | 15 +- 4 files changed, 132 insertions(+), 133 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol index a5e97d1188..1c2c2a1863 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -15,6 +15,7 @@ interface IValidatorsExitBus { } struct DeliveryHistory { + // index in array of requests uint256 lastDeliveredKeyIndex; uint256 timestamp; } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index ce4551e906..656681e579 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -2,20 +2,19 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; -import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol"; -import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; -import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; -import { Versioned } from "../utils/Versioned.sol"; -import { ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData } from "../lib/ReportExitLimitUtils.sol"; -import { PausableUntil } from "../utils/PausableUntil.sol"; -import { IValidatorsExitBus} from "../interfaces/IValidatorExitBus.sol"; +import {AccessControlEnumerable} from "../utils/access/AccessControlEnumerable.sol"; +import {UnstructuredStorage} from "../lib/UnstructuredStorage.sol"; +import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; +import {Versioned} from "../utils/Versioned.sol"; +import {ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData} from "../lib/ReportExitLimitUtils.sol"; +import {PausableUntil} from "../utils/PausableUntil.sol"; +import {IValidatorsExitBus} from "../interfaces/IValidatorExitBus.sol"; interface IWithdrawalVault { function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; function getWithdrawalRequestFee() external view returns (uint256); } - contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ReportExitLimitUtilsStorage for bytes32; @@ -37,15 +36,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error NoExitRequestProvided(); /// @dev Events - event MadeRefund( - address sender, - uint256 refundValue - ); - - event StoredExitRequestHash( - bytes32 exitRequestHash - ); - + event MadeRefund(address sender, uint256 refundValue); + event StoredExitRequestHash(bytes32 exitRequestHash); event ValidatorExitRequest( uint256 indexed stakingModuleId, uint256 indexed nodeOperatorId, @@ -53,11 +45,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorPubkey, uint256 timestamp ); - - event ExitRequestsLimitSet( - uint256 _maxExitRequestsLimit, - uint256 _exitRequestsLimitIncreasePerBlock - ); + event ExitRequestsLimitSet(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock); event DirectExitRequest( uint256 indexed stakingModuleId, @@ -66,18 +54,17 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 timestamp ); struct RequestStatus { - // Total items count in report (by default type(uint32).max, update on first report delivery) - uint256 totalItemsCount; - // Total processed items in report (by default 0) - uint256 deliveredItemsCount; - // Vebo contract version at the time of hash submittion - uint256 contractVersion; - - DeliveryHistory[] deliverHistory; + // Total items count in report (by default type(uint32).max, update on first report delivery) + uint256 totalItemsCount; + // Total processed items in report (by default 0) + uint256 deliveredItemsCount; + // Vebo contract version at the time of hash submission + uint256 contractVersion; + DeliveryHistory[] deliverHistory; } bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); - bytes32 public constant DIRECT_EXIT_HASH_ROLE = keccak256("DIRECT_EXIT_HASH_ROLE"); + bytes32 public constant DIRECT_EXIT_ROLE = keccak256("DIRECT_EXIT_ROLE"); bytes32 public constant EXIT_REPORT_LIMIT_ROLE = keccak256("EXIT_REPORT_LIMIT_ROLE"); /// @notice An ACL role granting the permission to pause accepting validator exit requests bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); @@ -93,7 +80,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ILidoLocator internal immutable LOCATOR; - /// @notice The list format of the validator exit requests data. Used when all /// requests fit into a single transaction. /// @@ -113,26 +99,34 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 public constant DATA_FORMAT_LIST = 1; /// Hash constant for mapping exit requests storage - bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = - keccak256("lido.ValidatorsExitBus.reportHashes"); + bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); + + /// @dev Ensures the contract’s ETH balance is unchanged. + modifier preservesEthBalance() { + uint256 balanceBeforeCall = address(this).balance - msg.value; + _; + assert(address(this).balance == balanceBeforeCall); + } constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); } function submitReportHash(bytes32 exitReportHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { - uint256 contractVersion = getContractVersion(); - _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0,0)); + uint256 contractVersion = getContractVersion(); + _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); } function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external whenResumed { bytes calldata data = request.data; _checkContractVersion(contractVersion); - RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(data, request.dataFormat))]; + RequestStatus storage requestStatus = _storageExitRequestsHashes()[ + keccak256(abi.encode(data, request.dataFormat)) + ]; if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); + revert ExitHashWasNotSubmitted(); } if (request.dataFormat != DATA_FORMAT_LIST) { @@ -143,15 +137,16 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InvalidRequestsDataLength(); } - // TODO: hash requestsCount too - if (requestStatus.totalItemsCount == type(uint256).max ) { - requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; + // By default, totalItemsCount is set to type(uint256).max. + // If an exit is emitted for the request for the first time, the default value is used for totalItemsCount. + if (requestStatus.totalItemsCount == type(uint256).max) { + requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; } uint256 deliveredItemsCount = requestStatus.deliveredItemsCount; uint256 undeliveredItemsCount = requestStatus.totalItemsCount - deliveredItemsCount; - if (undeliveredItemsCount == 0 ) { + if (undeliveredItemsCount == 0) { revert RequestsAlreadyDelivered(); } @@ -159,18 +154,18 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 toDeliver; if (exitRequestLimitData.isExitReportLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); - if (limit == 0) { - revert ExitRequestsLimit(); - } + uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + if (limit == 0) { + revert ExitRequestsLimit(); + } - toDeliver = undeliveredItemsCount > limit - ? limit - : undeliveredItemsCount; + toDeliver = undeliveredItemsCount > limit ? limit : undeliveredItemsCount; - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit(exitRequestLimitData.updatePrevExitRequestsLimit(limit - toDeliver)); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitRequestsLimit(limit - toDeliver) + ); } else { - toDeliver = undeliveredItemsCount; + toDeliver = undeliveredItemsCount; } _processExitRequestsList(request.data, deliveredItemsCount, toDeliver); @@ -181,19 +176,23 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// @notice Triggers exits on the EL via the Withdrawal Vault contract after /// @dev This function verifies that the hash of the provided exit request data exists in storage // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. - function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable whenResumed { - uint256 prevBalance = address(this).balance - msg.value; + function triggerExits( + ExitRequestData calldata request, + uint256[] calldata keyIndexes + ) external payable whenResumed preservesEthBalance { bytes calldata data = request.data; - RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(data, request.dataFormat))]; + RequestStatus storage requestStatus = _storageExitRequestsHashes()[ + keccak256(abi.encode(data, request.dataFormat)) + ]; if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); + revert ExitHashWasNotSubmitted(); } address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - if (msg.value < keyIndexes.length * withdrawalFee ) { - revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); + if (msg.value < keyIndexes.length * withdrawalFee) { + revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); } uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; @@ -203,7 +202,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa // TODO: create library for reading DATA for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { - revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); + revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); } if (keyIndexes[i] > lastDeliveredKeyIndex) { @@ -219,75 +218,61 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa assembly { let dest := add(pubkeys, add(32, destOffset)) - calldatacopy( - dest, - add(data.offset, requestPublicKeyOffset), - PUBLIC_KEY_LENGTH - ) + calldatacopy(dest, add(data.offset, requestPublicKeyOffset), PUBLIC_KEY_LENGTH) } } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}(pubkeys); - - uint256 refund = msg.value - keyIndexes.length * withdrawalFee; - - if (refund > 0) { - (bool success, ) = msg.sender.call{value: refund}(""); - - if (!success) { - revert TriggerableWithdrawalRefundFailed(); - } - - emit MadeRefund(msg.sender, refund); - } + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( + pubkeys + ); - assert(address(this).balance == prevBalance); + _refundFee(keyIndexes.length * withdrawalFee); } - function triggerExitsDirectly(DirectExitData calldata exitData) external payable whenResumed onlyRole(DIRECT_EXIT_HASH_ROLE) returns (uint256) { - uint256 prevBalance = address(this).balance - msg.value; + function triggerExitsDirectly( + DirectExitData calldata exitData + ) external payable whenResumed onlyRole(DIRECT_EXIT_ROLE) preservesEthBalance returns (uint256) { address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); if (exitData.validatorsPubkeys.length == 0) { - revert NoExitRequestProvided(); + revert NoExitRequestProvided(); } - if ( exitData.validatorsPubkeys.length % PUBLIC_KEY_LENGTH != 0) { - revert InvalidPubkeysArray(); + if (exitData.validatorsPubkeys.length % PUBLIC_KEY_LENGTH != 0) { + revert InvalidPubkeysArray(); } // TODO: maybe add requestCount in DirectExitData uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; - if (msg.value < withdrawalFee * requestsCount ) { - revert InsufficientPayment(withdrawalFee, requestsCount , msg.value); + if (msg.value < withdrawalFee * requestsCount) { + revert InsufficientPayment(withdrawalFee, requestsCount, msg.value); } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}(exitData.validatorsPubkeys); - - emit DirectExitRequest(exitData.stakingModuleId, exitData.nodeOperatorId, exitData.validatorsPubkeys, _getTimestamp()); - - uint256 refund = msg.value - withdrawalFee * requestsCount; - - if (refund > 0) { - (bool success, ) = msg.sender.call{value: refund}(""); - - if (!success) { - revert TriggerableWithdrawalRefundFailed(); - } - - emit MadeRefund(msg.sender, refund); - } + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( + exitData.validatorsPubkeys + ); - assert(address(this).balance == prevBalance); + emit DirectExitRequest( + exitData.stakingModuleId, + exitData.nodeOperatorId, + exitData.validatorsPubkeys, + _getTimestamp() + ); - return refund; + return _refundFee(withdrawalFee * requestsCount); } - function setExitReportLimit(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { + function setExitReportLimit( + uint256 _maxExitRequestsLimit, + uint256 _exitRequestsLimitIncreasePerBlock + ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitReportLimit(_maxExitRequestsLimit, _exitRequestsLimitIncreasePerBlock) + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitReportLimit( + _maxExitRequestsLimit, + _exitRequestsLimitIncreasePerBlock + ) ); emit ExitRequestsLimitSet(_maxExitRequestsLimit, _exitRequestsLimitIncreasePerBlock); @@ -432,7 +417,23 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _getTimestamp() internal virtual view returns (uint256) { + function _refundFee(uint256 fee) internal returns (uint256) { + uint256 refund = msg.value - fee; + + if (refund > 0) { + (bool success, ) = msg.sender.call{value: refund}(""); + + if (!success) { + revert TriggerableWithdrawalRefundFailed(); + } + + emit MadeRefund(msg.sender, refund); + } + + return refund; + } + + function _getTimestamp() internal view virtual returns (uint256) { return block.timestamp; // solhint-disable-line not-rely-on-time } @@ -456,17 +457,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa request.deliverHistory.push(history); } - emit StoredExitRequestHash(exitRequestHash); } /// Storage helpers - function _storageExitRequestsHashes() internal pure returns ( - mapping(bytes32 => RequestStatus) storage r - ) { + function _storageExitRequestsHashes() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { bytes32 position = EXIT_REQUESTS_HASHES_POSITION; assembly { r.slot := position } } -} \ No newline at end of file +} diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 61728a527f..2888fc0b2d 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.9; import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; -import { Math256 } from "../../common/lib/Math256.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { BaseOracle } from "./BaseOracle.sol"; @@ -60,11 +59,11 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// Initialization & admin functions /// - constructor(uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator) - BaseOracle(secondsPerSlot, genesisTime) - ValidatorsExitBus(lidoLocator) - { - } + constructor( + uint256 secondsPerSlot, + uint256 genesisTime, + address lidoLocator + ) BaseOracle(secondsPerSlot, genesisTime) ValidatorsExitBus(lidoLocator) {} function initialize( address admin, @@ -246,15 +245,15 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (exitRequestLimitData.isExitReportLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); - if (data.requestsCount > limit) { - revert ExitRequestsLimit(); - } + if (data.requestsCount > limit) { + revert ExitRequestsLimit(); + } - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitRequestsLimit(limit - data.requestsCount) - ); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitRequestsLimit(limit - data.requestsCount) + ); } if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { @@ -283,7 +282,13 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storeExitRequestHash(exitRequestHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(requestsCount - 1, _getTimestamp())); + _storeExitRequestHash( + exitRequestHash, + requestsCount, + requestsCount, + contractVersion, + DeliveryHistory(requestsCount - 1, _getTimestamp()) + ); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index 55e7a914b8..d99fd4f1e9 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -56,25 +56,25 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { await deploy(); }); - it("Should revert without DIRECT_EXIT_HASH_ROLE role", async () => { + it("Should revert without DIRECT_EXIT_ROLE role", async () => { const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); exitData = { stakingModuleId: 1, nodeOperatorId: 0, - validatorsPubkeys: "0x" + concatenatedPubKeys + validatorsPubkeys: "0x" + concatenatedPubKeys, }; await expect( oracle.connect(stranger).triggerExitsDirectly(exitData, { value: 4, }), - ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_HASH_ROLE()); + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_ROLE()); }); it("Not enough fee", async () => { - const role = await oracle.DIRECT_EXIT_HASH_ROLE(); + const role = await oracle.DIRECT_EXIT_ROLE(); await oracle.grantRole(role, authorizedEntity); @@ -97,11 +97,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs( - exitData.stakingModuleId, - exitData.nodeOperatorId, - exitData.validatorsPubkeys, - timestamp, - ); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, exitData.validatorsPubkeys, timestamp); }); }); From c32f188fa06b28588730aacfd1148d63546536e0 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 10:28:10 +0200 Subject: [PATCH 084/405] refactor: simplify validator exit delay penalty logic --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 5dd288fb0a..f837296d6a 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1146,15 +1146,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return 60 * 60 * 24 * 2; // 2 days } - function _isValidatorExitDelayPenaltyApplicable( - uint256, // _nodeOperatorId - uint256, // _proofSlotTimestamp - bytes, // _publicKey - uint256 _eligibleToExitInSec - ) internal view returns (bool) { - return _eligibleToExitInSec >= _getExitDeadlineThreshold(); - } - function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, @@ -1186,12 +1177,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } function isValidatorExitDelayPenaltyApplicable( - uint256 _nodeOperatorId, - uint256 _proofSlotTimestamp, - bytes _publicKey, + uint256, // _nodeOperatorId + uint256, // _proofSlotTimestamp + bytes, // _publicKey uint256 _eligibleToExitInSec ) external view returns (bool) { - return _isValidatorExitDelayPenaltyApplicable(_nodeOperatorId, _proofSlotTimestamp, _publicKey, _eligibleToExitInSec); + return _eligibleToExitInSec >= _getExitDeadlineThreshold(); } function _removeUnusedSigningKeys(uint256 _nodeOperatorId, uint256 _fromIndex, uint256 _keysCount) internal { From 3dc481e476eae4aa209b5c814807c569665ce19c Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 13:03:36 +0200 Subject: [PATCH 085/405] wip: removed the stuck logic --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 154 +----- .../NodeOperatorsRegistry__Harness.sol | 11 +- test/0.4.24/nor/nor.aux.test.ts | 39 +- .../0.4.24/nor/nor.initialize.upgrade.test.ts | 6 - test/0.4.24/nor/nor.management.flow.test.ts | 205 +------- .../nor/nor.rewards.penalties.flow.test.ts | 480 ------------------ 6 files changed, 15 insertions(+), 880 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index f837296d6a..9331b7fb63 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -211,7 +211,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// [....totalSigningKeysCount.....:.........:<--------:---------]-------> /// : : : : : Packed64x4.Packed signingKeysStats; - Packed64x4.Packed stuckPenaltyStats; Packed64x4.Packed targetValidatorsStats; } @@ -463,45 +462,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { require(countsLength / 16 == count && idsLength % 8 == 0 && countsLength % 16 == 0, "INVALID_REPORT_DATA"); } - /// @notice Called by StakingRouter to update the number of the validators of the given node - /// operator that were requested to exit but failed to do so in the max allowed time - /// - /// @param _nodeOperatorIds bytes packed array of the node operators id - /// @param _stuckValidatorsCounts bytes packed array of the new number of stuck validators for the node operators - function updateStuckValidatorsCount(bytes _nodeOperatorIds, bytes _stuckValidatorsCounts) external { - _auth(STAKING_ROUTER_ROLE); - uint256 nodeOperatorsCount = _checkReportPayload(_nodeOperatorIds.length, _stuckValidatorsCounts.length); - uint256 totalNodeOperatorsCount = getNodeOperatorsCount(); - - uint256 nodeOperatorId; - uint256 validatorsCount; - uint256 _nodeOperatorIdsOffset; - uint256 _stuckValidatorsCountsOffset; - - /// @dev calldata layout: - /// | func sig (4 bytes) | ABI-enc data | - /// - /// ABI-enc data: - /// - /// | 32 bytes | 32 bytes | 32 bytes | ... | 32 bytes | ...... | - /// | ids len offset | counts len offset | ids len | ids | counts len | counts | - assembly { - _nodeOperatorIdsOffset := add(calldataload(4), 36) // arg1 calldata offset + 4 (signature len) + 32 (length slot) - _stuckValidatorsCountsOffset := add(calldataload(36), 36) // arg2 calldata offset + 4 (signature len) + 32 (length slot)) - } - for (uint256 i; i < nodeOperatorsCount;) { - /// @solidity memory-safe-assembly - assembly { - nodeOperatorId := shr(192, calldataload(add(_nodeOperatorIdsOffset, mul(i, 8)))) - validatorsCount := shr(128, calldataload(add(_stuckValidatorsCountsOffset, mul(i, 16)))) - i := add(i, 1) - } - _requireValidRange(nodeOperatorId < totalNodeOperatorsCount); - _updateStuckValidatorsCount(nodeOperatorId, validatorsCount); - } - _increaseValidatorsKeysNonce(); - } - /// @notice Called by StakingRouter to update the number of the validators in the EXITED state /// for node operator with given id /// @@ -597,7 +557,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _onlyExistedNodeOperator(_nodeOperatorId); _auth(STAKING_ROUTER_ROLE); - _updateStuckValidatorsCount(_nodeOperatorId, _stuckValidatorsCount); _updateExitedValidatorsCount(_nodeOperatorId, _exitedValidatorsCount, true /* _allowDecrease */ ); _increaseValidatorsKeysNonce(); } @@ -614,12 +573,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { ); uint256 depositedValidatorsCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); - uint256 stuckValidatorsCount = - _loadOperatorStuckPenaltyStats(_nodeOperatorId).get(STUCK_VALIDATORS_COUNT_OFFSET); - // sustain invariant exited + stuck <= deposited - assert(depositedValidatorsCount >= stuckValidatorsCount); - _requireValidRange(_exitedValidatorsCount <= depositedValidatorsCount - stuckValidatorsCount); + // sustain invariant exited <= deposited + _requireValidRange(_exitedValidatorsCount <= depositedValidatorsCount); signingKeysStats.set(TOTAL_EXITED_KEYS_COUNT_OFFSET, _exitedValidatorsCount); _saveOperatorSigningKeysStats(_nodeOperatorId, signingKeysStats); @@ -668,63 +624,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _increaseValidatorsKeysNonce(); } - /** - * @notice Set the stuck signings keys count - */ - function _updateStuckValidatorsCount(uint256 _nodeOperatorId, uint256 _stuckValidatorsCount) internal { - Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - uint256 curStuckValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); - if (_stuckValidatorsCount == curStuckValidatorsCount) return; - - Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - uint256 exitedValidatorsCount = signingKeysStats.get(TOTAL_EXITED_KEYS_COUNT_OFFSET); - uint256 depositedValidatorsCount = signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET); - - // sustain invariant exited + stuck <= deposited - assert(depositedValidatorsCount >= exitedValidatorsCount); - _requireValidRange(_stuckValidatorsCount <= depositedValidatorsCount - exitedValidatorsCount); - - uint256 curRefundedValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); - if (_stuckValidatorsCount <= curRefundedValidatorsCount && curStuckValidatorsCount > curRefundedValidatorsCount) { - stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, block.timestamp + getStuckPenaltyDelay()); - } - - stuckPenaltyStats.set(STUCK_VALIDATORS_COUNT_OFFSET, _stuckValidatorsCount); - _saveOperatorStuckPenaltyStats(_nodeOperatorId, stuckPenaltyStats); - emit StuckPenaltyStateChanged( - _nodeOperatorId, - _stuckValidatorsCount, - curRefundedValidatorsCount, - stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET) - ); - - _updateSummaryMaxValidatorsCount(_nodeOperatorId); - } - - function _updateRefundValidatorsKeysCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) internal { - Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - uint256 curRefundedValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); - if (_refundedValidatorsCount == curRefundedValidatorsCount) return; - - Packed64x4.Packed memory signingKeysStats = _loadOperatorSigningKeysStats(_nodeOperatorId); - _requireValidRange(_refundedValidatorsCount <= signingKeysStats.get(TOTAL_DEPOSITED_KEYS_COUNT_OFFSET)); - - uint256 curStuckValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); - if (_refundedValidatorsCount >= curStuckValidatorsCount && curRefundedValidatorsCount < curStuckValidatorsCount) { - stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, block.timestamp + getStuckPenaltyDelay()); - } - - stuckPenaltyStats.set(REFUNDED_VALIDATORS_COUNT_OFFSET, _refundedValidatorsCount); - _saveOperatorStuckPenaltyStats(_nodeOperatorId, stuckPenaltyStats); - emit StuckPenaltyStateChanged( - _nodeOperatorId, - curStuckValidatorsCount, - _refundedValidatorsCount, - stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET) - ); - _updateSummaryMaxValidatorsCount(_nodeOperatorId); - } - // @dev Recalculate and update the max validator count for operator and summary stats function _updateSummaryMaxValidatorsCount(uint256 _nodeOperatorId) internal { (uint256 oldMaxSigningKeysCount, uint256 newMaxSigningKeysCount) = _applyNodeOperatorLimits(_nodeOperatorId); @@ -1308,13 +1207,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _onlyExistedNodeOperator(_nodeOperatorId); Packed64x4.Packed memory operatorTargetStats = _loadOperatorTargetValidatorsStats(_nodeOperatorId); - Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); targetLimitMode = operatorTargetStats.get(TARGET_LIMIT_MODE_OFFSET); targetValidatorsCount = operatorTargetStats.get(TARGET_VALIDATORS_COUNT_OFFSET); - stuckValidatorsCount = stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET); + stuckValidatorsCount = 0; refundedValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); - stuckPenaltyEndTimestamp = stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET); + stuckPenaltyEndTimestamp = 0; (totalExitedValidators, totalDepositedValidators, depositableValidatorsCount) = _getNodeOperatorValidatorsSummary(_nodeOperatorId); @@ -1331,33 +1229,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { depositableValidatorsCount = totalMaxValidators - totalDepositedValidators; } - function _isOperatorPenalized(Packed64x4.Packed memory stuckPenaltyStats) internal view returns (bool) { - return stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET) < stuckPenaltyStats.get(STUCK_VALIDATORS_COUNT_OFFSET) - || block.timestamp <= stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET); - } - - function isOperatorPenalized(uint256 _nodeOperatorId) public view returns (bool) { - Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - return _isOperatorPenalized(stuckPenaltyStats); + function isOperatorPenalized(uint256 /* _nodeOperatorId */) public view returns (bool) { + // TODO: implement + return false; } - function isOperatorPenaltyCleared(uint256 _nodeOperatorId) public view returns (bool) { - Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - return !_isOperatorPenalized(stuckPenaltyStats) && stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET) == 0; - } - - function clearNodeOperatorPenalty(uint256 _nodeOperatorId) external returns (bool) { - Packed64x4.Packed memory stuckPenaltyStats = _loadOperatorStuckPenaltyStats(_nodeOperatorId); - require( - !_isOperatorPenalized(stuckPenaltyStats) && stuckPenaltyStats.get(STUCK_PENALTY_END_TIMESTAMP_OFFSET) != 0, - "CANT_CLEAR_PENALTY" - ); - stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, 0); - _saveOperatorStuckPenaltyStats(_nodeOperatorId, stuckPenaltyStats); - _updateSummaryMaxValidatorsCount(_nodeOperatorId); - _increaseValidatorsKeysNonce(); - - emit NodeOperatorPenaltyCleared(_nodeOperatorId); + function isOperatorPenaltyCleared(uint256 /* _nodeOperatorId */) public view returns (bool) { + // TODO: implement return true; } @@ -1472,13 +1350,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit RewardDistributionStateChanged(_state); } - /// @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); - } - function _increaseValidatorsKeysNonce() internal { uint256 keysOpIndex = KEYS_OP_INDEX_POSITION.getStorageUint256() + 1; KEYS_OP_INDEX_POSITION.setStorageUint256(keysOpIndex); @@ -1503,13 +1374,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _nodeOperators[_nodeOperatorId].targetValidatorsStats = _val; } - function _loadOperatorStuckPenaltyStats(uint256 _nodeOperatorId) internal view returns (Packed64x4.Packed memory) { - return _nodeOperators[_nodeOperatorId].stuckPenaltyStats; - } - - function _saveOperatorStuckPenaltyStats(uint256 _nodeOperatorId, Packed64x4.Packed memory _val) internal { - _nodeOperators[_nodeOperatorId].stuckPenaltyStats = _val; - } function _loadOperatorSigningKeysStats(uint256 _nodeOperatorId) internal view returns (Packed64x4.Packed memory) { return _nodeOperators[_nodeOperatorId].signingKeysStats; diff --git a/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol b/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol index e7378ad9d7..d7aa04f1c5 100644 --- a/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol +++ b/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol @@ -85,15 +85,10 @@ contract NodeOperatorsRegistry__Harness is NodeOperatorsRegistry { function harness__setNodeOperatorLimits( uint256 _nodeOperatorId, - uint64 stuckValidatorsCount, - uint64 refundedValidatorsCount, - uint64 stuckPenaltyEndAt + uint64, + uint64, + uint64 ) external { - Packed64x4.Packed memory stuckPenaltyStats = _nodeOperators[_nodeOperatorId].stuckPenaltyStats; - stuckPenaltyStats.set(STUCK_VALIDATORS_COUNT_OFFSET, stuckValidatorsCount); - stuckPenaltyStats.set(REFUNDED_VALIDATORS_COUNT_OFFSET, refundedValidatorsCount); - stuckPenaltyStats.set(STUCK_PENALTY_END_TIMESTAMP_OFFSET, stuckPenaltyEndAt); - _nodeOperators[_nodeOperatorId].stuckPenaltyStats = stuckPenaltyStats; _updateSummaryMaxValidatorsCount(_nodeOperatorId); } diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index a510d92ba7..eb1e9c402f 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -149,49 +149,14 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { }); it("Reverts if no such an operator exists", async () => { - await expect(nor.unsafeUpdateValidatorsCount(3n, 0n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); + await expect(nor.unsafeUpdateValidatorsCount(3n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); }); it("Reverts if has not STAKING_ROUTER_ROLE assigned", async () => { - await expect(nor.connect(stranger).unsafeUpdateValidatorsCount(firstNodeOperatorId, 0n, 0n)).to.be.revertedWith( + await expect(nor.connect(stranger).unsafeUpdateValidatorsCount(firstNodeOperatorId, 0n)).to.be.revertedWith( "APP_AUTH_FAILED", ); }); - - it("Can change stuck and exited keys arbitrary (even decreasing exited)", async () => { - const nonce = await nor.getNonce(); - - const beforeNOSummary = await nor.getNodeOperatorSummary(firstNodeOperatorId); - expect(beforeNOSummary.stuckValidatorsCount).to.equal(0n); - expect(beforeNOSummary.totalExitedValidators).to.equal(1n); - - await expect(nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperatorId, 3n, 2n)) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n) // doesn't affect stuck penalty deadline - .to.emit(nor, "ExitedSigningKeysCountChanged") - .withArgs(firstNodeOperatorId, 3n) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n); - - const middleNOSummary = await nor.getNodeOperatorSummary(firstNodeOperatorId); - expect(middleNOSummary.stuckValidatorsCount).to.equal(2n); - expect(middleNOSummary.totalExitedValidators).to.equal(3n); - - await expect(nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperatorId, 1n, 2n)) - .to.emit(nor, "ExitedSigningKeysCountChanged") - .withArgs(firstNodeOperatorId, 1n) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.not.emit(nor, "StuckPenaltyStateChanged"); - - const lastNOSummary = await nor.getNodeOperatorSummary(firstNodeOperatorId); - expect(lastNOSummary.stuckValidatorsCount).to.equal(2n); - expect(lastNOSummary.totalExitedValidators).to.equal(1n); - }); }); context("onWithdrawalCredentialsChanged", () => { diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 449330fd87..3e2d4442f0 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -32,7 +32,6 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const contractVersionV2 = 2n; - const contractVersionV3 = 3n; before(async () => { [deployer, user, stakingRouter, nodeOperatorsManager, signingKeysManager, limitsManager] = @@ -122,10 +121,6 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { await expect(nor.initialize(locator, moduleType, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) - .to.emit(nor, "ContractVersionSet") - .withArgs(contractVersionV3) - .and.to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(86400n) .and.to.emit(nor, "LocatorContractSet") .withArgs(await locator.getAddress()) .and.to.emit(nor, "StakingModuleTypeSet") @@ -136,7 +131,6 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { expect(await nor.getLocator()).to.equal(await locator.getAddress()); expect(await nor.getInitializationBlock()).to.equal(latestBlock + 1n); expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(MaxUint256); - expect(await nor.getStuckPenaltyDelay()).to.equal(86400n); expect(await nor.getContractVersion()).to.equal(4); expect(await nor.getType()).to.equal(moduleType); }); diff --git a/test/0.4.24/nor/nor.management.flow.test.ts b/test/0.4.24/nor/nor.management.flow.test.ts index d5c013c303..d7692899f5 100644 --- a/test/0.4.24/nor/nor.management.flow.test.ts +++ b/test/0.4.24/nor/nor.management.flow.test.ts @@ -6,7 +6,6 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { ACL, - Burner__MockForLidoHandleOracleReport, Kernel, Lido__HarnessForDistributeReward, LidoLocator, @@ -15,7 +14,6 @@ import { import { addNodeOperator, - advanceChainTime, certainAddress, ether, NodeOperatorConfig, @@ -26,7 +24,6 @@ import { import { addAragonApp, deployLidoDaoForNor } from "test/deploy"; import { Snapshot } from "test/suite"; -const ONE_DAY = 86400n; describe("NodeOperatorsRegistry.sol:management", () => { let deployer: HardhatEthersSigner; @@ -49,8 +46,6 @@ describe("NodeOperatorsRegistry.sol:management", () => { let originalState: string; - let burner: Burner__MockForLidoHandleOracleReport; - const firstNodeOperatorId = 0; const secondNodeOperatorId = 1; const thirdNodeOperatorId = 2; @@ -100,7 +95,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { [deployer, user, stakingRouter, nodeOperatorsManager, signingKeysManager, limitsManager, user1, user2, user3] = await ethers.getSigners(); - ({ lido, dao, acl, burner } = await deployLidoDaoForNor({ + ({ lido, dao, acl } = await deployLidoDaoForNor({ rootAccount: deployer, initialized: true, locatorConfig: { @@ -626,9 +621,6 @@ describe("NodeOperatorsRegistry.sol:management", () => { }); context("distributeReward()", () => { - const firstNodeOperator = 0; - const secondNodeOperator = 1; - beforeEach(async () => { await nor.harness__addNodeOperator("0", user1, 3, 3, 3, 0); await nor.harness__addNodeOperator("1", user2, 7, 7, 7, 0); @@ -700,151 +692,6 @@ describe("NodeOperatorsRegistry.sol:management", () => { .and.to.emit(nor, "RewardsDistributed") .withArgs(await user2.getAddress(), ether("7")); }); - - it("distribute with stopped works", async () => { - const totalRewardShares = ether("10"); - - await lido.setTotalPooledEther(ether("100")); - await lido.mintShares(await nor.getAddress(), totalRewardShares); - - // before - // operatorId | Total | Deposited | Exited | Active (deposited-exited) - // 0 3 3 0 3 - // 1 7 7 0 7 - // 2 0 0 0 0 - // ----------------------------------------------------------------------------- - // total 3 10 10 0 10 - // - // perValidatorShare 10*10^18 / 10 = 10^18 - - // update [operator, exited, stuck] - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperator, 1, 0); - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(secondNodeOperator, 1, 0); - - // after - // operatorId | Total | Deposited | Exited | Stuck | Active (deposited-exited) - // 0 3 3 1 0 2 - // 1 7 7 1 0 6 - // 2 0 0 0 0 0 - // ----------------------------------------------------------------------------- - // total 3 10 10 2 0 8 - // - // perValidatorShare 10*10^18 / 8 = 1250000000000000000 == 1.25 * 10^18 - - await expect(nor.distributeReward()) - .to.emit(nor, "RewardsDistributed") - .withArgs(await user1.getAddress(), ether(2 * 1.25 + "")) - .and.to.emit(nor, "RewardsDistributed") - .withArgs(await user2.getAddress(), ether(6 * 1.25 + "")); - }); - - it("penalized keys with stopped and stuck works", async () => { - const totalRewardShares = ether("10"); - - await lido.setTotalPooledEther(ether("100")); - await lido.mintShares(await nor.getAddress(), totalRewardShares); - - // before - // operatorId | Total | Deposited | Exited | Active (deposited-exited) - // 0 3 3 0 3 - // 1 7 7 0 7 - // 2 0 0 0 0 - // ----------------------------------------------------------------------------- - // total 3 10 10 0 10 - // - // perValidatorShare 10*10^18 / 10 = 10^18 - - // update [operator, exited, stuck] - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperator, 1, 1); - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(secondNodeOperator, 1, 0); - - // after - // operatorId | Total | Deposited | Exited | Stuck | Active (deposited-exited) - // 0 3 3 1 1 2 - // 1 7 7 1 0 6 - // 2 0 0 0 0 0 - // ----------------------------------------------------------------------------- - // total 3 10 10 2 1 8 - // - // perValidatorShare 10*10^18 / 8 = 1250000000000000000 == 1.25 * 10^18 - // but half goes to burner - await expect(await nor.getRewardDistributionState()).to.be.equal(RewardDistributionState.ReadyForDistribution); - - await expect(nor.distributeReward()) - .to.emit(nor, "RewardsDistributed") - .withArgs(await user1.getAddress(), ether(1.25 + "")) - .and.to.emit(nor, "RewardsDistributed") - .withArgs(await user2.getAddress(), ether(6 * 1.25 + "")) - .and.to.emit(nor, "NodeOperatorPenalized") - .withArgs(await user1.getAddress(), ether(1.25 + "")) - .and.to.emit(burner, "StETHBurnRequested") - .withArgs(false, await nor.getAddress(), ether("2.5"), ether("1.25")); - }); - - it("penalized firstOperator, add refund but 2 days have not passed yet", async () => { - await lido.setTotalPooledEther(ether("100")); - await lido.mintShares(await nor.getAddress(), ether("10")); - - // update [operator, exited, stuck] - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperator, 1, 1); - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(secondNodeOperator, 1, 0); - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperator, 1); - - await expect(nor.distributeReward()) - .to.emit(nor, "RewardsDistributed") - .withArgs(await user1.getAddress(), ether(1.25 + "")) - .and.to.emit(nor, "RewardsDistributed") - .withArgs(await user2.getAddress(), ether(6 * 1.25 + "")) - .and.to.emit(nor, "NodeOperatorPenalized") - .withArgs(await user1.getAddress(), ether(1.25 + "")); - }); - - it("penalized firstOperator, add refund less than stuck validators", async () => { - await lido.setTotalPooledEther(ether("100")); - await lido.mintShares(await nor.getAddress(), ether("10")); - - // update [operator, exited, stuck] - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperator, 2, 1); - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(secondNodeOperator, 3, 0); - - // perValidator = ETH(10) / 5 = 2 eth - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperator, 1); - - await expect(nor.distributeReward()) - .to.emit(nor, "RewardsDistributed") - .withArgs(await user1.getAddress(), ether(1 + "")) - .and.to.emit(nor, "RewardsDistributed") - .withArgs(await user2.getAddress(), ether(4 * 2 + "")) - .and.to.emit(nor, "NodeOperatorPenalized") - .withArgs(await user1.getAddress(), ether(1 + "")); - }); - - it("penalized firstOperator, add refund and 2 days passed", async () => { - await lido.setTotalPooledEther(ether("100")); - await lido.mintShares(await nor.getAddress(), ether("10")); - - expect(await nor.isOperatorPenalized(firstNodeOperator)).to.be.false; - - // update [operator, exited, stuck] - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperator, 1, 1); - await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(secondNodeOperator, 1, 0); - expect(await nor.isOperatorPenalized(firstNodeOperator)).to.be.true; - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperator, 1); - expect(await nor.isOperatorPenalized(firstNodeOperator)).to.be.true; - - await advanceChainTime(2n * ONE_DAY); - - expect(await nor.isOperatorPenalized(firstNodeOperator)).to.be.false; - - await expect(nor.distributeReward()) - .to.emit(nor, "RewardsDistributed") - .withArgs(await user1.getAddress(), ether(2.5 + "")) - .and.to.emit(nor, "RewardsDistributed") - .withArgs(await user2.getAddress(), ether(7.5 + "")); - }); }); context("getNodeOperatorIds", () => { @@ -961,54 +808,4 @@ describe("NodeOperatorsRegistry.sol:management", () => { expect(await nor.getLocator()).to.equal(ZeroAddress); }); }); - - context("getStuckPenaltyDelay", () => { - it("Returns stuck penalty delay", async () => { - expect(await nor.getStuckPenaltyDelay()).to.equal(penaltyDelay); - }); - - it("Allows reading the changed stuck penalty delay", async () => { - const maxStuckPenaltyDelay = await nor.MAX_STUCK_PENALTY_DELAY(); - - await nor.harness__setStuckPenaltyDelay(maxStuckPenaltyDelay); - expect(await nor.getStuckPenaltyDelay()).to.equal(maxStuckPenaltyDelay); - }); - - it("Allows reading zero stuck penalty delay", async () => { - await nor.harness__setStuckPenaltyDelay(0n); - expect(await nor.getStuckPenaltyDelay()).to.equal(0n); - }); - }); - - context("setStuckPenaltyDelay", () => { - it("Reverts if has no MANAGE_NODE_OPERATOR_ROLE assigned", async () => { - await expect(nor.setStuckPenaltyDelay(86400n)).to.be.revertedWith("APP_AUTH_FAILED"); - }); - - it("Reverts if invalid range value provided", async () => { - const maxStuckPenaltyDelay = await nor.MAX_STUCK_PENALTY_DELAY(); - - await expect( - nor.connect(nodeOperatorsManager).setStuckPenaltyDelay(maxStuckPenaltyDelay + 1n), - ).to.be.revertedWith("OUT_OF_RANGE"); - }); - - it("Sets a new value for the stuck penalty delay", async () => { - await expect(nor.connect(nodeOperatorsManager).setStuckPenaltyDelay(7200n)) - .to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(7200n); - - const stuckPenaltyDelay = await nor.getStuckPenaltyDelay(); - expect(stuckPenaltyDelay).to.equal(7200n); - }); - - it("Allows setting a zero delay", async () => { - await expect(nor.connect(nodeOperatorsManager).setStuckPenaltyDelay(0n)) - .to.emit(nor, "StuckPenaltyDelayChanged") - .withArgs(0n); - - const stuckPenaltyDelay = await nor.getStuckPenaltyDelay(); - expect(stuckPenaltyDelay).to.equal(0n); - }); - }); }); diff --git a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts index bac2e8dad7..0bdbcafe42 100644 --- a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts +++ b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts @@ -3,15 +3,12 @@ import { encodeBytes32String } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { time } from "@nomicfoundation/hardhat-network-helpers"; import { ACL, Kernel, Lido, LidoLocator, NodeOperatorsRegistry__Harness } from "typechain-types"; import { addNodeOperator, - advanceChainTime, certainAddress, - ether, NodeOperatorConfig, prepIdsCountsPayload, RewardDistributionState, @@ -161,139 +158,6 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { }); }); - context("updateStuckValidatorsCount", () => { - beforeEach(async () => { - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.equal( - secondNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[thirdNodeOperatorId])).to.equal( - thirdNodeOperatorId, - ); - - await nor.connect(nodeOperatorsManager).setStuckPenaltyDelay(86400n); - }); - - it("Reverts if has no STAKING_ROUTER_ROLE assigned", async () => { - const idsPayload = prepIdsCountsPayload([], []); - await expect(nor.updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)).to.be.revertedWith( - "APP_AUTH_FAILED", - ); - }); - - it("Reverts with INVALID_REPORT_DATA if report data is malformed", async () => { - const idsPayload = prepIdsCountsPayload([1n], [1n]); - - const malformedKeys = idsPayload.keysCounts + "00"; - await expect( - nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, malformedKeys), - ).to.be.revertedWith("INVALID_REPORT_DATA"); - }); - - it("Allows calling with zero length data", async () => { - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([], []); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.not.emit(nor, "StuckPenaltyStateChanged"); - }); - - it("Allows updating a single NO", async () => { - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n); - }); - - it("Allows updating a group of NOs", async () => { - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId), BigInt(secondNodeOperatorId)], [2n, 3n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 3n, 0n, 0n); - }); - - it("Does nothing if stuck keys haven't changed", async () => { - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n); - - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.not.emit(nor, "StuckPenaltyStateChanged"); - }); - - it("Allows setting stuck count to zero after all", async () => { - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n); - - const idsPayloadZero = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [0n]); - - const timestamp = BigInt(await time.latest()); - await expect( - nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayloadZero.operatorIds, idsPayloadZero.keysCounts), - ) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 0n, 0n, timestamp + 86400n + 1n); - }); - - it("Penalizes node operators with stuck penalty active", async () => { - await lido.connect(user).resume(); - await user.sendTransaction({ to: await lido.getAddress(), value: ether("1.0") }); - await lido.connect(user).transfer(await nor.getAddress(), await lido.balanceOf(user)); - - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([1n], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(1n, 2n, 0n, 0n); - - await nor.harness__setRewardDistributionState(RewardDistributionState.ReadyForDistribution); - await expect(nor.connect(stakingRouter).distributeReward()).to.emit(nor, "NodeOperatorPenalized"); - }); - }); - context("updateExitedValidatorsCount", () => { beforeEach(async () => { expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( @@ -305,8 +169,6 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[thirdNodeOperatorId])).to.equal( thirdNodeOperatorId, ); - - await nor.connect(nodeOperatorsManager).setStuckPenaltyDelay(86400n); }); it("Reverts if has no STAKING_ROUTER_ROLE assigned", async () => { @@ -404,78 +266,6 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { }); }); - context("updateRefundedValidatorsCount", () => { - beforeEach(async () => { - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.equal( - secondNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[thirdNodeOperatorId])).to.equal( - thirdNodeOperatorId, - ); - - await nor.connect(nodeOperatorsManager).setStuckPenaltyDelay(86400n); - }); - - it("Revers if no such an operator exists", async () => { - await expect(nor.updateRefundedValidatorsCount(4n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); - }); - - it("Reverts if has no STAKING_ROUTER_ROLE assigned", async () => { - await expect(nor.updateRefundedValidatorsCount(firstNodeOperatorId, 0n)).to.be.revertedWith("APP_AUTH_FAILED"); - }); - - it("Allows updating a single NO", async () => { - const nonce = await nor.getNonce(); - await expect(nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n)) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 0n, 1n, 0n) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n); - }); - - it("Does nothing if refunded keys haven't changed", async () => { - const nonce = await nor.getNonce(); - await expect(nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n)) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 0n, 1n, 0n) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n); - - await expect(nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.not.emit(nor, "StuckPenaltyStateChanged"); - }); - - it("Allows setting refunded count to zero after all", async () => { - const nonce = await nor.getNonce(); - await expect(nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n)) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 0n, 1n, 0n) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n); - - await expect(nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 0n)) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 0n, 0n, 0n) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n); - }); - }); - context("onExitedAndStuckValidatorsCountsUpdated", () => { beforeEach(async () => { expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( @@ -504,264 +294,6 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { }); }); - context("isOperatorPenalized", () => { - beforeEach(async () => { - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.equal( - secondNodeOperatorId, - ); - }); - - it("Returns false if no such an operator exists", async () => { - await expect(await nor.isOperatorPenalized(10n)).to.be.false; - }); - - it("Returns false for non-penalized operator", async () => { - expect(await nor.isOperatorPenalized(firstNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenalized(secondNodeOperatorId)).to.be.false; - }); - - it("Returns true if stuck > refunded", async () => { - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n); - - const nonce = await nor.getNonce(); - let idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 1n, 0n); - - idsPayload = prepIdsCountsPayload([BigInt(secondNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 2n, 0n, 0n); - - expect(await nor.isOperatorPenalized(firstNodeOperatorId)).to.be.true; - expect(await nor.isOperatorPenalized(secondNodeOperatorId)).to.be.true; - }); - - it("Returns true if penalty hasn't ended yet", async () => { - const nonce = await nor.getNonce(); - let idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n); - - idsPayload = prepIdsCountsPayload([BigInt(secondNodeOperatorId)], [3n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 3n, 0n, 0n); - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 2n); - await nor.connect(stakingRouter).updateRefundedValidatorsCount(secondNodeOperatorId, 3n); - - expect(await nor.isOperatorPenalized(firstNodeOperatorId)).to.be.true; - expect(await nor.isOperatorPenalized(secondNodeOperatorId)).to.be.true; - }); - }); - - context("isOperatorPenaltyCleared", () => { - beforeEach(async () => { - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.equal( - secondNodeOperatorId, - ); - }); - - it("Returns true if no such an operator exists", async () => { - await expect(await nor.isOperatorPenaltyCleared(10n)).to.be.true; - }); - - it("Returns true for non-penalized operator", async () => { - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.true; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.true; - }); - - it("Returns false if stuck > refunded", async () => { - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n); - - const nonce = await nor.getNonce(); - let idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 1n, 0n); - - idsPayload = prepIdsCountsPayload([BigInt(secondNodeOperatorId)], [2n]); - - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 2n, 0n, 0n); - - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.false; - }); - - it("Returns true if penalty hasn't ended yet", async () => { - const nonce = await nor.getNonce(); - let idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n); - - idsPayload = prepIdsCountsPayload([BigInt(secondNodeOperatorId)], [3n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 3n, 0n, 0n); - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 2n); - await nor.connect(stakingRouter).updateRefundedValidatorsCount(secondNodeOperatorId, 3n); - - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.false; - - await advanceChainTime((await nor.getStuckPenaltyDelay()) + 1n); - - await nor.clearNodeOperatorPenalty(firstNodeOperatorId); - - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.true; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.false; - - await nor.clearNodeOperatorPenalty(secondNodeOperatorId); - - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.true; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.true; - }); - }); - - context("clearNodeOperatorPenalty", () => { - beforeEach(async () => { - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.equal( - firstNodeOperatorId, - ); - expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[secondNodeOperatorId])).to.equal( - secondNodeOperatorId, - ); - }); - - it("Reverts if no such an operator exists", async () => { - await expect(nor.clearNodeOperatorPenalty(10n)).to.be.revertedWith("CANT_CLEAR_PENALTY"); - }); - - it("Reverts if hasn't been penalized yet", async () => { - await expect(nor.clearNodeOperatorPenalty(firstNodeOperatorId)).to.be.revertedWith("CANT_CLEAR_PENALTY"); - await expect(nor.clearNodeOperatorPenalty(secondNodeOperatorId)).to.be.revertedWith("CANT_CLEAR_PENALTY"); - }); - - it("Reverts if the penalty delay hasn't passed yet", async () => { - const nonce = await nor.getNonce(); - let idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [1n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 1n, 0n, 0n); - - idsPayload = prepIdsCountsPayload([BigInt(secondNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 2n, 0n, 0n); - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 1n); - await nor.connect(stakingRouter).updateRefundedValidatorsCount(secondNodeOperatorId, 2n); - - await expect(nor.clearNodeOperatorPenalty(firstNodeOperatorId)).to.be.revertedWith("CANT_CLEAR_PENALTY"); - await expect(nor.clearNodeOperatorPenalty(secondNodeOperatorId)).to.be.revertedWith("CANT_CLEAR_PENALTY"); - }); - - it("Clear the penalized state", async () => { - const nonce = await nor.getNonce(); - let idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [3n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 3n, 0n, 0n); - - idsPayload = prepIdsCountsPayload([BigInt(secondNodeOperatorId)], [4n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 2n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 2n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(secondNodeOperatorId, 4n, 0n, 0n); - - await nor.connect(stakingRouter).updateRefundedValidatorsCount(firstNodeOperatorId, 5n); - await nor.connect(stakingRouter).updateRefundedValidatorsCount(secondNodeOperatorId, 5n); - - await advanceChainTime((await nor.getStuckPenaltyDelay()) + 1n); - - expect(await nor.isOperatorPenalized(firstNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenalized(secondNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.false; - - await expect(await nor.clearNodeOperatorPenalty(firstNodeOperatorId)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 5n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 5n) - .to.emit(nor, "NodeOperatorPenaltyCleared") - .withArgs(firstNodeOperatorId); - - await expect(await nor.clearNodeOperatorPenalty(secondNodeOperatorId)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 6n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 6n) - .to.emit(nor, "NodeOperatorPenaltyCleared") - .withArgs(secondNodeOperatorId); - - expect(await nor.isOperatorPenalized(firstNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenalized(secondNodeOperatorId)).to.be.false; - expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.true; - expect(await nor.isOperatorPenaltyCleared(secondNodeOperatorId)).to.be.true; - }); - }); - context("getRewardsDistribution", () => { it("Returns empty lists if no operators", async () => { const [recipients, shares, penalized] = await nor.getRewardsDistribution(10n); @@ -814,16 +346,6 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { thirdNodeOperatorId, ); - const nonce = await nor.getNonce(); - const idsPayload = prepIdsCountsPayload([BigInt(firstNodeOperatorId)], [2n]); - await expect(nor.connect(stakingRouter).updateStuckValidatorsCount(idsPayload.operatorIds, idsPayload.keysCounts)) - .to.emit(nor, "KeysOpIndexSet") - .withArgs(nonce + 1n) - .to.emit(nor, "NonceChanged") - .withArgs(nonce + 1n) - .to.emit(nor, "StuckPenaltyStateChanged") - .withArgs(firstNodeOperatorId, 2n, 0n, 0n); - const [recipients, shares, penalized] = await nor.getRewardsDistribution(100n); expect(recipients.length).to.be.equal(2n); @@ -840,11 +362,9 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { expect(recipients[0]).to.be.equal(NODE_OPERATORS[firstNodeOperatorId].rewardAddress); expect(shares[0]).to.be.equal((100n * firstNOActiveKeys) / totalActiveKeys); - expect(penalized[0]).to.be.equal(true); expect(recipients[1]).to.be.equal(NODE_OPERATORS[secondNodeOperatorId].rewardAddress); expect(shares[1]).to.be.equal((100n * secondNOActiveKeys) / totalActiveKeys); - expect(penalized[1]).to.be.equal(false); }); }); }); From 169da9b9a79005b7734f1fd85cce094fc0417662 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 13:23:19 +0200 Subject: [PATCH 086/405] wip: remove old stuck methods --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 41 ++----------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 9331b7fb63..52d676d34e 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -241,15 +241,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { initialized(); } - function _initialize_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) internal { + function _initialize_v2(address _locator, bytes32 _type, uint256 /* _stuckPenaltyDelay */) internal { _onlyNonZeroAddress(_locator); LIDO_LOCATOR_POSITION.setStorageAddress(_locator); TYPE_POSITION.setStorageBytes32(_type); _setContractVersion(2); - - _setStuckPenaltyDelay(_stuckPenaltyDelay); - // set unlimited allowance for burner from staking router // to burn stuck keys penalized shares IStETH(getLocator().lido()).approve(getLocator().burner(), ~uint256(0)); @@ -499,17 +496,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _increaseValidatorsKeysNonce(); } - /// @notice Updates the number of the refunded validators for node operator with the given id - /// @param _nodeOperatorId Id of the node operator - /// @param _refundedValidatorsCount New number of refunded validators of the node operator - function updateRefundedValidatorsCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) external { - _onlyExistedNodeOperator(_nodeOperatorId); - _auth(STAKING_ROUTER_ROLE); - - _updateRefundValidatorsKeysCount(_nodeOperatorId, _refundedValidatorsCount); - _increaseValidatorsKeysNonce(); - } - /// @notice Permissionless method for distributing all accumulated module rewards among node operators /// based on the latest accounting report. /// @@ -548,11 +534,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// 'unsafely' means that this method can both increase and decrease exited and stuck counters /// @param _nodeOperatorId Id of the node operator /// @param _exitedValidatorsCount New number of EXITED validators for the node operator - /// @param _stuckValidatorsCount New number of STUCK validator for the node operator function unsafeUpdateValidatorsCount( uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount, - uint256 _stuckValidatorsCount + uint256 _exitedValidatorsCount ) external { _onlyExistedNodeOperator(_nodeOperatorId); _auth(STAKING_ROUTER_ROLE); @@ -1211,7 +1195,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { targetLimitMode = operatorTargetStats.get(TARGET_LIMIT_MODE_OFFSET); targetValidatorsCount = operatorTargetStats.get(TARGET_VALIDATORS_COUNT_OFFSET); stuckValidatorsCount = 0; - refundedValidatorsCount = stuckPenaltyStats.get(REFUNDED_VALIDATORS_COUNT_OFFSET); + refundedValidatorsCount = 0; stuckPenaltyEndTimestamp = 0; (totalExitedValidators, totalDepositedValidators, depositableValidatorsCount) = @@ -1300,20 +1284,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return; } - (address[] memory recipients, uint256[] memory shares, bool[] memory penalized) = + (address[] memory recipients, uint256[] memory shares,) = getRewardsDistribution(sharesToDistribute); 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; - if (penalized[idx]) { - /// @dev half reward punishment - /// @dev ignore remainder since it accumulated on contract balance - shares[idx] >>= 1; - toBurn = toBurn.add(shares[idx]); - emit NodeOperatorPenalized(recipients[idx], shares[idx]); - } // TODO: apply penalty to the operator stETH.transferShares(recipients[idx], shares[idx]); distributed = distributed.add(shares[idx]); @@ -1328,16 +1305,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return ILidoLocator(LIDO_LOCATOR_POSITION.getStorageAddress()); } - function getStuckPenaltyDelay() public view returns (uint256) { - return STUCK_PENALTY_DELAY_POSITION.getStorageUint256(); - } - - function setStuckPenaltyDelay(uint256 _delay) external { - _auth(MANAGE_NODE_OPERATOR_ROLE); - - _setStuckPenaltyDelay(_delay); - } - /// @dev Get the current reward distribution state, anyone can monitor this state /// and distribute reward (call distributeReward method) among operators when it's `ReadyForDistribution` function getRewardDistributionState() public view returns (RewardDistributionState) { From b5903a9a1e50a26e9edbf576ece2833b60fc7275 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 13:29:27 +0200 Subject: [PATCH 087/405] fix: remove unused file --- scripts/triggerable-withdrawals/tw-deploy.js | 290 ------------------- 1 file changed, 290 deletions(-) delete mode 100644 scripts/triggerable-withdrawals/tw-deploy.js diff --git a/scripts/triggerable-withdrawals/tw-deploy.js b/scripts/triggerable-withdrawals/tw-deploy.js deleted file mode 100644 index 553aaeca4a..0000000000 --- a/scripts/triggerable-withdrawals/tw-deploy.js +++ /dev/null @@ -1,290 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const dotenv = __importStar(require("dotenv")); -const hardhat_1 = require("hardhat"); -const path_1 = require("path"); -const readline_1 = __importDefault(require("readline")); -const typechain_types_1 = require("typechain-types"); -const lib_1 = require("lib"); -dotenv.config({ path: (0, path_1.join)(__dirname, "../../.env") }); -function getEnvVariable(name, defaultValue) { - const value = process.env[name]; - if (value === undefined) { - if (defaultValue === undefined) { - throw new Error(`Env variable ${name} must be set`); - } - return defaultValue; - } - else { - (0, lib_1.log)(`Using env variable ${name}=${value}`); - return value; - } -} -/* Accounting Oracle args */ -// Must comply with the specification -// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 -const SECONDS_PER_SLOT = 12; -// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis -// and the current value: https://etherscan.io/address/0x852deD011285fe67063a08005c71a85690503Cee#readProxyContract#F6 -const GENESIS_TIME = 1606824023; -/* Oracle report sanity checker */ -// Defines the maximum number of validators that may be reported as "exited" -// per day, depending on the consensus layer churn limit. -// -// CURRENT_ACTIVE_VALIDATORS_NUMBER = ~1100000 // https://beaconcha.in/ -// CURRENT_EXIT_CHURN_LIMIT = 16 // https://www.validatorqueue.com/ -// EPOCHS_PER_DAY = 225 // (24 * 60 * 60) sec / 12 sec per slot / 32 slots per epoch -// -// https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/beacon-chain.md#validator-cycle -// MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT = 8 -// -// MAX_VALIDATORS_PER_DAY = EPOCHS_PER_DAY * MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT = 1800 // 225 * 8 -// MAX_VALIDATORS_AFTER_TWO_YEARS = MAX_VALIDATORS_PER_DAY * 365 * 2 + CURRENT_VALIDATORS_NUMBER // 1100000 + (1800 * 365 * 2) = ~2500000 -// -// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator-cycle -// CHURN_LIMIT_QUOTIENT = 65536 -// -// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_validator_churn_limit -// MAX_EXIT_CHURN_LIMIT_AFTER_TWO_YEARS = MAX_VALIDATORS_AFTER_TWO_YEARS / CHURN_LIMIT_QUOTIENT // 2500000 / 65536 = ~38 -// EXITED_VALIDATORS_PER_DAY_LIMIT = MAX_EXIT_CHURN_LIMIT_AFTER_TWO_YEARS * EPOCHS_PER_DAY // 38 * 225 = 8550 = ~9000 -const EXITED_VALIDATORS_PER_DAY_LIMIT = 9000; -// Defines the maximum number of validators that can be reported as "appeared" -// in a single day, limited by the maximum daily deposits via DSM -// -// BLOCKS_PER_DAY = (24 * 60 * 60) / 12 = 7200 -// MAX_DEPOSITS_PER_BLOCK = 150 -// MIN_DEPOSIT_BLOCK_DISTANCE = 25 -// -// APPEARED_VALIDATORS_PER_DAY_LIMIT = BLOCKS_PER_DAY / MIN_DEPOSIT_BLOCK_DISTANCE * MAX_DEPOSITS_PER_BLOCK = 43200 -// Current limits: https://etherscan.io/address/0xC77F8768774E1c9244BEed705C4354f2113CFc09#readContract#F10 -// https://etherscan.io/address/0xC77F8768774E1c9244BEed705C4354f2113CFc09#readContract#F11 -// The proposed limits remain unchanged for curated modules and reduced for CSM -const APPEARED_VALIDATORS_PER_DAY_LIMIT = 43200; -// Must match the current value https://docs.lido.fi/guides/verify-lido-v2-upgrade-manual/#oraclereportsanitychecker -const ANNUAL_BALANCE_INCREASE_BP_LIMIT = 1000; -const SIMULATED_SHARE_RATE_DEVIATION_BP_LIMIT = 50; -const MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT = 600; -// The optimal number of items is greater than 6 (2 items for stuck or exited keys per 3 modules) to ensure -// a small report can fit into a single transaction. However, there is additional capacity in case a module -// requires more than 2 items. Hence, the limit of 8 items per report was chosen. -const MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION = 8; -// This parameter defines the maximum number of node operators that can be reported per extra data list item. -// Gas consumption for updating a single node operator: -// -// - CSM: -// Average: ~16,650 gas -// Max: ~41,150 gas (in cases with unstuck keys under specific conditions) -// - Curated-based: ~15,500 gas -// -// Each transaction can contain up to 8 items, and each item is limited to a maximum of 1,000,000 gas. -// Thus, the total gas consumption per transaction remains within 8,000,000 gas. -// Using the higher value of CSM (41,150 gas), the calculation is as follows: -// -// Operators per item: 1,000,000 / 41,150 = 24.3 -// Thus, the limit was set at 24 operators per item. -const MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM = 24; -// Must match the current value https://docs.lido.fi/guides/verify-lido-v2-upgrade-manual/#oraclereportsanitychecker -const REQUEST_TIMESTAMP_MARGIN = 7680; -const MAX_POSITIVE_TOKEN_REBASE = 750000; -// Must match the value in LIP-23 https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-23.md -// and the proposed number on the research forum https://research.lido.fi/t/staking-router-community-staking-module-upgrade-announcement/8612 -const INITIAL_SLASHING_AMOUNT_P_WEI = 1000; -const INACTIVITY_PENALTIES_AMOUNT_P_WEI = 101; -// Must match the proposed number on the research forum https://research.lido.fi/t/staking-router-community-staking-module-upgrade-announcement/8612 -const CL_BALANCE_ORACLES_ERROR_UPPER_BP_LIMIT = 50; -const LIMITS = [ - EXITED_VALIDATORS_PER_DAY_LIMIT, - APPEARED_VALIDATORS_PER_DAY_LIMIT, - ANNUAL_BALANCE_INCREASE_BP_LIMIT, - SIMULATED_SHARE_RATE_DEVIATION_BP_LIMIT, - MAX_VALIDATOR_EXIT_REQUESTS_PER_REPORT, - MAX_ITEMS_PER_EXTRA_DATA_TRANSACTION, - MAX_NODE_OPERATORS_PER_EXTRA_DATA_ITEM, - REQUEST_TIMESTAMP_MARGIN, - MAX_POSITIVE_TOKEN_REBASE, - INITIAL_SLASHING_AMOUNT_P_WEI, - INACTIVITY_PENALTIES_AMOUNT_P_WEI, - CL_BALANCE_ORACLES_ERROR_UPPER_BP_LIMIT, -]; -async function main() { - const deployer = hardhat_1.ethers.getAddress(getEnvVariable("DEPLOYER")); - const chainId = (await hardhat_1.ethers.provider.getNetwork()).chainId; - (0, lib_1.log)((0, lib_1.cy)(`Deploy of contracts on chain ${chainId}`)); - const state = (0, lib_1.readNetworkState)(); - (0, lib_1.persistNetworkState)(state); - // Read contracts addresses from config - const APP_AGENT_ADDRESS = state[lib_1.Sk.appAgent].proxy.address; - const SC_ADMIN = APP_AGENT_ADDRESS; - const locatorImplContract = await (0, lib_1.loadContract)("LidoLocator", typechain_types_1.INTERMEDIATE_LOCATOR_IMPL); - const ACCOUNTING_ORACLE_PROXY = await locatorImplContract.accountingOracle(); - const VEBO = await locatorImplContract.validatorsExitBusOracle(); - const WITHDRAWAL_VAULT = await locatorImplContract.withdrawalVault(); - // Deploy ValidatorExitBusOracle - const validatorsExitBusOracle = (await (0, lib_1.deployWithoutProxy)(lib_1.Sk.validatorsExitBusOracle, "ValidatorsExitBusOracle", deployer)).address; - lib_1.log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle}`); - lib_1.log.emptyLine(); - // Deploy WithdrawalVault - const withdrawalVault = (await (0, lib_1.deployImplementation)(lib_1.Sk.withdrawalVault, "WithdrawalVault", deployer, [], { libraries })).address; - lib_1.log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); - lib_1.log.emptyLine(); - (0, lib_1.updateObjectInState)(lib_1.Sk.appSimpleDvt, { - implementation: { - contract: "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - address: appNodeOperatorsRegistry, - constructorArgs: [], - }, - }); - // Deploy DSM - const depositSecurityModuleParams = [ - LIDO, - DEPOSIT_CONTRACT_ADDRESS, - STAKING_ROUTER, - PAUSE_INTENT_VALIDITY_PERIOD_BLOCKS, - MAX_OPERATORS_PER_UNVETTING, - ]; - const depositSecurityModuleAddress = (await (0, lib_1.deployWithoutProxy)(lib_1.Sk.depositSecurityModule, "DepositSecurityModule", deployer, depositSecurityModuleParams)).address; - lib_1.log.success(`New DSM address: ${depositSecurityModuleAddress}`); - lib_1.log.emptyLine(); - const dsmContract = await (0, lib_1.loadContract)("DepositSecurityModule", depositSecurityModuleAddress); - await dsmContract.addGuardians(GUARDIANS, QUORUM); - await dsmContract.setOwner(APP_AGENT_ADDRESS); - lib_1.log.success(`Guardians list: ${await dsmContract.getGuardians()}`); - lib_1.log.success(`Quorum: ${await dsmContract.getGuardianQuorum()}`); - lib_1.log.emptyLine(); - // Deploy AO - const accountingOracleArgs = [LOCATOR, LIDO, LEGACY_ORACLE, SECONDS_PER_SLOT, GENESIS_TIME]; - const accountingOracleAddress = (await (0, lib_1.deployImplementation)(lib_1.Sk.accountingOracle, "AccountingOracle", deployer, accountingOracleArgs)).address; - lib_1.log.success(`AO implementation address: ${accountingOracleAddress}`); - lib_1.log.emptyLine(); - // Deploy OracleReportSanityCheckerArgs - const oracleReportSanityCheckerArgs = [LOCATOR, SC_ADMIN, LIMITS]; - const oracleReportSanityCheckerAddress = (await (0, lib_1.deployWithoutProxy)(lib_1.Sk.oracleReportSanityChecker, "OracleReportSanityChecker", deployer, oracleReportSanityCheckerArgs)).address; - lib_1.log.success(`OracleReportSanityChecker new address ${oracleReportSanityCheckerAddress}`); - lib_1.log.emptyLine(); - const locatorConfig = [ - [ - ACCOUNTING_ORACLE_PROXY, - depositSecurityModuleAddress, - EL_REWARDS_VAULT, - LEGACY_ORACLE, - LIDO, - oracleReportSanityCheckerAddress, - POST_TOKEN_REBASE_RECEIVER, - BURNER, - STAKING_ROUTER, - TREASURY_ADDRESS, - VEBO, - WQ, - WITHDRAWAL_VAULT, - ORACLE_DAEMON_CONFIG, - ], - ]; - const locatorAddress = (await (0, lib_1.deployImplementation)(lib_1.Sk.lidoLocator, "LidoLocator", deployer, locatorConfig)).address; - lib_1.log.success(`Locator implementation address ${locatorAddress}`); - lib_1.log.emptyLine(); - if (getEnvVariable("RUN_ON_FORK", "false") === "true") { - (0, lib_1.log)((0, lib_1.cy)("Deploy script was executed on fork, will skip verification")); - return; - } - await waitForPressButton(); - (0, lib_1.log)((0, lib_1.cy)("Continuing...")); - await (0, hardhat_1.run)("verify:verify", { - address: minFirstAllocationStrategyAddress, - constructorArguments: [], - contract: "contracts/common/lib/MinFirstAllocationStrategy.sol:MinFirstAllocationStrategy", - }); - await (0, hardhat_1.run)("verify:verify", { - address: stakingRouterAddress, - constructorArguments: [DEPOSIT_CONTRACT_ADDRESS], - libraries: { - MinFirstAllocationStrategy: minFirstAllocationStrategyAddress, - }, - contract: "contracts/0.8.9/StakingRouter.sol:StakingRouter", - }); - await (0, hardhat_1.run)("verify:verify", { - address: appNodeOperatorsRegistry, - constructorArguments: [], - libraries: { - MinFirstAllocationStrategy: minFirstAllocationStrategyAddress, - }, - contract: "contracts/0.4.24/nos/NodeOperatorsRegistry.sol:NodeOperatorsRegistry", - }); - await (0, hardhat_1.run)("verify:verify", { - address: depositSecurityModuleAddress, - constructorArguments: depositSecurityModuleParams, - contract: "contracts/0.8.9/DepositSecurityModule.sol:DepositSecurityModule", - }); - await (0, hardhat_1.run)("verify:verify", { - address: accountingOracleAddress, - constructorArguments: accountingOracleArgs, - contract: "contracts/0.8.9/oracle/AccountingOracle.sol:AccountingOracle", - }); - await (0, hardhat_1.run)("verify:verify", { - address: oracleReportSanityCheckerAddress, - constructorArguments: oracleReportSanityCheckerArgs, - contract: "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol:OracleReportSanityChecker", - }); - await (0, hardhat_1.run)("verify:verify", { - address: locatorAddress, - constructorArguments: locatorConfig, - contract: "contracts/0.8.9/LidoLocator.sol:LidoLocator", - }); -} -async function waitForPressButton() { - return new Promise((resolve) => { - (0, lib_1.log)((0, lib_1.cy)("When contracts will be ready for verification step, press Enter to continue...")); - const rl = readline_1.default.createInterface({ input: process.stdin }); - rl.on("line", () => { - rl.close(); - resolve(); - }); - }); -} -function getLocatorAddressesToString(ACCOUNTING_ORACLE_PROXY, EL_REWARDS_VAULT, LEGACY_ORACLE, LIDO, POST_TOKEN_REBASE_RECEIVER, BURNER, STAKING_ROUTER, TREASURY_ADDRESS, VEBO, WQ, WITHDRAWAL_VAULT, ORACLE_DAEMON_CONFIG) { - return [ - `ACCOUNTING_ORACLE_PROXY: ${ACCOUNTING_ORACLE_PROXY}`, - `EL_REWARDS_VAULT: ${EL_REWARDS_VAULT}`, - `LEGACY_ORACLE: ${LEGACY_ORACLE}`, - `LIDO: ${LIDO}`, - `POST_TOKEN_REBASE_RECEIVER: ${POST_TOKEN_REBASE_RECEIVER}`, - `BURNER: ${BURNER}`, - `STAKING_ROUTER: ${STAKING_ROUTER}`, - `TREASURY_ADDRESS: ${TREASURY_ADDRESS}`, - `VEBO: ${VEBO}`, - `WQ: ${WQ}`, - `WITHDRAWAL_VAULT: ${WITHDRAWAL_VAULT}`, - `ORACLE_DAEMON_CONFIG: ${ORACLE_DAEMON_CONFIG}`, - ]; -} -main() - .then(() => process.exit(0)) - .catch((error) => { - lib_1.log.error(error); - process.exit(1); -}); From 1595e694a7065129f39f1835f885f0ed2e65dc4d Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 13:37:13 +0200 Subject: [PATCH 088/405] fix: remove unused network configurations from Hardhat config --- hardhat.config.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 73ec426013..a687b025f4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -30,20 +30,6 @@ const config: HardhatUserConfig = { enabled: !process.env.SKIP_GAS_REPORT, }, networks: { - "local": { - url: process.env.LOCAL_RPC_URL || RPC_URL, - }, - "local-devnet": { - url: process.env.LOCAL_RPC_URL || RPC_URL, - accounts: [process.env.LOCAL_DEVNET_PK || ZERO_PK], - }, - "holesky": { - url: process.env.LOCAL_RPC_URL || RPC_URL, - }, - "mainnet-fork": { - url: process.env.MAINNET_RPC_URL || RPC_URL, - timeout: 20 * 60 * 1000, // 20 minutes - }, "hardhat": { // setting base fee to 0 to avoid extra calculations doesn't work :( // minimal base fee is 1 for EIP-1559 From 110727cad6662300b9957214fa04044ec4315286 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 17 Apr 2025 17:01:50 +0400 Subject: [PATCH 089/405] fix: return sorting requirement for validators in request & formatting & remove contract version from exitEmitEvent method --- .../0.8.9/interfaces/IValidatorExitBus.sol | 2 +- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 78 ++++++++----------- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 26 ++++++- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 25 ++---- ...r-exit-bus-oracle.submitReportData.test.ts | 13 +++- 5 files changed, 73 insertions(+), 71 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol index 1c2c2a1863..f216746b42 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -22,7 +22,7 @@ interface IValidatorsExitBus { function submitReportHash(bytes32 exitReportHash) external; - function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external; + function emitExitEvents(ExitRequestData calldata request) external; function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable; diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index c2b3ffd560..47eed4c10d 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; -import { UnstructuredStorage } from "./UnstructuredStorage.sol"; +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; // MSB ------------------------------------------------------------------------------> LSB // 256______________160____________________________128_______________32____________________________ 0 @@ -12,13 +12,12 @@ import { UnstructuredStorage } from "./UnstructuredStorage.sol"; // struct ExitRequestLimitData { - uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests - uint96 prevExitRequestsLimit; // limit value (<= `maxExitRequestLimit`) obtained on the previous exit request - uint32 maxExitRequestsLimitGrowthBlocks; // limit regeneration speed expressed in blocks - uint96 maxExitRequestsLimit; // maximum limit value + uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests + uint96 prevExitRequestsLimit; // limit value (<= `maxExitRequestLimit`) obtained on the previous exit request + uint32 maxExitRequestsLimitGrowthBlocks; // limit regeneration speed expressed in blocks + uint96 maxExitRequestsLimit; // maximum limit value } - library ReportExitLimitUtilsStorage { using UnstructuredStorage for bytes32; @@ -27,11 +26,7 @@ library ReportExitLimitUtilsStorage { uint256 internal constant PREV_EXIT_REQUESTS_LIMIT_OFFSET = 32; uint256 internal constant PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET = 0; - function getStorageExitRequestLimit(bytes32 _position) - internal - view - returns (ExitRequestLimitData memory data) - { + function getStorageExitRequestLimit(bytes32 _position) internal view returns (ExitRequestLimitData memory data) { uint256 slotValue = _position.getStorageUint256(); data.prevExitRequestsBlockNumber = uint32(slotValue >> PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET); @@ -43,19 +38,19 @@ library ReportExitLimitUtilsStorage { function setStorageExitRequestLimit(bytes32 _position, ExitRequestLimitData memory _data) internal { _position.setStorageUint256( (uint256(_data.prevExitRequestsBlockNumber) << PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET) | - (uint256(_data.prevExitRequestsLimit) << PREV_EXIT_REQUESTS_LIMIT_OFFSET) | - (uint256(_data.maxExitRequestsLimitGrowthBlocks) << MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET) | - (uint256(_data.maxExitRequestsLimit) << MAX_EXIT_REQUESTS_LIMIT_OFFSET) + (uint256(_data.prevExitRequestsLimit) << PREV_EXIT_REQUESTS_LIMIT_OFFSET) | + (uint256(_data.maxExitRequestsLimitGrowthBlocks) << MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET) | + (uint256(_data.maxExitRequestsLimit) << MAX_EXIT_REQUESTS_LIMIT_OFFSET) ); } } library ReportExitLimitUtils { /** - * @notice Calculate exit requests limit - * @dev using `_constGasMin` to make gas consumption independent of the current block number - */ - function calculateCurrentExitRequestLimit(ExitRequestLimitData memory _data) internal view returns(uint256 limit) { + * @notice Calculate exit requests limit + * @dev using `_constGasMin` to make gas consumption independent of the current block number + */ + function calculateCurrentExitRequestLimit(ExitRequestLimitData memory _data) internal view returns (uint256 limit) { uint256 exitRequestLimitIncPerBlock; if (_data.maxExitRequestsLimitGrowthBlocks != 0) { exitRequestLimitIncPerBlock = _data.maxExitRequestsLimit / _data.maxExitRequestsLimitGrowthBlocks; @@ -64,21 +59,15 @@ library ReportExitLimitUtils { uint256 blocksPassed = block.number - _data.prevExitRequestsBlockNumber; uint256 projectedLimit = _data.prevExitRequestsLimit + blocksPassed * exitRequestLimitIncPerBlock; - limit = _constGasMin( - projectedLimit, - _data.maxExitRequestsLimit - ); - - + limit = _constGasMin(projectedLimit, _data.maxExitRequestsLimit); } - /** - * @notice update exit requests limit repr after exit request - * @dev input `_data` param is mutated and the func returns effectively the same pointer - * @param _data exit request limit struct - * @param _newPrevExitRequestsLimit new value for the `prevExitRequests` field - */ + * @notice update exit requests limit repr after exit request + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data exit request limit struct + * @param _newPrevExitRequestsLimit new value for the `prevExitRequests` field + */ function updatePrevExitRequestsLimit( ExitRequestLimitData memory _data, uint256 _newPrevExitRequestsLimit @@ -91,14 +80,13 @@ library ReportExitLimitUtils { return _data; } - /** - * @notice update exit request limit repr with the desired limits - * @dev input `_data` param is mutated and the func returns effectively the same pointer - * @param _data exit request limit struct - * @param _maxExitRequestsLimit exit request limit max value - * @param _exitRequestsLimitIncreasePerBlock exit request limit increase (restoration) per block - */ + * @notice update exit request limit repr with the desired limits + * @dev input `_data` param is mutated and the func returns effectively the same pointer + * @param _data exit request limit struct + * @param _maxExitRequestsLimit exit request limit max value + * @param _exitRequestsLimitIncreasePerBlock exit request limit increase (restoration) per block + */ function setExitReportLimit( ExitRequestLimitData memory _data, uint256 _maxExitRequestsLimit, @@ -108,8 +96,8 @@ library ReportExitLimitUtils { require(_maxExitRequestsLimit <= type(uint96).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); require(_maxExitRequestsLimit >= _exitRequestsLimitIncreasePerBlock, "TOO_LARGE_LIMIT_INCREASE"); require( - (_exitRequestsLimitIncreasePerBlock == 0) - || (_maxExitRequestsLimit / _exitRequestsLimitIncreasePerBlock <= type(uint32).max), + (_exitRequestsLimitIncreasePerBlock == 0) || + (_maxExitRequestsLimit / _exitRequestsLimitIncreasePerBlock <= type(uint32).max), "TOO_SMALL_LIMIT_INCREASE" ); @@ -120,8 +108,9 @@ library ReportExitLimitUtils { ) { _data.prevExitRequestsLimit = uint96(_maxExitRequestsLimit); } - _data.maxExitRequestsLimitGrowthBlocks= - _exitRequestsLimitIncreasePerBlock != 0 ? uint32(_maxExitRequestsLimit/ _exitRequestsLimitIncreasePerBlock) : 0; + _data.maxExitRequestsLimitGrowthBlocks = _exitRequestsLimitIncreasePerBlock != 0 + ? uint32(_maxExitRequestsLimit / _exitRequestsLimitIncreasePerBlock) + : 0; _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); @@ -134,11 +123,10 @@ library ReportExitLimitUtils { * TODO: discuss this part * @notice check if max exit request limit is set. Otherwise there are no limits on exits */ - function isExitReportLimitSet(ExitRequestLimitData memory _data) internal pure returns(bool){ - return _data.maxExitRequestsLimit != 0; + function isExitReportLimitSet(ExitRequestLimitData memory _data) internal pure returns (bool) { + return _data.maxExitRequestsLimit != 0; } - /** * @notice find a minimum of two numbers with a constant gas consumption * @dev doesn't use branching logic inside @@ -152,4 +140,4 @@ library ReportExitLimitUtils { } min = (_lhs * lhsIsLess) + (_rhs * (1 - lhsIsLess)); } -} \ No newline at end of file +} diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 656681e579..3e95d73630 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -34,6 +34,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error ExitRequestsLimit(); error InvalidPubkeysArray(); error NoExitRequestProvided(); + error InvalidRequestsDataSortOrder(); /// @dev Events event MadeRefund(address sender, uint256 refundValue); @@ -117,9 +118,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); } - function emitExitEvents(ExitRequestData calldata request, uint256 contractVersion) external whenResumed { + function emitExitEvents(ExitRequestData calldata request) external whenResumed { bytes calldata data = request.data; - _checkContractVersion(contractVersion); RequestStatus storage requestStatus = _storageExitRequestsHashes()[ keccak256(abi.encode(data, request.dataFormat)) @@ -137,6 +137,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InvalidRequestsDataLength(); } + _checkContractVersion(requestStatus.contractVersion); + // By default, totalItemsCount is set to type(uint256).max. // If an exit is emitted for the request for the first time, the default value is used for totalItemsCount. if (requestStatus.totalItemsCount == type(uint256).max) { @@ -188,6 +190,17 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa if (requestStatus.contractVersion == 0) { revert ExitHashWasNotSubmitted(); } + + if (request.dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(request.dataFormat); + } + + if (request.data.length % PACKED_REQUEST_LENGTH != 0) { + revert InvalidRequestsDataLength(); + } + + _checkContractVersion(requestStatus.contractVersion); + address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); @@ -378,6 +391,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { uint256 offset; uint256 offsetPastEnd; + uint256 lastDataWithoutPubkey = 0; + uint256 timestamp = _getTimestamp(); assembly { offset := add(data.offset, mul(startIndex, PACKED_REQUEST_LENGTH)) @@ -390,8 +405,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa pubkey.length := 48 } - uint256 timestamp = _getTimestamp(); - while (offset < offsetPastEnd) { uint256 dataWithoutPubkey; assembly { @@ -405,6 +418,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa // dataWithoutPubkey // MSB <---------------------------------------------------------------------- LSB // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + if (dataWithoutPubkey <= lastDataWithoutPubkey) { + revert InvalidRequestsDataSortOrder(); + } + uint64 valIndex = uint64(dataWithoutPubkey); uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); @@ -413,6 +430,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InvalidRequestsData(); } + lastDataWithoutPubkey = dataWithoutPubkey; emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); } } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index 92c6f7ea77..d1dd734bce 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -85,17 +85,11 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequest = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; - await expect(oracle.emitExitEvents(exitRequest, 2)) + await expect(oracle.emitExitEvents(exitRequest)) .to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted") .withArgs(); }); - it("Wrong contract version", async () => { - await expect(oracle.emitExitEvents(exitRequest, 1)) - .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") - .withArgs(2, 1); - }); - it("Should revert without SUBMIT_REPORT_HASH_ROLE role", async () => { const request = [exitRequest.data, exitRequest.dataFormat]; const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["(bytes, uint256)"], [request])); @@ -121,7 +115,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { }); it("Emit ValidatorExit event", async () => { - const emitTx = await oracle.emitExitEvents(exitRequest, 2); + const emitTx = await oracle.emitExitEvents(exitRequest); const timestamp = await oracle.getTime(); await expect(emitTx) @@ -171,7 +165,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const submitTx = await oracle.connect(authorizedEntity).submitReportHash(hash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(hash); exitRequest = { dataFormat: 2, data: encodeExitRequestsDataList(exitRequests) }; - await expect(oracle.emitExitEvents(exitRequest, 2)) + await expect(oracle.emitExitEvents(exitRequest)) .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") .withArgs(2); }); @@ -202,7 +196,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash"); - const emitTx = await oracle.emitExitEvents(exitRequest, 2); + const emitTx = await oracle.emitExitEvents(exitRequest); const receipt = await emitTx.wait(); expect(receipt?.logs.length).to.eq(2); @@ -233,7 +227,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { // expect(history1.length).to.eq(1); // expect(history1[0].lastDeliveredKeyIndex).to.eq(1); - const emitTx2 = await oracle.emitExitEvents(exitRequest, 2); + const emitTx2 = await oracle.emitExitEvents(exitRequest); const receipt2 = await emitTx2.wait(); expect(receipt2?.logs.length).to.eq(1); @@ -252,7 +246,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { // expect(history2.length).to.eq(2); // expect(history2[1].lastDeliveredKeyIndex).to.eq(2); - const emitTx3 = await oracle.emitExitEvents(exitRequest, 2); + const emitTx3 = await oracle.emitExitEvents(exitRequest); const receipt3 = await emitTx2.wait(); expect(receipt3?.logs.length).to.eq(1); @@ -271,7 +265,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { // expect(history3.length).to.eq(3); // expect(history3[2].lastDeliveredKeyIndex).to.eq(3); - const emitTx4 = await oracle.emitExitEvents(exitRequest, 2); + const emitTx4 = await oracle.emitExitEvents(exitRequest); const receipt4 = await emitTx2.wait(); expect(receipt4?.logs.length).to.eq(1); @@ -290,9 +284,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { // expect(history4.length).to.eq(4); // expect(history4[3].lastDeliveredKeyIndex).to.eq(4); - await expect(oracle.emitExitEvents(exitRequest, 2)).to.be.revertedWithCustomError( - oracle, - "RequestsAlreadyDelivered", - ); + await expect(oracle.emitExitEvents(exitRequest)).to.be.revertedWithCustomError(oracle, "RequestsAlreadyDelivered"); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 30984dffda..4d935b7d59 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -621,8 +621,8 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const requests = [ { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 2, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 2, nodeOpId: 2, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[3] }, ]; const { reportData } = await prepareReportAndSubmitHash(requests); const exitRequestHash = ethers.keccak256( @@ -640,17 +640,22 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { data: reportData.data, }; - const emitTx = await oracle.emitExitEvents(exitRequest, 2); + const emitTx = await oracle.emitExitEvents(exitRequest); const timestamp = await oracle.getTime(); await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs(requests[0].moduleId, requests[0].nodeOpId, requests[0].valIndex, requests[0].valPubkey, timestamp); - await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs(requests[1].moduleId, requests[1].nodeOpId, requests[1].valIndex, requests[1].valPubkey, timestamp); + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[2].moduleId, requests[2].nodeOpId, requests[2].valIndex, requests[2].valPubkey, timestamp); + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(requests[3].moduleId, requests[3].nodeOpId, requests[3].valIndex, requests[3].valPubkey, timestamp); }); it("emits ValidatorExitRequest events", async () => { From 5f880f100fd49cb4606bb897b6196772c65bd0ab Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 19:26:45 +0200 Subject: [PATCH 090/405] wip: nor tw prototype --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 158 +++++++++++++----- 1 file changed, 114 insertions(+), 44 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 52d676d34e..31035b1331 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -69,14 +69,14 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 eligibleToExitInSec, uint256 proofSlotTimestamp ); - event TriggerableExitFeeSet( + event ValidatorExitTriggered( uint256 indexed nodeOperatorId, bytes publicKey, uint256 withdrawalRequestPaidFee, uint256 exitType ); - event PenaltyApplied(uint256 indexed nodeOperatorId, bytes publicKey, uint256 penaltyAmount, string penaltyType); event ExitDeadlineThresholdChanged(uint256 threshold); + event PenaltyFramesCountChanged(uint256 penaltyFramesCount); // Enum to represent the state of the reward distribution process enum RewardDistributionState { @@ -174,12 +174,16 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // bytes32 internal constant TYPE_POSITION = keccak256("lido.NodeOperatorsRegistry.type"); bytes32 internal constant TYPE_POSITION = 0xbacf4236659a602d72c631ba0b0d67ec320aaf523f3ae3590d7faee4f42351d0; - // bytes32 internal constant STUCK_PENALTY_DELAY_POSITION = keccak256("lido.NodeOperatorsRegistry.stuckPenaltyDelay"); - bytes32 internal constant STUCK_PENALTY_DELAY_POSITION = 0x8e3a1f3826a82c1116044b334cae49f3c3d12c3866a1c4b18af461e12e58a18e; - // bytes32 internal constant REWARD_DISTRIBUTION_STATE = keccak256("lido.NodeOperatorsRegistry.rewardDistributionState"); bytes32 internal constant REWARD_DISTRIBUTION_STATE = 0x4ddbb0dcdc5f7692e494c15a7fca1f9eb65f31da0b5ce1c3381f6a1a1fd579b6; + // Number of penalty frames to apply for a delayed exit + bytes32 internal constant PENALTY_FRAMES_COUNT = keccak256("lido.NodeOperatorsRegistry.penaltyFramesCount"); + + // Threshold in seconds after which a delayed exit is penalized + bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = keccak256("lido.NodeOperatorsRegistry.exitDelayThresholdSeconds"); + + // // DATA TYPES // @@ -218,6 +222,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed summarySigningKeysStats; } + struct NodeOperatorExitDelayStats { + // Map public key hash to boolean indicating if it's been processed + mapping(bytes32 => bool) processedKeys; + // Total count of validators with exit delays for this operator + uint256 totalExitDelayPenaltyCount; + } + // // STORAGE VARIABLES // @@ -225,6 +236,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @dev Mapping of all node operators. Mapping is used to be able to extend the struct. mapping(uint256 => NodeOperator) internal _nodeOperators; NodeOperatorSummary internal _nodeOperatorSummary; + mapping(uint256 => NodeOperatorExitDelayStats) internal _nodeOperatorExitDelayStats; // // METHODS @@ -262,7 +274,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _initialize_v4() internal { _setContractVersion(4); - // TODO: after devnet-1 set correct logic + PENALTY_FRAMES_COUNT.setStorageUint256(3); + EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(86400); } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators @@ -736,10 +749,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // so optimistically, set the count of max validators equal to the vetted validators count. newMaxSigningKeysCount = signingKeysStats.get(TOTAL_VETTED_KEYS_COUNT_OFFSET); - if (!isOperatorPenaltyCleared(_nodeOperatorId)) { - // when the node operator is penalized zeroing its depositable validators count - newMaxSigningKeysCount = depositedSigningKeysCount; - } else if (operatorTargetStats.get(TARGET_LIMIT_MODE_OFFSET) != 0) { + if (operatorTargetStats.get(TARGET_LIMIT_MODE_OFFSET) != 0) { // apply target limit when it's active and the node operator is not penalized newMaxSigningKeysCount = Math256.max( // max validators count can't be less than the deposited validators count @@ -926,6 +936,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { shares[idx] = activeValidatorsCount; penalized[idx] = isOperatorPenalized(operatorId); + if (penalized[idx]) { + _decrementExitDelayPenalty(operatorId); + } + ++idx; } @@ -1025,22 +1039,26 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } - function _getExitDeadlineThreshold() public view returns (uint256) { - return 60 * 60 * 24 * 2; // 2 days + function getExitDeadlineThreshold() public view returns (uint256) { + return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); } - function reportValidatorExitDelay( - uint256 _nodeOperatorId, - uint256 _proofSlotTimestamp, - bytes _publicKey, - uint256 _eligibleToExitInSec - ) external { - _auth(STAKING_ROUTER_ROLE); - require(_eligibleToExitInSec >= 0, "INVALID_EXIT_TIME"); // placeholder check - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); + function setExitDeadlineThreshold(uint256 _threshold) external { + _auth(MANAGE_NODE_OPERATOR_ROLE); + require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); + EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_threshold); + emit ExitDeadlineThresholdChanged(_threshold); + } - emit PenaltyApplied(_nodeOperatorId, _publicKey, 1 ether, "EXCESS_EXIT_TIME"); - emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); + function getPenaltyFramesCount() public view returns (uint256) { + return PENALTY_FRAMES_COUNT.getStorageUint256(); + } + + function setPenaltyFramesCount(uint256 _framesCount) external { + _auth(MANAGE_NODE_OPERATOR_ROLE); + require(_framesCount > 0, "INVALID_PENALTY_FRAMES_COUNT"); + PENALTY_FRAMES_COUNT.setStorageUint256(_framesCount); + emit PenaltyFramesCountChanged(_framesCount); } function onValidatorExitTriggered( @@ -1050,22 +1068,78 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 _exitType ) external { _auth(STAKING_ROUTER_ROLE); - require(_publicKey.length > 0, "INVALID_PUBLIC_KEY"); - - emit TriggerableExitFeeSet(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); - } - function exitDeadlineThreshold(uint256 /* _nodeOperatorId */) external view returns (uint256) { - return _getExitDeadlineThreshold(); + emit ValidatorExitTriggered(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } function isValidatorExitDelayPenaltyApplicable( - uint256, // _nodeOperatorId + uint256 _nodeOperatorId, uint256, // _proofSlotTimestamp - bytes, // _publicKey + bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool) { - return _eligibleToExitInSec >= _getExitDeadlineThreshold(); + bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + bool isProcessed = _isKeyProcessed(_nodeOperatorId, processedKeyHash); + if (isProcessed) { + return false; + } + return _eligibleToExitInSec >= getExitDeadlineThreshold(); + } + + function reportValidatorExitDelay( + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes _publicKey, + uint256 _eligibleToExitInSec + ) external { + _auth(STAKING_ROUTER_ROLE); + require(_publicKey.length == 48, "INVALID_PUBLIC_KEY"); + + // Check if exit delay exceeds the threshold + require(_eligibleToExitInSec >= getExitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); + + // Check if the key has been processed already + bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + bool isProcessed = _isKeyProcessed(_nodeOperatorId, processedKeyHash); + require(!isProcessed, "KEY_ALREADY_PROCESSED"); + + // Mark the key as processed + _markKeyAsProcessed(_nodeOperatorId, processedKeyHash); + + // Increment penalty stats for the node operator + _increaseExitDelayPenaltyBy(_nodeOperatorId, getPenaltyFramesCount()); + + emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); + } + + + function _isKeyProcessed(uint256 _nodeOperatorId, bytes32 _keyHash) internal view returns (bool) { + return _nodeOperatorExitDelayStats[_nodeOperatorId].processedKeys[_keyHash]; + } + + function _markKeyAsProcessed(uint256 _nodeOperatorId, bytes32 _keyHash) internal { + _nodeOperatorExitDelayStats[_nodeOperatorId].processedKeys[_keyHash] = true; + } + + function _increaseExitDelayPenaltyBy(uint256 _nodeOperatorId, uint256 _penaltyCount) internal { + _nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount += _penaltyCount; + } + + function _decrementExitDelayPenalty(uint256 _nodeOperatorId) internal { + if (_nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount > 0) { + _nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount--; + } + } + + function getExitDelayPenaltyCount(uint256 _nodeOperatorId) external view returns (uint256) { + return _nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount; + } + + function isOperatorPenalized(uint256 _nodeOperatorId) public view returns (bool) { + if (_nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount > 0) { + return true; + } + return false; } function _removeUnusedSigningKeys(uint256 _nodeOperatorId, uint256 _fromIndex, uint256 _keysCount) internal { @@ -1213,16 +1287,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { depositableValidatorsCount = totalMaxValidators - totalDepositedValidators; } - function isOperatorPenalized(uint256 /* _nodeOperatorId */) public view returns (bool) { - // TODO: implement - return false; - } - - function isOperatorPenaltyCleared(uint256 /* _nodeOperatorId */) public view returns (bool) { - // TODO: implement - return true; - } - /// @notice Returns total number of node operators function getNodeOperatorsCount() public view returns (uint256) { return TOTAL_OPERATORS_COUNT_POSITION.getStorageUint256(); @@ -1284,14 +1348,20 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return; } - (address[] memory recipients, uint256[] memory shares,) = + (address[] memory recipients, uint256[] memory shares, bool[] memory penalized) = getRewardsDistribution(sharesToDistribute); 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; - // TODO: apply penalty to the operator + if (penalized[idx]) { + /// @dev half reward punishment + /// @dev ignore remainder since it accumulated on contract balance + shares[idx] >>= 1; + 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]); From 73934ae012dc3f437748f63498b5ba15183e6c2a Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 17 Apr 2025 19:34:49 +0200 Subject: [PATCH 091/405] fix: unit tests --- .../NodeOperatorsRegistry__Harness.sol | 4 --- test/0.4.24/nor/nor.exit.manager.test.ts | 25 +++++-------------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol b/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol index d7aa04f1c5..e0977e690f 100644 --- a/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol +++ b/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol @@ -140,10 +140,6 @@ contract NodeOperatorsRegistry__Harness is NodeOperatorsRegistry { LIDO_LOCATOR_POSITION.setStorageAddress(_mockedLocator); } - function harness__setStuckPenaltyDelay(uint256 _stuckPenaltyDelay) external { - STUCK_PENALTY_DELAY_POSITION.setStorageUint256(_stuckPenaltyDelay); - } - function harness__setNonce(uint256 _nonce) external { KEYS_OP_INDEX_POSITION.setStorageUint256(_nonce); } diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 99ee01bd63..170aec413b 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -58,8 +58,8 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const penaltyDelay = 86400n; - const testPublicKey = "0x123456"; - const eligibleToExitInSec = 172800n; // 2 days + const testPublicKey = "0x" + "0".repeat(48 * 2); + const eligibleToExitInSec = 86400n; // 2 days const proofSlotTimestamp = 1234567890n; const withdrawalRequestPaidFee = 100000n; const exitType = 1n; @@ -146,8 +146,6 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { eligibleToExitInSec ) ) - .to.emit(nor, "PenaltyApplied") - .withArgs(firstNodeOperatorId, testPublicKey, ethers.parseEther("1"), "EXCESS_EXIT_TIME") .and.to.emit(nor, "ValidatorExitStatusUpdated") .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); }); @@ -191,26 +189,15 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { exitType ) ) - .to.emit(nor, "TriggerableExitFeeSet") + .to.emit(nor, "ValidatorExitTriggered") .withArgs(firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, exitType); }); - - it("reverts when public key is empty", async () => { - await expect( - nor.connect(stakingRouter).onValidatorExitTriggered( - firstNodeOperatorId, - "0x", - withdrawalRequestPaidFee, - exitType - ) - ).to.be.revertedWith("INVALID_PUBLIC_KEY"); - }); }); context("exitDeadlineThreshold", () => { it("returns the expected value", async () => { - const threshold = await nor.exitDeadlineThreshold(firstNodeOperatorId); - expect(threshold).to.equal(172800n); // 2 days in seconds + const threshold = await nor.getExitDeadlineThreshold(); + expect(threshold).to.equal(86400n); }); }); @@ -238,7 +225,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { firstNodeOperatorId, proofSlotTimestamp, testPublicKey, - 172799n // Less than the threshold + 1n // Less than the threshold ); expect(shouldPenalize).to.be.false; }); From 7e11e90b0c60b546d373955ac46ad0db6f75558e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 18 Apr 2025 02:06:03 +0400 Subject: [PATCH 092/405] fix: added onValidatorExitTriggered hook in tw methods --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 89 +++++++++++++++---- .../contracts/StakingRouter_MockForVEB.sol | 27 ++++++ ...t-bus-oracle.triggerExitHashVerify.test.ts | 37 +++++++- ...it-bus-oracle.triggerExitsDirectly.test.ts | 31 ++++++- test/deploy/validatorExitBusOracle.ts | 8 ++ 5 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 test/0.8.9/contracts/StakingRouter_MockForVEB.sol diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 3e95d73630..658f5e6d43 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -15,6 +15,17 @@ interface IWithdrawalVault { function getWithdrawalRequestFee() external view returns (uint256); } + +interface IStakingRouter { + function onValidatorExitTriggered( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + bytes calldata _publicKey, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType + ) external; +} + contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ReportExitLimitUtilsStorage for bytes32; @@ -51,7 +62,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa event DirectExitRequest( uint256 indexed stakingModuleId, uint256 indexed nodeOperatorId, - bytes validatorsPubkeys, + bytes validatoPubkey, uint256 timestamp ); struct RequestStatus { @@ -203,6 +214,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); + address stakingRouterAddr = LOCATOR.stakingRouter(); if (msg.value < keyIndexes.length * withdrawalFee) { revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); @@ -211,6 +223,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); + bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); // TODO: create library for reading DATA for (uint256 i = 0; i < keyIndexes.length; i++) { @@ -222,17 +235,40 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyWasNotDelivered(keyIndexes[i], lastDeliveredKeyIndex); } - /// - /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | - /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | - /// 16 bytes - part without pubkey - uint256 requestPublicKeyOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16; - uint256 destOffset = i * PUBLIC_KEY_LENGTH; + uint256 itemOffset; + uint256 dataWithoutPubkey; + uint256 index = keyIndexes[i]; assembly { - let dest := add(pubkeys, add(32, destOffset)) - calldatacopy(dest, add(data.offset, requestPublicKeyOffset), PUBLIC_KEY_LENGTH) + // Compute the start of this packed request (item) + itemOffset := add(data.offset, mul(PACKED_REQUEST_LENGTH, index)) + + // Load the first 16 bytes which contain moduleId (24 bits), + // nodeOpId (40 bits), and valIndex (64 bits). + dataWithoutPubkey := shr(128, calldataload(itemOffset)) + } + + // dataWithoutPubkey format (128 bits total): + // MSB <-------------------- 128 bits --------------------> LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + + uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); + uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + if (moduleId == 0) { + revert InvalidRequestsData(); } + + assembly { + let pubkeyCalldataOffset := add(itemOffset, 16) + let pubkeyMemPtr := add(pubkey, 32) + let dest := add(pubkeys, add(32, mul(PUBLIC_KEY_LENGTH, i))) + + calldatacopy(dest, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + } + + IStakingRouter(stakingRouterAddr).onValidatorExitTriggered(moduleId, nodeOpId, pubkey, withdrawalFee, 0); } IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( @@ -247,6 +283,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ) external payable whenResumed onlyRole(DIRECT_EXIT_ROLE) preservesEthBalance returns (uint256) { address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); + address stakingRouterAddr = LOCATOR.stakingRouter(); if (exitData.validatorsPubkeys.length == 0) { revert NoExitRequestProvided(); @@ -256,25 +293,41 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InvalidPubkeysArray(); } - // TODO: maybe add requestCount in DirectExitData uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; if (msg.value < withdrawalFee * requestsCount) { revert InsufficientPayment(withdrawalFee, requestsCount, msg.value); } + uint256 timestamp = _getTimestamp(); + + bytes calldata data = exitData.validatorsPubkeys; + + for (uint256 i = 0; i < requestsCount; i++) { + bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); + + assembly { + let offset := add(data.offset, mul(i, PUBLIC_KEY_LENGTH)) + let dest := add(pubkey, 0x20) + calldatacopy(dest, offset, PUBLIC_KEY_LENGTH) + } + + IStakingRouter(stakingRouterAddr).onValidatorExitTriggered( + exitData.stakingModuleId, + exitData.nodeOperatorId, + pubkey, + withdrawalFee, + 0 + ); + + emit DirectExitRequest(exitData.stakingModuleId, exitData.nodeOperatorId, pubkey, timestamp); + } + IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( exitData.validatorsPubkeys ); - emit DirectExitRequest( - exitData.stakingModuleId, - exitData.nodeOperatorId, - exitData.validatorsPubkeys, - _getTimestamp() - ); - - return _refundFee(withdrawalFee * requestsCount); + return _refundFee(requestsCount * withdrawalFee); } function setExitReportLimit( diff --git a/test/0.8.9/contracts/StakingRouter_MockForVEB.sol b/test/0.8.9/contracts/StakingRouter_MockForVEB.sol new file mode 100644 index 0000000000..c009d4c7f4 --- /dev/null +++ b/test/0.8.9/contracts/StakingRouter_MockForVEB.sol @@ -0,0 +1,27 @@ +pragma solidity 0.8.9; + +contract StakingRouter__MockForVebo { + event Mock__onValidatorExitTriggered( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + bytes publicKey, + uint256 withdrawalRequestPaidFee, + uint256 exitType + ); + + function onValidatorExitTriggered( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + bytes calldata _publicKey, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType + ) external { + emit Mock__onValidatorExitTriggered( + _stakingModuleId, + _nodeOperatorId, + _publicKey, + _withdrawalRequestPaidFee, + _exitType + ); + } +} diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index f4af02a33c..f9a249c11a 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -5,7 +5,12 @@ import { ethers } from "hardhat"; import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; +import { + HashConsensus__Harness, + StakingRouter__MockForVebo, + ValidatorsExitBus__Harness, + WithdrawalVault__MockForVebo, +} from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -31,6 +36,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; let withdrawalVault: WithdrawalVault__MockForVebo; + let stakingRouter: StakingRouter__MockForVebo; let oracleVersion: bigint; let exitRequests: ExitRequest[]; @@ -81,6 +87,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { oracle = deployed.oracle; consensus = deployed.consensus; withdrawalVault = deployed.withdrawalVault; + stakingRouter = deployed.stakingRouter; await initVEBO({ admin: admin.address, @@ -222,6 +229,22 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await expect(tx) .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") .withArgs("0x" + concatenatedPubKeys); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[0].moduleId, exitRequests[0].nodeOpId, pubkeys[0], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[1].moduleId, exitRequests[1].nodeOpId, pubkeys[1], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[2].moduleId, exitRequests[2].nodeOpId, pubkeys[2], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[3], 1, 0); }); it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { @@ -235,6 +258,18 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") .withArgs("0x" + concatenatedPubKeys); + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[0].moduleId, exitRequests[0].nodeOpId, pubkeys[0], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[1].moduleId, exitRequests[1].nodeOpId, pubkeys[1], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[2], 1, 0); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 7); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index d99fd4f1e9..605809db01 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -4,7 +4,12 @@ import { ethers } from "hardhat"; import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; +import { + HashConsensus__Harness, + StakingRouter__MockForVebo, + ValidatorsExitBus__Harness, + WithdrawalVault__MockForVebo, +} from "typechain-types"; import { deployVEBO, initVEBO } from "test/deploy"; @@ -21,12 +26,14 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; let withdrawalVault: WithdrawalVault__MockForVebo; + let stakingRouter: StakingRouter__MockForVebo; let authorizedEntity: HardhatEthersSigner; let stranger: HardhatEthersSigner; let exitData: DirectExitData; const LAST_PROCESSING_REF_SLOT = 1; + const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; interface DirectExitData { stakingModuleId: number; @@ -39,6 +46,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { oracle = deployed.oracle; consensus = deployed.consensus; withdrawalVault = deployed.withdrawalVault; + stakingRouter = deployed.stakingRouter; await initVEBO({ admin: admin.address, @@ -57,7 +65,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { }); it("Should revert without DIRECT_EXIT_ROLE role", async () => { - const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); exitData = { @@ -97,6 +104,24 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, exitData.validatorsPubkeys, timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp); + await expect(tx) + .to.emit(oracle, "DirectExitRequest") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp); + await expect(tx) + .to.emit(oracle, "DirectExitRequest") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], 1, 0); }); }); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index fd95c7bd92..b6f61e7db2 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -4,6 +4,7 @@ import { ethers } from "hardhat"; import { HashConsensus__Harness, ReportProcessor__Mock, + StakingRouter__MockForVebo, ValidatorsExitBusOracle, WithdrawalVault__MockForVebo, } from "typechain-types"; @@ -43,6 +44,10 @@ async function deployWithdrawalVault() { return await ethers.deployContract("WithdrawalVault__MockForVebo"); } +async function deploySR() { + return await ethers.deployContract("StakingRouter__MockForVebo"); +} + export async function deployVEBO( admin: string, { @@ -68,11 +73,13 @@ export async function deployVEBO( const { ao, lido } = await deployMockAccountingOracle(secondsPerSlot, genesisTime); const withdrawalVault = await deployWithdrawalVault(); + const stakingRouter = await deploySR(); await updateLidoLocatorImplementation(locatorAddr, { lido: await lido.getAddress(), accountingOracle: await ao.getAddress(), withdrawalVault, + stakingRouter, }); const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locatorAddr, admin); @@ -90,6 +97,7 @@ export async function deployVEBO( consensus, oracleReportSanityChecker, withdrawalVault, + stakingRouter, }; } From cc94b147d15f42e511ad7695c50a07bfdfcf22e6 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 18 Apr 2025 13:01:02 +0400 Subject: [PATCH 093/405] fix: lint --- test/deploy/validatorExitBusOracle.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index b6f61e7db2..db1448d60e 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -4,7 +4,6 @@ import { ethers } from "hardhat"; import { HashConsensus__Harness, ReportProcessor__Mock, - StakingRouter__MockForVebo, ValidatorsExitBusOracle, WithdrawalVault__MockForVebo, } from "typechain-types"; From 3b7303cf2c81b845d82c8e71dd392dd3bbb4310c Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 21 Apr 2025 08:52:11 +0200 Subject: [PATCH 094/405] feat: add eip7685 support to withdrawal vault --- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 45 +++++++++++- .../common/lib/Eip7251MaxEffectiveBalance.sol | 68 +++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 contracts/common/lib/Eip7251MaxEffectiveBalance.sol diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index 6fde4be879..ac03617bae 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -7,7 +7,7 @@ pragma solidity 0.8.9; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; - +import {Eip7251MaxEffectiveBalance} from "../common/lib/Eip7251MaxEffectiveBalance.sol"; /** * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests * @dev This contract enables validators to submit EIP-7002 withdrawal requests @@ -16,6 +16,7 @@ import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); bytes32 public constant ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE"); + bytes32 public constant ADD_CONSOLIDATION_REQUEST_ROLE = keccak256("ADD_CONSOLIDATION_REQUEST_ROLE"); uint256 internal constant PUBLIC_KEY_LENGTH = 48; @@ -100,6 +101,48 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { return TriggerableWithdrawals.getWithdrawalRequestFee(); } + /** + * @dev Submits EIP-7251 consolidation requests for the specified public keys. + * Each request consolidate validators. + * Refunds any excess fee to the caller after deducting the total fees, + * which are calculated based on the number of requests and the current minimum fee per withdrawal request. + * + * @param sourcePubkeys A tightly packed array of 48-byte source public keys corresponding to validators requesting consolidation. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @param targetPubkeys A tightly packed array of 48-byte target public keys corresponding to validators requesting consolidation. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @notice Reverts if: + * - The caller does not have the `ADD_CONSOLIDATION_REQUEST_ROLE`. + * - Validation of any of the provided public keys fails. + * - The source and target public key arrays have different lengths. + * - The provided public key arrays is empty. + * - The provided total consolidation fee is insufficient to cover all requests. + * - Refund of the excess fee fails. + */ + function addConsolidationRequests( + bytes calldata sourcePubkeys, + bytes calldata targetPubkeys + ) external payable onlyRole(ADD_CONSOLIDATION_REQUEST_ROLE) preservesEthBalance { + uint256 feePerRequest = Eip7251MaxEffectiveBalance.getConsolidationRequestFee(); + uint256 totalFee = _countPubkeys(sourcePubkeys) * feePerRequest; + + _requireSufficientFee(totalFee); + + Eip7251MaxEffectiveBalance.addConsolidationRequests(sourcePubkeys, targetPubkeys, feePerRequest); + + _refundExcessFee(totalFee); + } + + /** + * @dev Retrieves the current EIP-7251 consolidation fee. + * @return The minimum fee required per consolidation request. + */ + function getConsolidationRequestFee() external view returns (uint256) { + return Eip7251MaxEffectiveBalance.getConsolidationRequestFee(); + } + function _countPubkeys(bytes calldata pubkeys) internal pure returns (uint256) { return (pubkeys.length / PUBLIC_KEY_LENGTH); } diff --git a/contracts/common/lib/Eip7251MaxEffectiveBalance.sol b/contracts/common/lib/Eip7251MaxEffectiveBalance.sol new file mode 100644 index 0000000000..9e2208680e --- /dev/null +++ b/contracts/common/lib/Eip7251MaxEffectiveBalance.sol @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.8.9 <0.9.0; + +/** + * @title A lib for EIP-7251: Increase the MAX_EFFECTIVE_BALANCE. + * Allow to send consolidation and compound requests for validators. + */ +library Eip7251MaxEffectiveBalance { + error NoConsolidationRequests(); + error MalformedPubkeysArray(); + error PubkeyArraysLengthMismatch(); + error ConsolidationFeeReadFailed(); + error ConsolidationFeeInvalidData(); + error ConsolidationRequestAdditionFailed(bytes callData); + + address constant CONSOLIDATION_REQUEST = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; + uint256 internal constant PUBLIC_KEY_LENGTH = 48; + + function getConsolidationRequestFee() internal view returns (uint256) { + (bool success, bytes memory feeData) = CONSOLIDATION_REQUEST.staticcall(""); + + if (!success) { + revert ConsolidationFeeReadFailed(); + } + + if (feeData.length != 32) { + revert ConsolidationFeeInvalidData(); + } + + return abi.decode(feeData, (uint256)); + } + + function addConsolidationRequests( + bytes calldata sourcePubkeys, + bytes calldata targetPubkeys, + uint256 feePerRequest + ) internal { + if (sourcePubkeys.length == 0) { + revert NoConsolidationRequests(); + } + if (sourcePubkeys.length != targetPubkeys.length) { + revert PubkeyArraysLengthMismatch(); + } + if (sourcePubkeys.length % PUBLIC_KEY_LENGTH != 0) { + revert MalformedPubkeysArray(); + } + + uint256 requestsCount = sourcePubkeys.length / PUBLIC_KEY_LENGTH; + bytes memory request = new bytes(96); + + for (uint256 i = 0; i < requestsCount; i++) { + assembly { + calldatacopy(add(request, 32), add(sourcePubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) + calldatacopy(add(request, 80), add(targetPubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) + } + + (bool success, ) = CONSOLIDATION_REQUEST.call{value: feePerRequest}(request); + + if (!success) { + revert ConsolidationRequestAdditionFailed(request); + } + } + } +} From b0079bb68d55b22b825d473bee9ca8c8c6022e06 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 21 Apr 2025 13:41:29 +0200 Subject: [PATCH 095/405] feat: make all execution layer requests pauseable in withdrawal vault --- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 48 +++++++++++++++++++--- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index ac03617bae..9a7ccd8b06 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -5,15 +5,19 @@ pragma solidity 0.8.9; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; +import {PausableUntil} from "./utils/PausableUntil.sol"; import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; import {Eip7251MaxEffectiveBalance} from "../common/lib/Eip7251MaxEffectiveBalance.sol"; + /** * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests * @dev This contract enables validators to submit EIP-7002 withdrawal requests * and manages the associated fees. */ -abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { +abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUntil { + bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); + bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); bytes32 public constant ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE"); bytes32 public constant ADD_CONSOLIDATION_REQUEST_ROLE = keccak256("ADD_CONSOLIDATION_REQUEST_ROLE"); @@ -30,6 +34,40 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { assert(address(this).balance == balanceBeforeCall); } + /** + * @dev Resumes the general purpose execution layer requests. + * @notice Reverts if: + * - The contract is not paused. + * - The sender does not have the `RESUME_ROLE`. + */ + function resume() external onlyRole(RESUME_ROLE) { + _resume(); + } + + /** + * @notice Pauses the general purpose execution layer requests placement for a specified duration. + * @param _duration The pause duration in seconds (use `PAUSE_INFINITELY` for unlimited). + * @dev Reverts if: + * - The contract is already paused. + * - The sender does not have the `PAUSE_ROLE`. + * - A zero duration is passed. + */ + function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { + _pauseFor(_duration); + } + + /** + * @notice Pauses the general purpose execution layer requests placement until a specified timestamp. + * @param _pauseUntilInclusive The last second to pause until (inclusive). + * @dev Reverts if: + * - The timestamp is in the past. + * - The sender does not have the `PAUSE_ROLE`. + * - The contract is already paused. + */ + function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { + _pauseUntil(_pauseUntilInclusive); + } + /** * @dev Submits EIP-7002 full withdrawal requests for the specified public keys. * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. @@ -48,7 +86,7 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { */ function addFullWithdrawalRequests( bytes calldata pubkeys - ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) whenResumed preservesEthBalance { uint256 feePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); uint256 totalFee = _countPubkeys(pubkeys) * feePerRequest; @@ -82,7 +120,7 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { function addPartialWithdrawalRequests( bytes calldata pubkeys, uint64[] calldata amounts - ) external payable onlyRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + ) external payable onlyRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE) whenResumed preservesEthBalance { uint256 feePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); uint256 totalFee = _countPubkeys(pubkeys) * feePerRequest; @@ -117,14 +155,14 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable { * - The caller does not have the `ADD_CONSOLIDATION_REQUEST_ROLE`. * - Validation of any of the provided public keys fails. * - The source and target public key arrays have different lengths. - * - The provided public key arrays is empty. + * - The provided public key arrays are empty. * - The provided total consolidation fee is insufficient to cover all requests. * - Refund of the excess fee fails. */ function addConsolidationRequests( bytes calldata sourcePubkeys, bytes calldata targetPubkeys - ) external payable onlyRole(ADD_CONSOLIDATION_REQUEST_ROLE) preservesEthBalance { + ) external payable onlyRole(ADD_CONSOLIDATION_REQUEST_ROLE) whenResumed preservesEthBalance { uint256 feePerRequest = Eip7251MaxEffectiveBalance.getConsolidationRequestFee(); uint256 totalFee = _countPubkeys(sourcePubkeys) * feePerRequest; From 802b1a0c6f06db20829ec197a621cc86a2911cb4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 21 Apr 2025 16:53:02 +0200 Subject: [PATCH 096/405] feat: update Hardhat config for hoodi network and update deployment script --- hardhat.config.ts | 7 ++++- scripts/deploy-tw.sh | 4 +-- scripts/triggerable-withdrawals/tw-deploy.ts | 27 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index a687b025f4..5b774e64e2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -59,7 +59,12 @@ const config: HardhatUserConfig = { "holesky": { url: process.env.HOLESKY_RPC_URL || RPC_URL, chainId: 17000, - accounts: loadAccounts("holesky"), + // accounts: loadAccounts("holesky"), + }, + "hoodi": { + url: process.env.HOLESKY_RPC_URL || RPC_URL, + chainId: 560048, + // accounts: loadAccounts("holesky"), }, "sepolia": { url: process.env.SEPOLIA_RPC_URL || RPC_URL, diff --git a/scripts/deploy-tw.sh b/scripts/deploy-tw.sh index 6887510bd7..eabc206625 100755 --- a/scripts/deploy-tw.sh +++ b/scripts/deploy-tw.sh @@ -2,7 +2,7 @@ set -e +u set -o pipefail -export NETWORK=holesky +export NETWORK=hoodi export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise export SLOTS_PER_EPOCH=32 export GENESIS_TIME=1639659600 # just some time @@ -13,7 +13,7 @@ export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of defau export GAS_PRIORITY_FEE=1 export GAS_MAX_FEE=100 -export NETWORK_STATE_FILE="deployed-holesky.json" +export NETWORK_STATE_FILE="deployed-hoodi.json" # export NETWORK_STATE_DEFAULTS_FILE="scripts/scratch/deployed-testnet-defaults.json" diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 36fd82db55..22329ff3da 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -65,6 +65,33 @@ async function main() { await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, withdrawalVaultArgs) ).address; log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); + + const minFirstAllocationStrategyAddress = state[Sk.minFirstAllocationStrategy].address; + const libraries = { + MinFirstAllocationStrategy: minFirstAllocationStrategyAddress, + }; + + const DEPOSIT_CONTRACT_ADDRESS = state[Sk.chainSpec].depositContract; + log(`Deposit contract address: ${DEPOSIT_CONTRACT_ADDRESS}`); + const stakingRouterAddress = ( + await deployImplementation(Sk.stakingRouter, "StakingRouter", deployer, [DEPOSIT_CONTRACT_ADDRESS], { libraries }) + ).address; + + log(`StakingRouter implementation address: ${stakingRouterAddress}`); + + const NOR = await deployImplementation(Sk.appNodeOperatorsRegistry, "NodeOperatorsRegistry", deployer, [], { + libraries, + }); + + log.success(`NOR implementation address: ${NOR.address}`); + + log.emptyLine(); + + log(`Configuration for voting script:`); + log(`VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle}" +WITHDRAWAL_VAULT_IMPL = "${withdrawalVault}" +STAKING_ROUTER_IMPL = "${stakingRouterAddress}" +NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}"`); } main() From d1f78b6764de4ecd37bd60f94703de46316f8b79 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 22 Apr 2025 01:05:43 +0400 Subject: [PATCH 097/405] fix: limit for tw requests --- .../0.8.9/interfaces/IValidatorExitBus.sol | 16 ++- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 16 ++- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 100 ++++++++++++++---- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 16 ++- ...r-exit-bus-oracle.submitReportData.test.ts | 16 ++- 6 files changed, 136 insertions(+), 30 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol index f216746b42..fd55cd012f 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -20,6 +20,20 @@ interface IValidatorsExitBus { uint256 timestamp; } + struct ExitLimits { + /// @notice Maximum limit value for exits that will be processed through the CL + /// TODO: @dev Must fit into uint16 (<= 65_535) ? Is this value the same as exitedValidatorsPerDayLimit; in OracleReportSanityChecker + uint256 maxExitRequestsLimit; + /// @notice Exit limit increase per block for exits that will be processed through the CL + /// @dev This value will be used for limit replenishment + uint256 exitRequestsLimitIncreasePerBlock; + /// @notice Maximum limit value for exits that will be processed via TW (eip-7002) + /// TODO: @dev Must fit into uint16 (<= 65_535) ? Is this value the same as exitedValidatorsPerDayLimit; in OracleReportSanityChecker + uint256 maxTWExitRequestsLimit; + /// @notice Exit limit increase per block for exits that will be processed via TW (eip-7002) + uint256 twExitRequestsLimitIncreasePerBlock; + } + function submitReportHash(bytes32 exitReportHash) external; function emitExitEvents(ExitRequestData calldata request) external; @@ -28,7 +42,7 @@ interface IValidatorsExitBus { function triggerExitsDirectly(DirectExitData calldata exitData) external payable returns (uint256); - function setExitReportLimit(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock) external; + function setExitRequestLimit(ExitLimits calldata limits) external; function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index 47eed4c10d..0f5be31b78 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -11,11 +11,16 @@ import {UnstructuredStorage} from "./UnstructuredStorage.sol"; // |<--- 96 bits --->|<---------- 32 bits -------->|<--- 96 bits ---->|<----- 32 bits ------------->| // +// TODO: maybe we need smaller type for maxExitRequestsLimit struct ExitRequestLimitData { uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests - uint96 prevExitRequestsLimit; // limit value (<= `maxExitRequestLimit`) obtained on the previous exit request - uint32 maxExitRequestsLimitGrowthBlocks; // limit regeneration speed expressed in blocks - uint96 maxExitRequestsLimit; // maximum limit value + // Remaining portion of the limit available after the previous request. + // Always less than or equal to `maxExitRequestsLimit`. + uint96 prevExitRequestsLimit; + // Number of block to regenerate limit from 0 to maxExitRequestsLimit + uint32 maxExitRequestsLimitGrowthBlocks; + // TODO: mabe use uint16 type + uint96 maxExitRequestsLimit; // maximum exit requests limit value } library ReportExitLimitUtilsStorage { @@ -87,7 +92,7 @@ library ReportExitLimitUtils { * @param _maxExitRequestsLimit exit request limit max value * @param _exitRequestsLimitIncreasePerBlock exit request limit increase (restoration) per block */ - function setExitReportLimit( + function setExitRequestLimit( ExitRequestLimitData memory _data, uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock @@ -114,6 +119,7 @@ library ReportExitLimitUtils { _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); + // do we need to set block here, or for some edge case we need to have 0 here _data.prevExitRequestsBlockNumber = uint32(block.number); return _data; @@ -123,7 +129,7 @@ library ReportExitLimitUtils { * TODO: discuss this part * @notice check if max exit request limit is set. Otherwise there are no limits on exits */ - function isExitReportLimitSet(ExitRequestLimitData memory _data) internal pure returns (bool) { + function isExitRequestLimitSet(ExitRequestLimitData memory _data) internal pure returns (bool) { return _data.maxExitRequestsLimit != 0; } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 658f5e6d43..dbea1c3384 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -57,7 +57,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorPubkey, uint256 timestamp ); - event ExitRequestsLimitSet(uint256 _maxExitRequestsLimit, uint256 _exitRequestsLimitIncreasePerBlock); + event ExitRequestsLimitSet( + uint256 maxExitRequestsLimit, + uint256 exitRequestsLimitIncreasePerBlock, + uint256 maxTWExitRequestsLimit, + uint256 twExitRequestsLimitIncreasePerBlock + ); event DirectExitRequest( uint256 indexed stakingModuleId, @@ -83,8 +88,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// @notice An ACL role granting the permission to resume accepting validator exit requests bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); - /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; @@ -112,6 +115,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); + bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = + keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); + bytes32 public constant TW_EXIT_REQUEST_LIMIT_POSITION = + keccak256("lido.ValidatorsExitBus.maxTWExitRequestsLimit"); /// @dev Ensures the contract’s ETH balance is unchanged. modifier preservesEthBalance() { @@ -166,7 +173,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); uint256 toDeliver; - if (exitRequestLimitData.isExitReportLimitSet()) { + if (exitRequestLimitData.isExitRequestLimitSet()) { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (limit == 0) { revert ExitRequestsLimit(); @@ -212,9 +219,23 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkContractVersion(requestStatus.contractVersion); + // limit check + ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + + if (exitRequestLimitData.isExitRequestLimitSet()) { + uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + + if (keyIndexes.length > limit) { + revert ExitRequestsLimit(); + } + + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitRequestsLimit(limit - keyIndexes.length) + ); + } + address withdrawalVaultAddr = LOCATOR.withdrawalVault(); uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - address stakingRouterAddr = LOCATOR.stakingRouter(); if (msg.value < keyIndexes.length * withdrawalFee) { revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); @@ -268,7 +289,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) } - IStakingRouter(stakingRouterAddr).onValidatorExitTriggered(moduleId, nodeOpId, pubkey, withdrawalFee, 0); + IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( + moduleId, + nodeOpId, + pubkey, + withdrawalFee, + 0 + ); } IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( @@ -281,9 +308,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa function triggerExitsDirectly( DirectExitData calldata exitData ) external payable whenResumed onlyRole(DIRECT_EXIT_ROLE) preservesEthBalance returns (uint256) { - address withdrawalVaultAddr = LOCATOR.withdrawalVault(); - uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - address stakingRouterAddr = LOCATOR.stakingRouter(); + address withdrawalVault = LOCATOR.withdrawalVault(); + uint256 withdrawalFee = IWithdrawalVault(withdrawalVault).getWithdrawalRequestFee(); if (exitData.validatorsPubkeys.length == 0) { revert NoExitRequestProvided(); @@ -295,6 +321,21 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; + ExitRequestLimitData memory twExitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION + .getStorageExitRequestLimit(); + + if (twExitRequestLimitData.isExitRequestLimitSet()) { + uint256 limit = twExitRequestLimitData.calculateCurrentExitRequestLimit(); + + if (requestsCount > limit) { + revert ExitRequestsLimit(); + } + + TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + twExitRequestLimitData.updatePrevExitRequestsLimit(limit - requestsCount) + ); + } + if (msg.value < withdrawalFee * requestsCount) { revert InsufficientPayment(withdrawalFee, requestsCount, msg.value); } @@ -312,7 +353,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa calldatacopy(dest, offset, PUBLIC_KEY_LENGTH) } - IStakingRouter(stakingRouterAddr).onValidatorExitTriggered( + IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( exitData.stakingModuleId, exitData.nodeOperatorId, pubkey, @@ -323,25 +364,46 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa emit DirectExitRequest(exitData.stakingModuleId, exitData.nodeOperatorId, pubkey, timestamp); } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( + IWithdrawalVault(withdrawalVault).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( exitData.validatorsPubkeys ); return _refundFee(requestsCount * withdrawalFee); } - function setExitReportLimit( - uint256 _maxExitRequestsLimit, - uint256 _exitRequestsLimitIncreasePerBlock - ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { + function setExitRequestLimit(ExitLimits calldata limits) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { + require(limits.maxExitRequestsLimit != 0, "ZERO_MAX_EXIT_REQUEST_LIMIT"); + require(limits.maxTWExitRequestsLimit != 0, "ZERO_MAX_TW_EXIT_REQUEST_LIMIT"); + require( + limits.maxExitRequestsLimit >= limits.exitRequestsLimitIncreasePerBlock, + "TOO_LARGE_EXIT_LIMIT_INCREASE" + ); + require( + limits.maxTWExitRequestsLimit >= limits.twExitRequestsLimitIncreasePerBlock, + "TOO_LARGE_TW_EXIT_LIMIT_INCREASE" + ); + // TODO: what maximum value for block distance to replenish limits we can set here? limits.maxExitRequestsLimit / limits.exitRequestsLimitIncreasePerBlock + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitReportLimit( - _maxExitRequestsLimit, - _exitRequestsLimitIncreasePerBlock + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitRequestLimit( + limits.maxExitRequestsLimit, + limits.exitRequestsLimitIncreasePerBlock ) ); - emit ExitRequestsLimitSet(_maxExitRequestsLimit, _exitRequestsLimitIncreasePerBlock); + TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitRequestLimit( + limits.maxTWExitRequestsLimit, + limits.twExitRequestsLimitIncreasePerBlock + ) + ); + + emit ExitRequestsLimitSet( + limits.maxExitRequestsLimit, + limits.exitRequestsLimitIncreasePerBlock, + limits.maxTWExitRequestsLimit, + limits.twExitRequestsLimitIncreasePerBlock + ); } function getExitRequestsDeliveryHistory( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 2888fc0b2d..b2e361e0d7 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -244,7 +244,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { // Check VEB common limit ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - if (exitRequestLimitData.isExitReportLimitSet()) { + if (exitRequestLimitData.isExitRequestLimitSet()) { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (data.requestsCount > limit) { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index d1dd734bce..2f470f9e23 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -43,6 +43,13 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { data: string; } + interface ExitRequestLimitData { + prevExitRequestsBlockNumber: number; + prevExitRequestsLimit: number; + maxExitRequestsLimitGrowthBlocks: number; + maxExitRequestsLimit: number; + } + const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { const pubkeyHex = de0x(valPubkey); expect(pubkeyHex.length).to.equal(48 * 2); @@ -173,8 +180,13 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { it("Should deliver part of request if limit is smaller than number of requests", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, authorizedEntity); - const exitLimitTx = await oracle.connect(authorizedEntity).setExitReportLimit(2, 1); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1); + const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit({ + maxExitRequestsLimit: 2, + exitRequestsLimitIncreasePerBlock: 1, + twExitRequestsLimitIncreasePerBlock: 1, + maxTWExitRequestsLimit: 2, + }); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1, 2, 1); exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 4d935b7d59..6a25af5d22 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -57,6 +57,13 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { data: string; } + interface ExitRequestLimitData { + prevExitRequestsBlockNumber: number; + prevExitRequestsLimit: number; + maxExitRequestsLimitGrowthBlocks: number; + maxExitRequestsLimit: number; + } + const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( @@ -613,8 +620,13 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { it("Set exit limit", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, admin); - const exitLimitTx = await oracle.connect(admin).setExitReportLimit(4, 1); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1); + const exitLimitTx = await oracle.connect(admin).setExitRequestLimit({ + maxExitRequestsLimit: 4, + exitRequestsLimitIncreasePerBlock: 1, + maxTWExitRequestsLimit: 4, + twExitRequestsLimitIncreasePerBlock: 1, + }); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1, 4, 1); }); it("deliver report by actor different from oracle", async () => { From c352a90f0d80962930a3c99ef6da763fd3b361f0 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 22 Apr 2025 17:18:10 +0400 Subject: [PATCH 098/405] fix: tw limits --- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 10 ++--- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 18 ++++----- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- ...r-exit-bus-oracle.submitReportData.test.ts | 7 ---- ...t-bus-oracle.triggerExitHashVerify.test.ts | 34 +++++++++++++++++ ...it-bus-oracle.triggerExitsDirectly.test.ts | 38 ++++++++++++++++++- 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol index 0f5be31b78..7a273b84e9 100644 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ReportExitLimitUtils.sol @@ -19,7 +19,7 @@ struct ExitRequestLimitData { uint96 prevExitRequestsLimit; // Number of block to regenerate limit from 0 to maxExitRequestsLimit uint32 maxExitRequestsLimitGrowthBlocks; - // TODO: mabe use uint16 type + // TODO: maybe use uint16 type uint96 maxExitRequestsLimit; // maximum exit requests limit value } @@ -77,8 +77,6 @@ library ReportExitLimitUtils { ExitRequestLimitData memory _data, uint256 _newPrevExitRequestsLimit ) internal view returns (ExitRequestLimitData memory) { - // assert(_data.prevExitRequestsBlockNumber != 0); - _data.prevExitRequestsLimit = uint96(_newPrevExitRequestsLimit); _data.prevExitRequestsBlockNumber = uint32(block.number); @@ -119,14 +117,14 @@ library ReportExitLimitUtils { _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); - // do we need to set block here, or for some edge case we need to have 0 here - _data.prevExitRequestsBlockNumber = uint32(block.number); + if (_data.prevExitRequestsBlockNumber != 0) { + _data.prevExitRequestsBlockNumber = uint32(block.number); + } return _data; } /** - * TODO: discuss this part * @notice check if max exit request limit is set. Otherwise there are no limits on exits */ function isExitRequestLimitSet(ExitRequestLimitData memory _data) internal pure returns (bool) { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index dbea1c3384..e3964f03c4 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -42,7 +42,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error InvalidRequestsDataLength(); error InvalidRequestsData(); error RequestsAlreadyDelivered(); - error ExitRequestsLimit(); + // TODO: create better name than prevLimit + error ExitRequestsLimit(uint256 requestsCount, uint256 prevLimit); + error TWExitRequestsLimit(uint256 requestsCount, uint256 prevLimit); error InvalidPubkeysArray(); error NoExitRequestProvided(); error InvalidRequestsDataSortOrder(); @@ -115,10 +117,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); - bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = - keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); - bytes32 public constant TW_EXIT_REQUEST_LIMIT_POSITION = - keccak256("lido.ValidatorsExitBus.maxTWExitRequestsLimit"); + bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); + bytes32 public constant TW_EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxTWExitRequestsLimit"); /// @dev Ensures the contract’s ETH balance is unchanged. modifier preservesEthBalance() { @@ -176,7 +176,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa if (exitRequestLimitData.isExitRequestLimitSet()) { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (limit == 0) { - revert ExitRequestsLimit(); + revert ExitRequestsLimit(undeliveredItemsCount, limit); } toDeliver = undeliveredItemsCount > limit ? limit : undeliveredItemsCount; @@ -226,10 +226,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (keyIndexes.length > limit) { - revert ExitRequestsLimit(); + revert TWExitRequestsLimit(keyIndexes.length, limit); } - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( exitRequestLimitData.updatePrevExitRequestsLimit(limit - keyIndexes.length) ); } @@ -328,7 +328,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 limit = twExitRequestLimitData.calculateCurrentExitRequestLimit(); if (requestsCount > limit) { - revert ExitRequestsLimit(); + revert TWExitRequestsLimit(requestsCount, limit); } TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index b2e361e0d7..cb51dabea8 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -248,7 +248,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (data.requestsCount > limit) { - revert ExitRequestsLimit(); + revert ExitRequestsLimit(data.requestsCount, limit); } EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 6a25af5d22..7d1682f134 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -57,13 +57,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { data: string; } - interface ExitRequestLimitData { - prevExitRequestsBlockNumber: number; - prevExitRequestsLimit: number; - maxExitRequestsLimitGrowthBlocks: number; - maxExitRequestsLimit: number; - } - const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; const reportDataHash = ethers.keccak256( diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index f9a249c11a..a97943755a 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -118,6 +118,19 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); }; + it("Should set limit for tw", async () => { + const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(role, admin); + const exitLimitTx = await oracle.connect(admin).setExitRequestLimit({ + maxExitRequestsLimit: 4, + exitRequestsLimitIncreasePerBlock: 1, + twExitRequestsLimitIncreasePerBlock: 1, + maxTWExitRequestsLimit: 2, + }); + + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1, 2, 1); + }); + it("initially, consensus report is empty and is not being processed", async () => { const report = await oracle.getConsensusReport(); expect(report.hash).to.equal(ZeroHash); @@ -217,6 +230,27 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect(procState.requestsSubmitted).to.equal(exitRequests.length); }); + it("Out of tw exit request limit", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { + value: 10, + }), + ) + .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") + .withArgs(3, 2); + }); + + it("Increase limit", async () => { + const exitLimitTx = await oracle.connect(admin).setExitRequestLimit({ + maxExitRequestsLimit: 4, + exitRequestsLimitIncreasePerBlock: 1, + twExitRequestsLimitIncreasePerBlock: 1, + maxTWExitRequestsLimit: 10, + }); + + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1, 10, 1); + }); + it("someone submitted exit report data and triggered exit", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index 605809db01..ed75bfec87 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -64,6 +64,19 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { await deploy(); }); + it("Should set limit for tw", async () => { + const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(role, authorizedEntity); + const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit({ + maxExitRequestsLimit: 2, + exitRequestsLimitIncreasePerBlock: 1, + twExitRequestsLimitIncreasePerBlock: 1, + maxTWExitRequestsLimit: 2, + }); + + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1, 2, 1); + }); + it("Should revert without DIRECT_EXIT_ROLE role", async () => { const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); @@ -80,11 +93,34 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_ROLE()); }); - it("Not enough fee", async () => { + it("Grant DIRECT_EXIT_ROLE role", async () => { const role = await oracle.DIRECT_EXIT_ROLE(); await oracle.grantRole(role, authorizedEntity); + }); + it("Out of tw exit request limit", async () => { + await expect( + oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + value: 4, + }), + ) + .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") + .withArgs(3, 2); + }); + + it("Increase limit", async () => { + const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit({ + maxExitRequestsLimit: 2, + exitRequestsLimitIncreasePerBlock: 1, + twExitRequestsLimitIncreasePerBlock: 1, + maxTWExitRequestsLimit: 4, + }); + + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1, 4, 1); + }); + + it("Not enough fee", async () => { await expect( oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { value: 2, From 450b940351a50730117a6ae0a040913507900fa5 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 22 Apr 2025 21:08:56 +0200 Subject: [PATCH 099/405] feat: validator exit verifier use new staking router interface --- contracts/0.8.25/ValidatorExitVerifier.sol | 4 ++-- contracts/0.8.25/interfaces/IStakingRouter.sol | 2 +- test/0.8.25/contracts/StakingRouter_Mock.sol | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitVerifier.sol b/contracts/0.8.25/ValidatorExitVerifier.sol index 3d356a72fb..acffb68afb 100644 --- a/contracts/0.8.25/ValidatorExitVerifier.sol +++ b/contracts/0.8.25/ValidatorExitVerifier.sol @@ -197,7 +197,7 @@ contract ValidatorExitVerifier { _verifyValidatorIsNotExited(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); - stakingRouter.shouldValidatorBePenalized( + stakingRouter.reportValidatorExitDelay( moduleId, nodeOpId, proofSlotTimestamp, @@ -248,7 +248,7 @@ contract ValidatorExitVerifier { _verifyValidatorIsNotExited(oldBlock.header, witness, pubkey, valIndex); - stakingRouter.shouldValidatorBePenalized( + stakingRouter.reportValidatorExitDelay( moduleId, nodeOpId, proofSlotTimestamp, diff --git a/contracts/0.8.25/interfaces/IStakingRouter.sol b/contracts/0.8.25/interfaces/IStakingRouter.sol index 7cf603803a..db9c92badd 100644 --- a/contracts/0.8.25/interfaces/IStakingRouter.sol +++ b/contracts/0.8.25/interfaces/IStakingRouter.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.25; interface IStakingRouter { - function shouldValidatorBePenalized( + function reportValidatorExitDelay( uint256 _moduleId, uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol index 848e8f1cf8..c9c610d073 100644 --- a/test/0.8.25/contracts/StakingRouter_Mock.sol +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.25; import {IStakingRouter} from "contracts/0.8.25/interfaces/IStakingRouter.sol"; contract StakingRouter_Mock is IStakingRouter { - // An event to track when shouldValidatorBePenalized is called + // An event to track when reportValidatorExitDelay is called event UnexitedValidatorReported( uint256 moduleId, uint256 nodeOperatorId, @@ -13,7 +13,7 @@ contract StakingRouter_Mock is IStakingRouter { uint256 secondsSinceEligibleExitRequest ); - function shouldValidatorBePenalized( + function reportValidatorExitDelay( uint256 moduleId, uint256 nodeOperatorId, uint256 _proofSlotTimestamp, From 51b82524ad329a4d31271aa58f983c30eda81929 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 23 Apr 2025 02:02:54 +0400 Subject: [PATCH 100/405] fix: library for daily limit exits --- .../0.8.9/interfaces/IValidatorExitBus.sol | 16 +- contracts/0.8.9/lib/ExitLimitUtils.sol | 117 ++++++++++ contracts/0.8.9/oracle/ValidatorsExitBus.sol | 125 +++++----- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 13 +- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 220 +++++++++--------- ...r-exit-bus-oracle.submitReportData.test.ts | 34 ++- ...t-bus-oracle.triggerExitHashVerify.test.ts | 74 +++--- ...it-bus-oracle.triggerExitsDirectly.test.ts | 72 ++++-- 8 files changed, 408 insertions(+), 263 deletions(-) create mode 100644 contracts/0.8.9/lib/ExitLimitUtils.sol diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol index fd55cd012f..385da1b3c8 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -20,20 +20,6 @@ interface IValidatorsExitBus { uint256 timestamp; } - struct ExitLimits { - /// @notice Maximum limit value for exits that will be processed through the CL - /// TODO: @dev Must fit into uint16 (<= 65_535) ? Is this value the same as exitedValidatorsPerDayLimit; in OracleReportSanityChecker - uint256 maxExitRequestsLimit; - /// @notice Exit limit increase per block for exits that will be processed through the CL - /// @dev This value will be used for limit replenishment - uint256 exitRequestsLimitIncreasePerBlock; - /// @notice Maximum limit value for exits that will be processed via TW (eip-7002) - /// TODO: @dev Must fit into uint16 (<= 65_535) ? Is this value the same as exitedValidatorsPerDayLimit; in OracleReportSanityChecker - uint256 maxTWExitRequestsLimit; - /// @notice Exit limit increase per block for exits that will be processed via TW (eip-7002) - uint256 twExitRequestsLimitIncreasePerBlock; - } - function submitReportHash(bytes32 exitReportHash) external; function emitExitEvents(ExitRequestData calldata request) external; @@ -42,7 +28,7 @@ interface IValidatorsExitBus { function triggerExitsDirectly(DirectExitData calldata exitData) external payable returns (uint256); - function setExitRequestLimit(ExitLimits calldata limits) external; + function setExitRequestLimit(uint256 exitsDailyLimit, uint256 twExitsDailyLimit) external; function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol new file mode 100644 index 0000000000..8d9311568a --- /dev/null +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import {UnstructuredStorage} from "./UnstructuredStorage.sol"; + +// MSB -----------------------------------> LSB +// 256___________160_______________ 64______________ 0 +// |_______________|________________|_______________| +// | dailyLimit | dailyExitCount | currentDay | +// |<-- 96 bits -->| <-- 96 bits -->|<-- 64 bits -->| +// + +struct ExitRequestLimitData { + uint96 dailyLimit; + uint96 dailyExitCount; + uint64 currentDay; +} + +library ExitLimitUtilsStorage { + using UnstructuredStorage for bytes32; + + uint256 internal constant DAILY_LIMIT_OFFSET = 160; + uint256 internal constant DAILY_EXIT_COUNT_OFFSET = 64; + uint256 internal constant CURRENT_DAY_OFFSET = 0; + + function getStorageExitRequestLimit(bytes32 _position) internal view returns (ExitRequestLimitData memory data) { + uint256 slotValue = _position.getStorageUint256(); + + data.currentDay = uint64(slotValue >> CURRENT_DAY_OFFSET); + data.dailyExitCount = uint96(slotValue >> DAILY_EXIT_COUNT_OFFSET); + data.dailyLimit = uint96(slotValue >> DAILY_LIMIT_OFFSET); + } + + function setStorageExitRequestLimit(bytes32 _position, ExitRequestLimitData memory _data) internal { + _position.setStorageUint256( + (uint256(_data.currentDay) << CURRENT_DAY_OFFSET) | + (uint256(_data.dailyExitCount) << DAILY_EXIT_COUNT_OFFSET) | + (uint256(_data.dailyLimit) << DAILY_LIMIT_OFFSET) + ); + } +} + +library ExitLimitUtils { + /** + * @notice Returns the current limit for the current day + * @param data Exit request limit struct + * @param day Full days since the Unix epoch (block.timestamp / 1 days) + */ + function remainingLimit(ExitRequestLimitData memory data, uint256 day) internal pure returns (uint256) { + // TODO: uint64? + if (data.currentDay != day) { + return data.dailyLimit; + } + + return data.dailyExitCount >= data.dailyLimit ? 0 : data.dailyLimit - data.dailyExitCount; + } + + /** + * @notice Updates the current request counter and day in the exit limit data + * @param data Exit request limit struct + * @param currentDay Full days since the Unix epoch (block.timestamp / 1 days) + * @param newCount New requests amount spent during the day + */ + function updateRequestsCounter( + ExitRequestLimitData memory data, + uint256 currentDay, + uint256 newCount + ) internal pure returns (ExitRequestLimitData memory) { + if (data.currentDay != currentDay) { + data.currentDay = uint64(currentDay); + data.dailyExitCount = 0; + } + + uint256 updatedCount = uint256(data.dailyExitCount) + newCount; + require(updatedCount <= type(uint96).max, "DAILY_EXIT_COUNT_OVERFLOW"); + + data.dailyExitCount = uint96(updatedCount); + + return data; + } + + /** + * @notice check if max daily exit request limit is set. Otherwise there are no limits on exits + */ + function isExitDailyLimitSet(ExitRequestLimitData memory data) internal pure returns (bool) { + return data.dailyLimit != 0; + } + + /** + * @notice Update daily limit + * @param data Exit request limit struct + * @param limit Exit request limit per day + * @dev TODO: maybe we need use here uin96 + * what will happen if method got argument with bigger value than uint96? + */ + function setExitDailyLimit( + ExitRequestLimitData memory data, + uint256 limit + ) internal view returns (ExitRequestLimitData memory) { + require(limit != 0, "ZERO_EXIT_REQUESTS_LIMIT"); + require(limit <= type(uint96).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); + + uint64 day = uint64(block.timestamp / 1 days); + require(data.currentDay <= day, "INVALID_TIMESTAMP_BACKWARD"); + + data.dailyLimit = uint96(limit); + + if (day == data.currentDay && data.dailyExitCount > data.dailyLimit) { + data.dailyExitCount = data.dailyLimit; + } + + // other values doesnt look like we need to set here in other cases + + return data; + } +} diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index e3964f03c4..102aa71bd2 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -6,7 +6,7 @@ import {AccessControlEnumerable} from "../utils/access/AccessControlEnumerable.s import {UnstructuredStorage} from "../lib/UnstructuredStorage.sol"; import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; import {Versioned} from "../utils/Versioned.sol"; -import {ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData} from "../lib/ReportExitLimitUtils.sol"; +import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; import {PausableUntil} from "../utils/PausableUntil.sol"; import {IValidatorsExitBus} from "../interfaces/IValidatorExitBus.sol"; @@ -28,8 +28,8 @@ interface IStakingRouter { contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; - using ReportExitLimitUtilsStorage for bytes32; - using ReportExitLimitUtils for ExitRequestLimitData; + using ExitLimitUtilsStorage for bytes32; + using ExitLimitUtils for ExitRequestLimitData; /// @dev Errors error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); @@ -42,9 +42,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error InvalidRequestsDataLength(); error InvalidRequestsData(); error RequestsAlreadyDelivered(); - // TODO: create better name than prevLimit - error ExitRequestsLimit(uint256 requestsCount, uint256 prevLimit); - error TWExitRequestsLimit(uint256 requestsCount, uint256 prevLimit); + error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + error TWExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); error InvalidPubkeysArray(); error NoExitRequestProvided(); error InvalidRequestsDataSortOrder(); @@ -59,12 +58,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorPubkey, uint256 timestamp ); - event ExitRequestsLimitSet( - uint256 maxExitRequestsLimit, - uint256 exitRequestsLimitIncreasePerBlock, - uint256 maxTWExitRequestsLimit, - uint256 twExitRequestsLimitIncreasePerBlock - ); + event ExitRequestsLimitSet(uint256 exitRequestsLimit, uint256 twExitRequestsLimit); event DirectExitRequest( uint256 indexed stakingModuleId, @@ -117,8 +111,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); - bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestsLimit"); - bytes32 public constant TW_EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxTWExitRequestsLimit"); + bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.exitDailyLimit"); + bytes32 public constant TW_EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.twExitDailyLimit"); /// @dev Ensures the contract’s ETH balance is unchanged. modifier preservesEthBalance() { @@ -171,26 +165,31 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - uint256 toDeliver; + uint256 requestsToDeliver; + + if (exitRequestLimitData.isExitDailyLimitSet()) { + uint256 day = _getTimestamp() / 1 days; + uint256 limit = exitRequestLimitData.remainingLimit(day); - if (exitRequestLimitData.isExitRequestLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); if (limit == 0) { revert ExitRequestsLimit(undeliveredItemsCount, limit); } - toDeliver = undeliveredItemsCount > limit ? limit : undeliveredItemsCount; + requestsToDeliver = undeliveredItemsCount > limit ? limit : undeliveredItemsCount; EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitRequestsLimit(limit - toDeliver) + exitRequestLimitData.updateRequestsCounter(day, requestsToDeliver) ); } else { - toDeliver = undeliveredItemsCount; + requestsToDeliver = undeliveredItemsCount; } - _processExitRequestsList(request.data, deliveredItemsCount, toDeliver); - requestStatus.deliverHistory.push(DeliveryHistory(deliveredItemsCount + toDeliver - 1, _getTimestamp())); - requestStatus.deliveredItemsCount += toDeliver; + _processExitRequestsList(request.data, deliveredItemsCount, requestsToDeliver); + + requestStatus.deliverHistory.push( + DeliveryHistory(deliveredItemsCount + requestsToDeliver - 1, _getTimestamp()) + ); + requestStatus.deliveredItemsCount += requestsToDeliver; } /// @notice Triggers exits on the EL via the Withdrawal Vault contract after @@ -219,28 +218,27 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkContractVersion(requestStatus.contractVersion); - // limit check - ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + address withdrawalVaultAddr = LOCATOR.withdrawalVault(); + uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - if (exitRequestLimitData.isExitRequestLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + if (msg.value < keyIndexes.length * withdrawalFee) { + revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); + } + + ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + if (exitRequestLimitData.isExitDailyLimitSet()) { + uint256 day = _getTimestamp() / 1 days; + uint256 limit = exitRequestLimitData.remainingLimit(day); if (keyIndexes.length > limit) { revert TWExitRequestsLimit(keyIndexes.length, limit); } TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitRequestsLimit(limit - keyIndexes.length) + exitRequestLimitData.updateRequestsCounter(day, keyIndexes.length) ); } - address withdrawalVaultAddr = LOCATOR.withdrawalVault(); - uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); - - if (msg.value < keyIndexes.length * withdrawalFee) { - revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); - } - uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); @@ -321,27 +319,26 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; - ExitRequestLimitData memory twExitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION - .getStorageExitRequestLimit(); + if (msg.value < withdrawalFee * requestsCount) { + revert InsufficientPayment(withdrawalFee, requestsCount, msg.value); + } - if (twExitRequestLimitData.isExitRequestLimitSet()) { - uint256 limit = twExitRequestLimitData.calculateCurrentExitRequestLimit(); + ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + uint256 timestamp = _getTimestamp(); + + if (exitRequestLimitData.isExitDailyLimitSet()) { + uint256 day = timestamp / 1 days; + uint256 limit = exitRequestLimitData.remainingLimit(day); if (requestsCount > limit) { revert TWExitRequestsLimit(requestsCount, limit); } TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - twExitRequestLimitData.updatePrevExitRequestsLimit(limit - requestsCount) + exitRequestLimitData.updateRequestsCounter(day, requestsCount) ); } - if (msg.value < withdrawalFee * requestsCount) { - revert InsufficientPayment(withdrawalFee, requestsCount, msg.value); - } - - uint256 timestamp = _getTimestamp(); - bytes calldata data = exitData.validatorsPubkeys; for (uint256 i = 0; i < requestsCount; i++) { @@ -371,39 +368,23 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa return _refundFee(requestsCount * withdrawalFee); } - function setExitRequestLimit(ExitLimits calldata limits) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { - require(limits.maxExitRequestsLimit != 0, "ZERO_MAX_EXIT_REQUEST_LIMIT"); - require(limits.maxTWExitRequestsLimit != 0, "ZERO_MAX_TW_EXIT_REQUEST_LIMIT"); - require( - limits.maxExitRequestsLimit >= limits.exitRequestsLimitIncreasePerBlock, - "TOO_LARGE_EXIT_LIMIT_INCREASE" - ); - require( - limits.maxTWExitRequestsLimit >= limits.twExitRequestsLimitIncreasePerBlock, - "TOO_LARGE_TW_EXIT_LIMIT_INCREASE" - ); - // TODO: what maximum value for block distance to replenish limits we can set here? limits.maxExitRequestsLimit / limits.exitRequestsLimitIncreasePerBlock + function setExitRequestLimit( + uint256 exitsDailyLimit, + uint256 twExitsDailyLimit + ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { + require(exitsDailyLimit != 0, "ZERO_MAX_EXIT_REQUEST_LIMIT"); + require(twExitsDailyLimit != 0, "ZERO_MAX_TW_EXIT_REQUEST_LIMIT"); + require(exitsDailyLimit >= twExitsDailyLimit, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitRequestLimit( - limits.maxExitRequestsLimit, - limits.exitRequestsLimitIncreasePerBlock - ) + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(exitsDailyLimit) ); TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitRequestLimit( - limits.maxTWExitRequestsLimit, - limits.twExitRequestsLimitIncreasePerBlock - ) + TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(twExitsDailyLimit) ); - emit ExitRequestsLimitSet( - limits.maxExitRequestsLimit, - limits.exitRequestsLimitIncreasePerBlock, - limits.maxTWExitRequestsLimit, - limits.twExitRequestsLimitIncreasePerBlock - ); + emit ExitRequestsLimitSet(exitsDailyLimit, twExitsDailyLimit); } function getExitRequestsDeliveryHistory( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index cb51dabea8..08f80053e7 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -8,7 +8,7 @@ import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; import { BaseOracle } from "./BaseOracle.sol"; import { ValidatorsExitBus } from "./ValidatorsExitBus.sol"; -import { ReportExitLimitUtils, ReportExitLimitUtilsStorage, ExitRequestLimitData } from "../lib/ReportExitLimitUtils.sol"; +import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; interface IOracleReportSanityChecker { function checkExitBusOracleReport(uint256 _exitRequestsCount) external view; @@ -18,8 +18,8 @@ interface IOracleReportSanityChecker { contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { using UnstructuredStorage for bytes32; using SafeCast for uint256; - using ReportExitLimitUtilsStorage for bytes32; - using ReportExitLimitUtils for ExitRequestLimitData; + using ExitLimitUtilsStorage for bytes32; + using ExitLimitUtils for ExitRequestLimitData; error AdminCannotBeZero(); @@ -244,15 +244,16 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { // Check VEB common limit ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - if (exitRequestLimitData.isExitRequestLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitRequestLimit(); + if (exitRequestLimitData.isExitDailyLimitSet()) { + uint256 day = _getTimestamp() / 1 days; + uint256 limit = exitRequestLimitData.remainingLimit(day); if (data.requestsCount > limit) { revert ExitRequestsLimit(data.requestsCount, limit); } EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitRequestsLimit(limit - data.requestsCount) + exitRequestLimitData.updateRequestsCounter(day, data.requestsCount) ); } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index 2f470f9e23..b2c8b78a8c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -43,13 +43,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { data: string; } - interface ExitRequestLimitData { - prevExitRequestsBlockNumber: number; - prevExitRequestsLimit: number; - maxExitRequestsLimitGrowthBlocks: number; - maxExitRequestsLimit: number; - } - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { const pubkeyHex = de0x(valPubkey); expect(pubkeyHex.length).to.equal(48 * 2); @@ -177,125 +170,138 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { .withArgs(2); }); - it("Should deliver part of request if limit is smaller than number of requests", async () => { - const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); - await oracle.grantRole(role, authorizedEntity); - const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit({ - maxExitRequestsLimit: 2, - exitRequestsLimitIncreasePerBlock: 1, - twExitRequestsLimitIncreasePerBlock: 1, - maxTWExitRequestsLimit: 2, + describe("Exit Request Limits", function () { + before(async () => { + const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(role, authorizedEntity); + await consensus.advanceTimeBy(24 * 60 * 60); }); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1, 2, 1); - - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, - ]; - - exitRequest = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; - - exitRequestHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), - ); - // const history0 = await oracle.getDeliveryHistory(exitRequestHash); - // expect(history0.length).to.eq(0); + it("Should deliver request fully as it is below limit (5)", async () => { + await oracle.connect(authorizedEntity).setExitRequestLimit(5, 2); - const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); - await expect(submitTx).to.emit(oracle, "StoredExitRequestHash"); - - const emitTx = await oracle.emitExitEvents(exitRequest); - - const receipt = await emitTx.wait(); - expect(receipt?.logs.length).to.eq(2); + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + ]; - const timestamp = await oracle.getTime(); + exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[0].moduleId, - exitRequests[0].nodeOpId, - exitRequests[0].valIndex, - exitRequests[0].valPubkey, - timestamp, + exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), ); - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[1].moduleId, - exitRequests[1].nodeOpId, - exitRequests[1].valIndex, - exitRequests[1].valPubkey, - timestamp, - ); + await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + const emitTx = await oracle.emitExitEvents(exitRequest); + const receipt = await emitTx.wait(); - // const history1 = await oracle.getDeliveryHistory(exitRequestHash); - // expect(history1.length).to.eq(1); - // expect(history1[0].lastDeliveredKeyIndex).to.eq(1); + expect(receipt?.logs.length).to.eq(2); - const emitTx2 = await oracle.emitExitEvents(exitRequest); + const timestamp = await oracle.getTime(); - const receipt2 = await emitTx2.wait(); - expect(receipt2?.logs.length).to.eq(1); + for (const request of exitRequests) { + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + }); - await expect(emitTx2) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[2].moduleId, - exitRequests[2].nodeOpId, - exitRequests[2].valIndex, - exitRequests[2].valPubkey, - timestamp, + it("Should deliver part of request equal to remaining limit", async () => { + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), ); - // const history2 = await oracle.getDeliveryHistory(exitRequestHash); - // expect(history2.length).to.eq(2); - // expect(history2[1].lastDeliveredKeyIndex).to.eq(2); + await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + const emitTx = await oracle.emitExitEvents(exitRequest); + const receipt = await emitTx.wait(); - const emitTx3 = await oracle.emitExitEvents(exitRequest); + expect(receipt?.logs.length).to.eq(3); - const receipt3 = await emitTx2.wait(); - expect(receipt3?.logs.length).to.eq(1); - - await expect(emitTx3) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[3].moduleId, - exitRequests[3].nodeOpId, - exitRequests[3].valIndex, - exitRequests[3].valPubkey, - timestamp, - ); + const timestamp = await oracle.getTime(); - // const history3 = await oracle.getDeliveryHistory(exitRequestHash); - // expect(history3.length).to.eq(3); - // expect(history3[2].lastDeliveredKeyIndex).to.eq(3); + for (let i = 0; i < 3; i++) { + const request = exitRequests[i]; + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + }); - const emitTx4 = await oracle.emitExitEvents(exitRequest); + it("Should revert when limit exceeded for the day", async () => { + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + await expect(oracle.emitExitEvents(exitRequest)) + .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") + .withArgs(2, 0); + }); - const receipt4 = await emitTx2.wait(); - expect(receipt4?.logs.length).to.eq(1); + it("Should process remaining requests after a day passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + const emitTx = await oracle.emitExitEvents(exitRequest); + const timestamp = await oracle.getTime(); + + for (let i = 3; i < 5; i++) { + const request = exitRequests[i]; + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + }); - await expect(emitTx4) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[4].moduleId, - exitRequests[4].nodeOpId, - exitRequests[4].valIndex, - exitRequests[4].valPubkey, - timestamp, + it("Should revert when no new requests to deliver", async () => { + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + await expect(oracle.emitExitEvents(exitRequest)).to.be.revertedWithCustomError( + oracle, + "RequestsAlreadyDelivered", ); - - // const history4 = await oracle.getDeliveryHistory(exitRequestHash); - // expect(history4.length).to.eq(4); - // expect(history4[3].lastDeliveredKeyIndex).to.eq(4); - - await expect(oracle.emitExitEvents(exitRequest)).to.be.revertedWithCustomError(oracle, "RequestsAlreadyDelivered"); + }); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 7d1682f134..0c749e25b8 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -610,16 +610,15 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); after(async () => await Snapshot.restore(originalState)); + it("some time passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); + it("Set exit limit", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, admin); - const exitLimitTx = await oracle.connect(admin).setExitRequestLimit({ - maxExitRequestsLimit: 4, - exitRequestsLimitIncreasePerBlock: 1, - maxTWExitRequestsLimit: 4, - twExitRequestsLimitIncreasePerBlock: 1, - }); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1, 4, 1); + const exitLimitTx = await oracle.connect(admin).setExitRequestLimit(7, 7); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(7, 7); }); it("deliver report by actor different from oracle", async () => { @@ -672,10 +671,23 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { ]; const { reportData } = await prepareReportAndSubmitHash(requests); - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( - oracle, - "ExitRequestsLimit", - ); + await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) + .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") + .withArgs(4, 3); + }); + + it("day passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); + + it("Limit regenerated in a day", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 3, valIndex: 4, valPubkey: PUBKEYS[4] }, + ]; + const { reportData } = await prepareReportAndSubmitHash(requests); const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); const timestamp = await consensus.getTime(); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts index a97943755a..408d08d4ce 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts @@ -118,17 +118,16 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); }; + it("some time passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); + it("Should set limit for tw", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, admin); - const exitLimitTx = await oracle.connect(admin).setExitRequestLimit({ - maxExitRequestsLimit: 4, - exitRequestsLimitIncreasePerBlock: 1, - twExitRequestsLimitIncreasePerBlock: 1, - maxTWExitRequestsLimit: 2, - }); + const exitLimitTx = await oracle.connect(admin).setExitRequestLimit(8, 8); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1, 2, 1); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(8, 8); }); it("initially, consensus report is empty and is not being processed", async () => { @@ -230,27 +229,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect(procState.requestsSubmitted).to.equal(exitRequests.length); }); - it("Out of tw exit request limit", async () => { - await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { - value: 10, - }), - ) - .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") - .withArgs(3, 2); - }); - - it("Increase limit", async () => { - const exitLimitTx = await oracle.connect(admin).setExitRequestLimit({ - maxExitRequestsLimit: 4, - exitRequestsLimitIncreasePerBlock: 1, - twExitRequestsLimitIncreasePerBlock: 1, - maxTWExitRequestsLimit: 10, - }); - - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 1, 10, 1); - }); - it("someone submitted exit report data and triggered exit", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, @@ -337,4 +315,44 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") .withArgs(5, 4); }); + + it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { + value: 10, + }), + ) + .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") + .withArgs(3, 1); + }); + + it("some time passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); + + it("Limit regenerated in a day", async () => { + const tx = await oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { + value: 10, + }); + + const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; + const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); + await expect(tx) + .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") + .withArgs("0x" + concatenatedPubKeys); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[0].moduleId, exitRequests[0].nodeOpId, pubkeys[0], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[1].moduleId, exitRequests[1].nodeOpId, pubkeys[1], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[2], 1, 0); + + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 7); + }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index ed75bfec87..78969669aa 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -64,17 +64,16 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { await deploy(); }); + it("some time passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); + it("Should set limit for tw", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, authorizedEntity); - const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit({ - maxExitRequestsLimit: 2, - exitRequestsLimitIncreasePerBlock: 1, - twExitRequestsLimitIncreasePerBlock: 1, - maxTWExitRequestsLimit: 2, - }); + const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit(4, 4); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1, 2, 1); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 4); }); it("Should revert without DIRECT_EXIT_ROLE role", async () => { @@ -99,41 +98,66 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { await oracle.grantRole(role, authorizedEntity); }); - it("Out of tw exit request limit", async () => { + it("Not enough fee", async () => { await expect( oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { - value: 4, + value: 2, }), ) - .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") - .withArgs(3, 2); + .to.be.revertedWithCustomError(oracle, "InsufficientPayment") + .withArgs(1, 3, 2); }); - it("Increase limit", async () => { - const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit({ - maxExitRequestsLimit: 2, - exitRequestsLimitIncreasePerBlock: 1, - twExitRequestsLimitIncreasePerBlock: 1, - maxTWExitRequestsLimit: 4, + it("Emit ValidatorExit event and should trigger withdrawals", async () => { + const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + value: 4, }); + const timestamp = await oracle.getTime(); + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(2, 1, 4, 1); + await expect(tx) + .to.emit(oracle, "DirectExitRequest") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp); + await expect(tx) + .to.emit(oracle, "DirectExitRequest") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp); + await expect(tx) + .to.emit(oracle, "DirectExitRequest") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], 1, 0); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], 1, 0); }); - it("Not enough fee", async () => { + it("Out of tw exit request limit", async () => { await expect( oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { - value: 2, + value: 4, }), ) - .to.be.revertedWithCustomError(oracle, "InsufficientPayment") - .withArgs(1, 3, 2); + .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") + .withArgs(3, 1); }); - it("Emit ValidatorExit event and should trigger withdrawals", async () => { - const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + it("some time passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); + + it("Limit regenerated in a day", async () => { + const tx = oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { value: 4, }); + const timestamp = await oracle.getTime(); await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); From 204529522caff05000e24d199ff917d3e1460294 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 23 Apr 2025 13:41:42 +0200 Subject: [PATCH 101/405] feat: tw nor implementation --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 144 +++++++----------- test/0.4.24/nor/nor.aux.test.ts | 4 +- test/0.4.24/nor/nor.exit.manager.test.ts | 2 +- 3 files changed, 56 insertions(+), 94 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 31035b1331..46fdad469f 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -76,7 +76,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 exitType ); event ExitDeadlineThresholdChanged(uint256 threshold); - event PenaltyFramesCountChanged(uint256 penaltyFramesCount); // Enum to represent the state of the reward distribution process enum RewardDistributionState { @@ -127,8 +126,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // StuckPenaltyStats /// @dev stuck keys count from oracle report + /// @dev [DEPRECATED] uint8 internal constant STUCK_VALIDATORS_COUNT_OFFSET = 0; /// @dev refunded keys count from dao + /// @dev [DEPRECATED] uint8 internal constant REFUNDED_VALIDATORS_COUNT_OFFSET = 1; /// @dev extra penalty time after stuck keys resolved (refunded and/or exited) /// @notice field is also used as flag for "half-cleaned" penalty status @@ -136,6 +137,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// `STUCK_VALIDATORS_COUNT <= REFUNDED_VALIDATORS_COUNT && STUCK_PENALTY_END_TIMESTAMP <= refund timestamp + STUCK_PENALTY_DELAY` /// When operator refund all stuck validators and time has pass STUCK_PENALTY_DELAY, but STUCK_PENALTY_END_TIMESTAMP not zeroed, /// then Operator can receive rewards but can't get new deposits until the new Oracle report or `clearNodeOperatorPenalty` is called. + /// @dev [DEPRECATED] uint8 internal constant STUCK_PENALTY_END_TIMESTAMP_OFFSET = 2; // Summary SigningKeysStats @@ -174,14 +176,15 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // bytes32 internal constant TYPE_POSITION = keccak256("lido.NodeOperatorsRegistry.type"); bytes32 internal constant TYPE_POSITION = 0xbacf4236659a602d72c631ba0b0d67ec320aaf523f3ae3590d7faee4f42351d0; + // bytes32 internal constant STUCK_PENALTY_DELAY_POSITION = keccak256("lido.NodeOperatorsRegistry.stuckPenaltyDelay"); + bytes32 internal constant STUCK_PENALTY_DELAY_POSITION = 0x8e3a1f3826a82c1116044b334cae49f3c3d12c3866a1c4b18af461e12e58a18e; + // bytes32 internal constant REWARD_DISTRIBUTION_STATE = keccak256("lido.NodeOperatorsRegistry.rewardDistributionState"); bytes32 internal constant REWARD_DISTRIBUTION_STATE = 0x4ddbb0dcdc5f7692e494c15a7fca1f9eb65f31da0b5ce1c3381f6a1a1fd579b6; - // Number of penalty frames to apply for a delayed exit - bytes32 internal constant PENALTY_FRAMES_COUNT = keccak256("lido.NodeOperatorsRegistry.penaltyFramesCount"); - // Threshold in seconds after which a delayed exit is penalized - bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = keccak256("lido.NodeOperatorsRegistry.exitDelayThresholdSeconds"); + // bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = keccak256("lido.NodeOperatorsRegistry.exitDelayThresholdSeconds"); + bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = 0x96656d3ece9cdbe3bd729ff6d7df8d0aeb457ff7c7c42372184ae30b10b37976; // @@ -215,6 +218,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// [....totalSigningKeysCount.....:.........:<--------:---------]-------> /// : : : : : Packed64x4.Packed signingKeysStats; + Packed64x4.Packed stuckPenaltyStats; Packed64x4.Packed targetValidatorsStats; } @@ -222,13 +226,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Packed64x4.Packed summarySigningKeysStats; } - struct NodeOperatorExitDelayStats { - // Map public key hash to boolean indicating if it's been processed - mapping(bytes32 => bool) processedKeys; - // Total count of validators with exit delays for this operator - uint256 totalExitDelayPenaltyCount; - } - // // STORAGE VARIABLES // @@ -236,7 +233,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @dev Mapping of all node operators. Mapping is used to be able to extend the struct. mapping(uint256 => NodeOperator) internal _nodeOperators; NodeOperatorSummary internal _nodeOperatorSummary; - mapping(uint256 => NodeOperatorExitDelayStats) internal _nodeOperatorExitDelayStats; // // METHODS @@ -274,10 +270,18 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _initialize_v4() internal { _setContractVersion(4); - PENALTY_FRAMES_COUNT.setStorageUint256(3); EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(86400); } + /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. + /// For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md + /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L230 + // function finalizeUpgrade_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) external + + /// @notice A function to finalize upgrade to v3 (from v2). Can be called only once. + /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L298 + // function finalizeUpgrade_v3() external + /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators /// @param _name Human-readable name /// @param _rewardAddress Ethereum 1 address which receives stETH rewards for this operator @@ -543,17 +547,20 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.ReadyForDistribution); } - /// @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 + /// @notice [DEPRECATED] `_stuckValidatorsCount` is ignored. + /// @notice Unsafely updates the number of validators in the EXITED/STUCK states for node operator with given id. /// @param _nodeOperatorId Id of the node operator /// @param _exitedValidatorsCount New number of EXITED validators for the node operator + /// @dev _stuckValidatorsCount [DEPRECATED] Ignored. function unsafeUpdateValidatorsCount( uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount + uint256 _exitedValidatorsCount, + uint256 /* _stuckValidatorsCount */ ) external { _onlyExistedNodeOperator(_nodeOperatorId); _auth(STAKING_ROUTER_ROLE); + // _updateStuckValidatorsCount(_nodeOperatorId, _stuckValidatorsCount); // removed _updateExitedValidatorsCount(_nodeOperatorId, _exitedValidatorsCount, true /* _allowDecrease */ ); _increaseValidatorsKeysNonce(); } @@ -900,6 +907,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } /// @notice Returns the rewards distribution proportional to the effective stake for each node operator. + /// @notice [DEPRECATED] The `penalized` array is no longer relevant and always contains only `false`. /// @param _totalRewardShares Total amount of reward shares to distribute. function getRewardsDistribution(uint256 _totalRewardShares) public @@ -934,11 +942,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { recipients[idx] = _nodeOperators[operatorId].rewardAddress; // prefill shares array with 'key share' for recipient, see below shares[idx] = activeValidatorsCount; - penalized[idx] = isOperatorPenalized(operatorId); - if (penalized[idx]) { - _decrementExitDelayPenalty(operatorId); - } + // [DEPRECATED] Penalized flag is no longer relevant. Always false. + // penalized[idx] = false; ++idx; } @@ -1039,7 +1045,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } - function getExitDeadlineThreshold() public view returns (uint256) { + function exitDeadlineThreshold() public view returns (uint256) { return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); } @@ -1050,17 +1056,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit ExitDeadlineThresholdChanged(_threshold); } - function getPenaltyFramesCount() public view returns (uint256) { - return PENALTY_FRAMES_COUNT.getStorageUint256(); - } - - function setPenaltyFramesCount(uint256 _framesCount) external { - _auth(MANAGE_NODE_OPERATOR_ROLE); - require(_framesCount > 0, "INVALID_PENALTY_FRAMES_COUNT"); - PENALTY_FRAMES_COUNT.setStorageUint256(_framesCount); - emit PenaltyFramesCountChanged(_framesCount); - } - function onValidatorExitTriggered( uint256 _nodeOperatorId, bytes _publicKey, @@ -1073,17 +1068,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } function isValidatorExitDelayPenaltyApplicable( - uint256 _nodeOperatorId, + uint256, // _nodeOperatorId uint256, // _proofSlotTimestamp - bytes _publicKey, + bytes, // _publicKey uint256 _eligibleToExitInSec ) external view returns (bool) { - bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); - bool isProcessed = _isKeyProcessed(_nodeOperatorId, processedKeyHash); - if (isProcessed) { - return false; - } - return _eligibleToExitInSec >= getExitDeadlineThreshold(); + return _eligibleToExitInSec >= exitDeadlineThreshold(); } function reportValidatorExitDelay( @@ -1096,52 +1086,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { require(_publicKey.length == 48, "INVALID_PUBLIC_KEY"); // Check if exit delay exceeds the threshold - require(_eligibleToExitInSec >= getExitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); - - // Check if the key has been processed already - bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); - bool isProcessed = _isKeyProcessed(_nodeOperatorId, processedKeyHash); - require(!isProcessed, "KEY_ALREADY_PROCESSED"); - - // Mark the key as processed - _markKeyAsProcessed(_nodeOperatorId, processedKeyHash); - - // Increment penalty stats for the node operator - _increaseExitDelayPenaltyBy(_nodeOperatorId, getPenaltyFramesCount()); + require(_eligibleToExitInSec >= exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } - - function _isKeyProcessed(uint256 _nodeOperatorId, bytes32 _keyHash) internal view returns (bool) { - return _nodeOperatorExitDelayStats[_nodeOperatorId].processedKeys[_keyHash]; - } - - function _markKeyAsProcessed(uint256 _nodeOperatorId, bytes32 _keyHash) internal { - _nodeOperatorExitDelayStats[_nodeOperatorId].processedKeys[_keyHash] = true; - } - - function _increaseExitDelayPenaltyBy(uint256 _nodeOperatorId, uint256 _penaltyCount) internal { - _nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount += _penaltyCount; - } - - function _decrementExitDelayPenalty(uint256 _nodeOperatorId) internal { - if (_nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount > 0) { - _nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount--; - } - } - - function getExitDelayPenaltyCount(uint256 _nodeOperatorId) external view returns (uint256) { - return _nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount; - } - - function isOperatorPenalized(uint256 _nodeOperatorId) public view returns (bool) { - if (_nodeOperatorExitDelayStats[_nodeOperatorId].totalExitDelayPenaltyCount > 0) { - return true; - } - return false; - } - function _removeUnusedSigningKeys(uint256 _nodeOperatorId, uint256 _fromIndex, uint256 _keysCount) internal { _onlyExistedNodeOperator(_nodeOperatorId); _onlyNodeOperatorManager(msg.sender, _nodeOperatorId); @@ -1287,6 +1236,20 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { depositableValidatorsCount = totalMaxValidators - totalDepositedValidators; } + /// @notice [DEPRECATED] Penalty logic removed. Always returns `false`. + /// @dev _nodeOperatorId Ignored. + /// @return Always returns `false`. + function isOperatorPenalized(uint256 /* _nodeOperatorId */) public pure returns (bool) { + return false; + } + + /// @notice [DEPRECATED] Penalty logic removed. Always returns `true`. + /// @dev _nodeOperatorId Ignored. + /// @return Always returns `true`. + function isOperatorPenaltyCleared(uint256 /* _nodeOperatorId */) public pure returns (bool) { + return true; + } + /// @notice Returns total number of node operators function getNodeOperatorsCount() public view returns (uint256) { return TOTAL_OPERATORS_COUNT_POSITION.getStorageUint256(); @@ -1348,20 +1311,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return; } - (address[] memory recipients, uint256[] memory shares, bool[] memory penalized) = + (address[] memory recipients, uint256[] memory shares,) = getRewardsDistribution(sharesToDistribute); 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; - if (penalized[idx]) { - /// @dev half reward punishment - /// @dev ignore remainder since it accumulated on contract balance - shares[idx] >>= 1; - 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]); @@ -1375,6 +1331,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return ILidoLocator(LIDO_LOCATOR_POSITION.getStorageAddress()); } + /// @notice [DEPRECATED] Stuck penalty delay logic removed. Always returns 0. + /// @return Always returns 0. + function getStuckPenaltyDelay() public pure returns (uint256) { + return 0; + } + /// @dev Get the current reward distribution state, anyone can monitor this state /// and distribute reward (call distributeReward method) among operators when it's `ReadyForDistribution` function getRewardDistributionState() public view returns (RewardDistributionState) { diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index eb1e9c402f..9266a7daee 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -149,11 +149,11 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { }); it("Reverts if no such an operator exists", async () => { - await expect(nor.unsafeUpdateValidatorsCount(3n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); + await expect(nor.unsafeUpdateValidatorsCount(3n, 0n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); }); it("Reverts if has not STAKING_ROUTER_ROLE assigned", async () => { - await expect(nor.connect(stranger).unsafeUpdateValidatorsCount(firstNodeOperatorId, 0n)).to.be.revertedWith( + await expect(nor.connect(stranger).unsafeUpdateValidatorsCount(firstNodeOperatorId, 0n, 0n)).to.be.revertedWith( "APP_AUTH_FAILED", ); }); diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 170aec413b..e2f3d2713c 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -196,7 +196,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { context("exitDeadlineThreshold", () => { it("returns the expected value", async () => { - const threshold = await nor.getExitDeadlineThreshold(); + const threshold = await nor.exitDeadlineThreshold(); expect(threshold).to.equal(86400n); }); }); From 6a46d4662a00e063aa90d46575c6496cfa2e4efd Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 23 Apr 2025 13:42:18 +0200 Subject: [PATCH 102/405] feat: remove libs for withdrawal and consolidation requests --- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 167 ++-- .../common/lib/Eip7251MaxEffectiveBalance.sol | 68 -- .../common/lib/TriggerableWithdrawals.sol | 182 ---- .../EIP7002WithdrawalRequest__Mock.sol | 0 test/0.8.9/contracts/RefundFailureTester.sol | 39 - test/0.8.9/withdrawalVault.test.ts | 905 ------------------ .../withdrawalVault}/eip7002Mock.ts | 0 .../withdrawalVault}/utils.ts | 0 .../withdrawalVault/withdrawalVault.test.ts | 589 ++++++++++++ .../TriggerableWithdrawals__Harness.sol | 37 - .../triggerableWithdrawals.test.ts | 541 ----------- 11 files changed, 676 insertions(+), 1852 deletions(-) delete mode 100644 contracts/common/lib/Eip7251MaxEffectiveBalance.sol delete mode 100644 contracts/common/lib/TriggerableWithdrawals.sol rename test/{common => 0.8.9}/contracts/EIP7002WithdrawalRequest__Mock.sol (100%) delete mode 100644 test/0.8.9/contracts/RefundFailureTester.sol delete mode 100644 test/0.8.9/withdrawalVault.test.ts rename test/{common/lib/triggerableWithdrawals => 0.8.9/withdrawalVault}/eip7002Mock.ts (100%) rename test/{common/lib/triggerableWithdrawals => 0.8.9/withdrawalVault}/utils.ts (100%) create mode 100644 test/0.8.9/withdrawalVault/withdrawalVault.test.ts delete mode 100644 test/common/contracts/TriggerableWithdrawals__Harness.sol delete mode 100644 test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index 9a7ccd8b06..ac9795a511 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -7,25 +7,31 @@ pragma solidity 0.8.9; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {PausableUntil} from "./utils/PausableUntil.sol"; -import {TriggerableWithdrawals} from "../common/lib/TriggerableWithdrawals.sol"; -import {Eip7251MaxEffectiveBalance} from "../common/lib/Eip7251MaxEffectiveBalance.sol"; - /** * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests * @dev This contract enables validators to submit EIP-7002 withdrawal requests * and manages the associated fees. */ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUntil { + address constant CONSOLIDATION_REQUEST = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; + address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; + bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); - bytes32 public constant ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE"); + bytes32 public constant ADD_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_WITHDRAWAL_REQUEST_ROLE"); bytes32 public constant ADD_CONSOLIDATION_REQUEST_ROLE = keccak256("ADD_CONSOLIDATION_REQUEST_ROLE"); uint256 internal constant PUBLIC_KEY_LENGTH = 48; - error InsufficientFee(uint256 providedFee, uint256 requiredFee); - error ExcessFeeRefundFailed(); + error ZeroArgument(string name); + error MalformedPubkeysArray(); + error ArraysLengthMismatch(uint256 firstArrayLength, uint256 secondArrayLength); + + error FeeReadFailed(); + error FeeInvalidData(); + error IncorrectFee(uint256 providedFee, uint256 requiredFee); + + error RequestAdditionFailed(bytes callData); /// @dev Ensures the contract’s ETH balance is unchanged. modifier preservesEthBalance() { @@ -69,66 +75,53 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt } /** - * @dev Submits EIP-7002 full withdrawal requests for the specified public keys. - * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. - * Refunds any excess fee to the caller after deducting the total fees, - * which are calculated based on the number of public keys and the current minimum fee per withdrawal request. - * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @notice Reverts if: - * - The caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE`. - * - The provided public key array is empty. - * - Validation of any of the provided public keys fails. - * - The provided total withdrawal fee is insufficient to cover all requests. - * - Refund of the excess fee fails. - */ - function addFullWithdrawalRequests( - bytes calldata pubkeys - ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) whenResumed preservesEthBalance { - uint256 feePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); - uint256 totalFee = _countPubkeys(pubkeys) * feePerRequest; - - _requireSufficientFee(totalFee); - - TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); - - _refundExcessFee(totalFee); - } - - /** - * @dev Submits EIP-7002 partial withdrawal requests for the specified public keys with corresponding amounts. - * Each request instructs a validator to withdraw a specified amount of ETH via their execution layer (0x01) withdrawal credentials. - * Refunds any excess fee to the caller after deducting the total fees, - * which are calculated based on the number of public keys and the current minimum fee per withdrawal request. + * @dev Submits EIP-7002 full or partial withdrawal requests for the specified public keys. + * Each full withdrawal request instructs a validator to fully withdraw its stake and exit its duties as a validator. + * Each partial withdrawal request instructs a validator to withdraw a specified amount of ETH. * * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting partial withdrawals. * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... * * @param amounts An array of 8-byte unsigned integers representing the amounts to be withdrawn for each corresponding public key. + * For full withdrawal requests, the amount should be set to 0. + * For partial withdrawal requests, the amount should be greater than 0. * * @notice Reverts if: - * - The caller does not have the `ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE`. + * - The caller does not have the `ADD_WITHDRAWAL_REQUEST_ROLE`. * - The provided public key array is empty. + * - The provided public key array malformed. * - The provided public key and amount arrays are not of equal length. - * - Full withdrawal requested for any pubkeys (withdrawal amount = 0). - * - Validation of any of the provided public keys fails. - * - The provided total withdrawal fee is insufficient to cover all requests. - * - Refund of the excess fee fails. + * - The provided total withdrawal fee value is invalid. */ - function addPartialWithdrawalRequests( + function addWithdrawalRequests( bytes calldata pubkeys, uint64[] calldata amounts - ) external payable onlyRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE) whenResumed preservesEthBalance { - uint256 feePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee(); - uint256 totalFee = _countPubkeys(pubkeys) * feePerRequest; + ) external payable onlyRole(ADD_WITHDRAWAL_REQUEST_ROLE) whenResumed preservesEthBalance { + if (pubkeys.length == 0) revert ZeroArgument("pubkeys"); + if (pubkeys.length % PUBLIC_KEY_LENGTH != 0) revert MalformedPubkeysArray(); - _requireSufficientFee(totalFee); + uint256 requestsCount = _countPubkeys(pubkeys); + if (requestsCount != amounts.length) revert ArraysLengthMismatch(requestsCount, amounts.length); - TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, feePerRequest); + uint256 feePerRequest = _getRequestFee(WITHDRAWAL_REQUEST); + uint256 totalFee = requestsCount * feePerRequest; - _refundExcessFee(totalFee); + _requireExactFee(totalFee); + + bytes memory request = new bytes(56); + for (uint256 i = 0; i < requestsCount; i++) { + uint64 amount = amounts[i]; + assembly { + calldatacopy(add(request, 32), add(pubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) + mstore(add(request, 80), shl(192, amount)) + } + + (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(request); + + if (!success) { + revert RequestAdditionFailed(request); + } + } } /** @@ -136,14 +129,11 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt * @return The minimum fee required per withdrawal request. */ function getWithdrawalRequestFee() external view returns (uint256) { - return TriggerableWithdrawals.getWithdrawalRequestFee(); + return _getRequestFee(WITHDRAWAL_REQUEST); } /** * @dev Submits EIP-7251 consolidation requests for the specified public keys. - * Each request consolidate validators. - * Refunds any excess fee to the caller after deducting the total fees, - * which are calculated based on the number of requests and the current minimum fee per withdrawal request. * * @param sourcePubkeys A tightly packed array of 48-byte source public keys corresponding to validators requesting consolidation. * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... @@ -153,24 +143,38 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt * * @notice Reverts if: * - The caller does not have the `ADD_CONSOLIDATION_REQUEST_ROLE`. - * - Validation of any of the provided public keys fails. - * - The source and target public key arrays have different lengths. - * - The provided public key arrays are empty. - * - The provided total consolidation fee is insufficient to cover all requests. - * - Refund of the excess fee fails. + * - The provided public key array is empty. + * - The provided public key array malformed. + * - The provided source public key and target public key arrays are not of equal length. + * - The provided total withdrawal fee value is invalid. */ function addConsolidationRequests( bytes calldata sourcePubkeys, bytes calldata targetPubkeys ) external payable onlyRole(ADD_CONSOLIDATION_REQUEST_ROLE) whenResumed preservesEthBalance { - uint256 feePerRequest = Eip7251MaxEffectiveBalance.getConsolidationRequestFee(); - uint256 totalFee = _countPubkeys(sourcePubkeys) * feePerRequest; + if (sourcePubkeys.length == 0) revert ZeroArgument("sourcePubkeys"); + if (sourcePubkeys.length % PUBLIC_KEY_LENGTH != 0) revert MalformedPubkeysArray(); + if (sourcePubkeys.length != targetPubkeys.length) + revert ArraysLengthMismatch(sourcePubkeys.length, sourcePubkeys.length); + + uint256 requestsCount = _countPubkeys(sourcePubkeys); + uint256 feePerRequest = _getRequestFee(CONSOLIDATION_REQUEST); + + _requireExactFee(requestsCount * feePerRequest); - _requireSufficientFee(totalFee); + bytes memory request = new bytes(96); + for (uint256 i = 0; i < requestsCount; i++) { + assembly { + calldatacopy(add(request, 32), add(sourcePubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) + calldatacopy(add(request, 80), add(targetPubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) + } - Eip7251MaxEffectiveBalance.addConsolidationRequests(sourcePubkeys, targetPubkeys, feePerRequest); + (bool success, ) = CONSOLIDATION_REQUEST.call{value: feePerRequest}(request); - _refundExcessFee(totalFee); + if (!success) { + revert RequestAdditionFailed(request); + } + } } /** @@ -178,27 +182,30 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt * @return The minimum fee required per consolidation request. */ function getConsolidationRequestFee() external view returns (uint256) { - return Eip7251MaxEffectiveBalance.getConsolidationRequestFee(); + return _getRequestFee(CONSOLIDATION_REQUEST); } - function _countPubkeys(bytes calldata pubkeys) internal pure returns (uint256) { - return (pubkeys.length / PUBLIC_KEY_LENGTH); - } + function _getRequestFee(address requestedContract) internal view returns (uint256) { + (bool success, bytes memory feeData) = requestedContract.staticcall(""); - function _requireSufficientFee(uint256 requiredFee) internal view { - if (requiredFee > msg.value) { - revert InsufficientFee(msg.value, requiredFee); + if (!success) { + revert FeeReadFailed(); } + + if (feeData.length != 32) { + revert FeeInvalidData(); + } + + return abi.decode(feeData, (uint256)); } - function _refundExcessFee(uint256 fee) internal { - uint256 refund = msg.value - fee; - if (refund > 0) { - (bool success, ) = msg.sender.call{value: refund}(""); + function _countPubkeys(bytes calldata pubkeys) internal pure returns (uint256) { + return (pubkeys.length / PUBLIC_KEY_LENGTH); + } - if (!success) { - revert ExcessFeeRefundFailed(); - } + function _requireExactFee(uint256 requiredFee) internal view { + if (requiredFee != msg.value) { + revert IncorrectFee(msg.value, requiredFee); } } } diff --git a/contracts/common/lib/Eip7251MaxEffectiveBalance.sol b/contracts/common/lib/Eip7251MaxEffectiveBalance.sol deleted file mode 100644 index 9e2208680e..0000000000 --- a/contracts/common/lib/Eip7251MaxEffectiveBalance.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -/* See contracts/COMPILERS.md */ -// solhint-disable-next-line lido/fixed-compiler-version -pragma solidity >=0.8.9 <0.9.0; - -/** - * @title A lib for EIP-7251: Increase the MAX_EFFECTIVE_BALANCE. - * Allow to send consolidation and compound requests for validators. - */ -library Eip7251MaxEffectiveBalance { - error NoConsolidationRequests(); - error MalformedPubkeysArray(); - error PubkeyArraysLengthMismatch(); - error ConsolidationFeeReadFailed(); - error ConsolidationFeeInvalidData(); - error ConsolidationRequestAdditionFailed(bytes callData); - - address constant CONSOLIDATION_REQUEST = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; - uint256 internal constant PUBLIC_KEY_LENGTH = 48; - - function getConsolidationRequestFee() internal view returns (uint256) { - (bool success, bytes memory feeData) = CONSOLIDATION_REQUEST.staticcall(""); - - if (!success) { - revert ConsolidationFeeReadFailed(); - } - - if (feeData.length != 32) { - revert ConsolidationFeeInvalidData(); - } - - return abi.decode(feeData, (uint256)); - } - - function addConsolidationRequests( - bytes calldata sourcePubkeys, - bytes calldata targetPubkeys, - uint256 feePerRequest - ) internal { - if (sourcePubkeys.length == 0) { - revert NoConsolidationRequests(); - } - if (sourcePubkeys.length != targetPubkeys.length) { - revert PubkeyArraysLengthMismatch(); - } - if (sourcePubkeys.length % PUBLIC_KEY_LENGTH != 0) { - revert MalformedPubkeysArray(); - } - - uint256 requestsCount = sourcePubkeys.length / PUBLIC_KEY_LENGTH; - bytes memory request = new bytes(96); - - for (uint256 i = 0; i < requestsCount; i++) { - assembly { - calldatacopy(add(request, 32), add(sourcePubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) - calldatacopy(add(request, 80), add(targetPubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) - } - - (bool success, ) = CONSOLIDATION_REQUEST.call{value: feePerRequest}(request); - - if (!success) { - revert ConsolidationRequestAdditionFailed(request); - } - } - } -} diff --git a/contracts/common/lib/TriggerableWithdrawals.sol b/contracts/common/lib/TriggerableWithdrawals.sol deleted file mode 100644 index b20b7a2bf5..0000000000 --- a/contracts/common/lib/TriggerableWithdrawals.sol +++ /dev/null @@ -1,182 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -/* See contracts/COMPILERS.md */ -// solhint-disable-next-line lido/fixed-compiler-version -pragma solidity >=0.8.9 <0.9.0; - -/** - * @title A lib for EIP-7002: Execution layer triggerable withdrawals. - * Allow validators to trigger withdrawals and exits from their execution layer (0x01) withdrawal credentials. - */ -library TriggerableWithdrawals { - address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; - - uint256 internal constant PUBLIC_KEY_LENGTH = 48; - uint256 internal constant WITHDRAWAL_AMOUNT_LENGTH = 8; - uint256 internal constant WITHDRAWAL_REQUEST_CALLDATA_LENGTH = 56; - - error WithdrawalFeeReadFailed(); - error WithdrawalFeeInvalidData(); - error WithdrawalRequestAdditionFailed(bytes callData); - - error NoWithdrawalRequests(); - error MalformedPubkeysArray(); - error PartialWithdrawalRequired(uint256 index); - error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount); - - /** - * @dev Send EIP-7002 full withdrawal requests for the specified public keys. - * Each request instructs a validator to fully withdraw its stake and exit its duties as a validator. - * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @param feePerRequest The withdrawal fee for each withdrawal request. - * - Must be greater than or equal to the current minimal withdrawal fee. - * - * @notice Reverts if: - * - Validation of the public keys fails. - * - The provided fee per request is insufficient. - * - The contract has an insufficient balance to cover the total fees. - */ - function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) internal { - uint256 keysCount = _validateAndCountPubkeys(pubkeys); - - bytes memory callData = new bytes(56); - - for (uint256 i = 0; i < keysCount; i++) { - _copyPubkeyToMemory(pubkeys, callData, i); - - (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(callData); - - if (!success) { - revert WithdrawalRequestAdditionFailed(callData); - } - } - } - - /** - * @dev Send EIP-7002 partial withdrawal requests for the specified public keys with corresponding amounts. - * Each request instructs a validator to partially withdraw its stake. - * A partial withdrawal is any withdrawal where the amount is greater than zero, - * allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn), - * the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. - * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @param amounts An array of corresponding partial withdrawal amounts for each public key. - * - * @param feePerRequest The withdrawal fee for each withdrawal request. - * - Must be greater than or equal to the current minimal withdrawal fee. - * - * @notice Reverts if: - * - Validation of the public keys fails. - * - The pubkeys and amounts length mismatch. - * - Full withdrawal requested for any pubkeys (withdrawal amount = 0). - * - The provided fee per request is insufficient. - * - The contract has an insufficient balance to cover the total fees. - */ - function addPartialWithdrawalRequests( - bytes calldata pubkeys, - uint64[] calldata amounts, - uint256 feePerRequest - ) internal { - for (uint256 i = 0; i < amounts.length; i++) { - if (amounts[i] == 0) { - revert PartialWithdrawalRequired(i); - } - } - - addWithdrawalRequests(pubkeys, amounts, feePerRequest); - } - - /** - * @dev Send EIP-7002 partial or full withdrawal requests for the specified public keys with corresponding amounts. - * Each request instructs a validator to partially or fully withdraw its stake. - - * 1. A partial withdrawal is any withdrawal where the amount is greater than zero, - * allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn), - * the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested. - * - * 2. A full withdrawal is a withdrawal where the amount is equal to zero, - * allows to fully withdraw validator stake and exit its duties as a validator. - * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting full withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @param amounts An array of corresponding partial withdrawal amounts for each public key. - * - * @param feePerRequest The withdrawal fee for each withdrawal request. - * - Must be greater than or equal to the current minimal withdrawal fee. - * - * @notice Reverts if: - * - Validation of the public keys fails. - * - The pubkeys and amounts length mismatch. - * - The provided fee per request is insufficient. - * - The contract has an insufficient balance to cover the total fees. - */ - function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest) internal { - uint256 keysCount = _validateAndCountPubkeys(pubkeys); - - if (keysCount != amounts.length) { - revert MismatchedArrayLengths(keysCount, amounts.length); - } - - bytes memory callData = new bytes(56); - for (uint256 i = 0; i < keysCount; i++) { - _copyPubkeyToMemory(pubkeys, callData, i); - _copyAmountToMemory(callData, amounts[i]); - - (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(callData); - - if (!success) { - revert WithdrawalRequestAdditionFailed(callData); - } - } - } - - /** - * @dev Retrieves the current EIP-7002 withdrawal fee. - * @return The minimum fee required per withdrawal request. - */ - function getWithdrawalRequestFee() internal view returns (uint256) { - (bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall(""); - - if (!success) { - revert WithdrawalFeeReadFailed(); - } - - if (feeData.length != 32) { - revert WithdrawalFeeInvalidData(); - } - - return abi.decode(feeData, (uint256)); - } - - function _copyPubkeyToMemory(bytes calldata pubkeys, bytes memory target, uint256 keyIndex) private pure { - assembly { - calldatacopy(add(target, 32), add(pubkeys.offset, mul(keyIndex, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) - } - } - - function _copyAmountToMemory(bytes memory target, uint64 amount) private pure { - assembly { - mstore(add(target, 80), shl(192, amount)) - } - } - - function _validateAndCountPubkeys(bytes calldata pubkeys) private pure returns (uint256) { - if (pubkeys.length % PUBLIC_KEY_LENGTH != 0) { - revert MalformedPubkeysArray(); - } - - uint256 keysCount = pubkeys.length / PUBLIC_KEY_LENGTH; - if (keysCount == 0) { - revert NoWithdrawalRequests(); - } - - return keysCount; - } -} diff --git a/test/common/contracts/EIP7002WithdrawalRequest__Mock.sol b/test/0.8.9/contracts/EIP7002WithdrawalRequest__Mock.sol similarity index 100% rename from test/common/contracts/EIP7002WithdrawalRequest__Mock.sol rename to test/0.8.9/contracts/EIP7002WithdrawalRequest__Mock.sol diff --git a/test/0.8.9/contracts/RefundFailureTester.sol b/test/0.8.9/contracts/RefundFailureTester.sol deleted file mode 100644 index 3d054cb9cd..0000000000 --- a/test/0.8.9/contracts/RefundFailureTester.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.8.9; - -interface IWithdrawalVault { - function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; - function addPartialWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; - function getWithdrawalRequestFee() external view returns (uint256); -} - -/** - * @notice This is a contract for testing refund failure in WithdrawalVault contract - */ -contract RefundFailureTester { - IWithdrawalVault private immutable withdrawalVault; - - constructor(address _withdrawalVault) { - withdrawalVault = IWithdrawalVault(_withdrawalVault); - } - - receive() external payable { - revert("Refund failed intentionally"); - } - - function addFullWithdrawalRequests(bytes calldata pubkeys) external payable { - require(msg.value > withdrawalVault.getWithdrawalRequestFee(), "Not enough eth for Refund"); - - // withdrawal vault should fail to refund - withdrawalVault.addFullWithdrawalRequests{value: msg.value}(pubkeys); - } - - function addPartialWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable { - require(msg.value > withdrawalVault.getWithdrawalRequestFee(), "Not enough eth for Refund"); - - // withdrawal vault should fail to refund - withdrawalVault.addPartialWithdrawalRequests{value: msg.value}(pubkeys, amounts); - } -} diff --git a/test/0.8.9/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault.test.ts deleted file mode 100644 index 8d8646cff5..0000000000 --- a/test/0.8.9/withdrawalVault.test.ts +++ /dev/null @@ -1,905 +0,0 @@ -import { expect } from "chai"; -import { ContractTransactionResponse, ZeroAddress } from "ethers"; -import { ethers } from "hardhat"; - -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; - -import { - EIP7002WithdrawalRequest__Mock, - ERC20__Harness, - ERC721__Harness, - Lido__MockForWithdrawalVault, - RefundFailureTester, - WithdrawalVault__Harness, -} from "typechain-types"; - -import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, proxify, streccak } from "lib"; - -import { findEIP7002MockEvents, testEIP7002Mock } from "test/common/lib/triggerableWithdrawals/eip7002Mock"; -import { generateWithdrawalRequestPayload } from "test/common/lib/triggerableWithdrawals/utils"; -import { Snapshot } from "test/suite"; - -const PETRIFIED_VERSION = MAX_UINT256; - -const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); -const ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE"); - -describe("WithdrawalVault.sol", () => { - let owner: HardhatEthersSigner; - let treasury: HardhatEthersSigner; - let validatorsExitBus: HardhatEthersSigner; - let stranger: HardhatEthersSigner; - - let originalState: string; - - let lido: Lido__MockForWithdrawalVault; - let lidoAddress: string; - - let withdrawalsPredeployed: EIP7002WithdrawalRequest__Mock; - - let impl: WithdrawalVault__Harness; - let vault: WithdrawalVault__Harness; - let vaultAddress: string; - - before(async () => { - [owner, treasury, validatorsExitBus, stranger] = await ethers.getSigners(); - - withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); - - expect(await withdrawalsPredeployed.getAddress()).to.equal(EIP7002_ADDRESS); - - lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); - lidoAddress = await lido.getAddress(); - - impl = await ethers.deployContract("WithdrawalVault__Harness", [lidoAddress, treasury.address], owner); - - [vault] = await proxify({ impl, admin: owner }); - vaultAddress = await vault.getAddress(); - }); - - beforeEach(async () => (originalState = await Snapshot.take())); - - afterEach(async () => await Snapshot.restore(originalState)); - - context("Constructor", () => { - it("Reverts if the Lido address is zero", async () => { - await expect( - ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address]), - ).to.be.revertedWithCustomError(vault, "ZeroAddress"); - }); - - it("Reverts if the treasury address is zero", async () => { - await expect(ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress])).to.be.revertedWithCustomError( - vault, - "ZeroAddress", - ); - }); - - it("Sets initial properties", async () => { - expect(await vault.LIDO()).to.equal(lidoAddress, "Lido address"); - expect(await vault.TREASURY()).to.equal(treasury.address, "Treasury address"); - }); - - it("Petrifies the implementation", async () => { - expect(await impl.getContractVersion()).to.equal(PETRIFIED_VERSION); - }); - - it("Returns 0 as the initial contract version", async () => { - expect(await vault.getContractVersion()).to.equal(0n); - }); - }); - - context("initialize", () => { - it("Should revert if the contract is already initialized", async () => { - await vault.initialize(owner); - - await expect(vault.initialize(owner)) - .to.be.revertedWithCustomError(vault, "UnexpectedContractVersion") - .withArgs(2, 0); - }); - - it("Initializes the contract", async () => { - await expect(vault.initialize(owner)).to.emit(vault, "ContractVersionSet").withArgs(2); - }); - - it("Should revert if admin address is zero", async () => { - await expect(vault.initialize(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); - }); - - it("Should set admin role during initialization", async () => { - const adminRole = await vault.DEFAULT_ADMIN_ROLE(); - expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); - expect(await vault.hasRole(adminRole, owner)).to.equal(false); - - await vault.initialize(owner); - - expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); - expect(await vault.hasRole(adminRole, owner)).to.equal(true); - expect(await vault.hasRole(adminRole, stranger)).to.equal(false); - }); - }); - - context("finalizeUpgrade_v2()", () => { - it("Should revert with UnexpectedContractVersion error when called on implementation", async () => { - await expect(impl.finalizeUpgrade_v2(owner)) - .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") - .withArgs(MAX_UINT256, 1); - }); - - it("Should revert with UnexpectedContractVersion error when called on deployed from scratch WithdrawalVaultV2", async () => { - await vault.initialize(owner); - - await expect(vault.finalizeUpgrade_v2(owner)) - .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") - .withArgs(2, 1); - }); - - context("Simulate upgrade from v1", () => { - beforeEach(async () => { - await vault.harness__initializeContractVersionTo(1); - }); - - it("Should revert if admin address is zero", async () => { - await expect(vault.finalizeUpgrade_v2(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); - }); - - it("Should set correct contract version", async () => { - expect(await vault.getContractVersion()).to.equal(1); - await vault.finalizeUpgrade_v2(owner); - expect(await vault.getContractVersion()).to.be.equal(2); - }); - - it("Should set admin role during finalization", async () => { - const adminRole = await vault.DEFAULT_ADMIN_ROLE(); - expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); - expect(await vault.hasRole(adminRole, owner)).to.equal(false); - - await vault.finalizeUpgrade_v2(owner); - - expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); - expect(await vault.hasRole(adminRole, owner)).to.equal(true); - expect(await vault.hasRole(adminRole, stranger)).to.equal(false); - }); - }); - }); - - context("Access control", () => { - it("Returns ACL roles", async () => { - expect(await vault.ADD_FULL_WITHDRAWAL_REQUEST_ROLE()).to.equal(ADD_FULL_WITHDRAWAL_REQUEST_ROLE); - }); - - it("Sets up roles", async () => { - await vault.initialize(owner); - - expect(await vault.getRoleMemberCount(ADD_FULL_WITHDRAWAL_REQUEST_ROLE)).to.equal(0); - expect(await vault.hasRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(false); - - await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); - - expect(await vault.getRoleMemberCount(ADD_FULL_WITHDRAWAL_REQUEST_ROLE)).to.equal(1); - expect(await vault.hasRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(true); - }); - }); - - context("withdrawWithdrawals", () => { - beforeEach(async () => await vault.initialize(owner)); - - it("Reverts if the caller is not Lido", async () => { - await expect(vault.connect(stranger).withdrawWithdrawals(0)).to.be.revertedWithCustomError(vault, "NotLido"); - }); - - it("Reverts if amount is 0", async () => { - await expect(lido.mock_withdrawFromVault(vaultAddress, 0)).to.be.revertedWithCustomError(vault, "ZeroAmount"); - }); - - it("Reverts if not enough funds are available", async () => { - await expect(lido.mock_withdrawFromVault(vaultAddress, 1)) - .to.be.revertedWithCustomError(vault, "NotEnoughEther") - .withArgs(1, 0); - }); - - it("Withdraws the requested amount", async () => { - await setBalance(vaultAddress, 10); - - await expect(lido.mock_withdrawFromVault(vaultAddress, 1)).to.emit(lido, "WithdrawalsReceived").withArgs(1); - }); - }); - - context("recoverERC20", () => { - let token: ERC20__Harness; - let tokenAddress: string; - - before(async () => { - token = await ethers.deployContract("ERC20__Harness", ["Test Token", "TT"]); - - tokenAddress = await token.getAddress(); - }); - - it("Reverts if the token is not a contract", async () => { - await expect(vault.recoverERC20(ZeroAddress, 1)).to.be.revertedWith("Address: call to non-contract"); - }); - - it("Reverts if the recovered amount is 0", async () => { - await expect(vault.recoverERC20(ZeroAddress, 0)).to.be.revertedWithCustomError(vault, "ZeroAmount"); - }); - - it("Transfers the requested amount", async () => { - await token.mint(vaultAddress, 10); - - expect(await token.balanceOf(vaultAddress)).to.equal(10); - expect(await token.balanceOf(treasury.address)).to.equal(0); - - await expect(vault.recoverERC20(tokenAddress, 1)) - .to.emit(vault, "ERC20Recovered") - .withArgs(owner, tokenAddress, 1); - - expect(await token.balanceOf(vaultAddress)).to.equal(9); - expect(await token.balanceOf(treasury.address)).to.equal(1); - }); - }); - - context("recoverERC721", () => { - let token: ERC721__Harness; - let tokenAddress: string; - - before(async () => { - token = await ethers.deployContract("ERC721__Harness", ["Test NFT", "tNFT"]); - - tokenAddress = await token.getAddress(); - }); - - it("Reverts if the token is not a contract", async () => { - await expect(vault.recoverERC721(ZeroAddress, 0)).to.be.reverted; - }); - - it("Transfers the requested token id", async () => { - await token.mint(vaultAddress, 1); - - expect(await token.ownerOf(1)).to.equal(vaultAddress); - expect(await token.ownerOf(1)).to.not.equal(treasury.address); - - await expect(vault.recoverERC721(tokenAddress, 1)) - .to.emit(vault, "ERC721Recovered") - .withArgs(owner, tokenAddress, 1); - - expect(await token.ownerOf(1)).to.equal(treasury.address); - }); - }); - - context("get triggerable withdrawal request fee", () => { - it("Should get fee from the EIP 7002 contract", async function () { - await withdrawalsPredeployed.mock__setFee(333n); - expect( - (await vault.getWithdrawalRequestFee()) == 333n, - "withdrawal request should use fee from the EIP 7002 contract", - ); - }); - - it("Should revert if fee read fails", async function () { - await withdrawalsPredeployed.mock__setFailOnGetFee(true); - await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); - }); - - ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { - it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); - - await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); - }); - }); - }); - - async function getFee(): Promise { - const fee = await vault.getWithdrawalRequestFee(); - - return ethers.parseUnits(fee.toString(), "wei"); - } - - async function getWithdrawalCredentialsContractBalance(): Promise { - const contractAddress = await vault.getAddress(); - return await ethers.provider.getBalance(contractAddress); - } - - async function getWithdrawalsPredeployedContractBalance(): Promise { - const contractAddress = await withdrawalsPredeployed.getAddress(); - return await ethers.provider.getBalance(contractAddress); - } - - context("add triggerable withdrawal requests", () => { - beforeEach(async () => { - await vault.initialize(owner); - await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); - await vault.connect(owner).grantRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); - }); - - it("Should revert if the caller is not Validator Exit Bus", async () => { - await expect(vault.connect(stranger).addFullWithdrawalRequests("0x1234")).to.be.revertedWithOZAccessControlError( - stranger.address, - ADD_FULL_WITHDRAWAL_REQUEST_ROLE, - ); - - await expect( - vault.connect(stranger).addPartialWithdrawalRequests("0x1234", [1n]), - ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE); - }); - - it("Should revert if empty arrays are provided", async function () { - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests("0x", { value: 1n }), - ).to.be.revertedWithCustomError(vault, "NoWithdrawalRequests"); - - await expect( - vault.connect(validatorsExitBus).addPartialWithdrawalRequests("0x", [], { value: 1n }), - ).to.be.revertedWithCustomError(vault, "NoWithdrawalRequests"); - }); - - it("Should revert if array lengths do not match", async function () { - const requestCount = 2; - const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); - const amounts = [1n]; - - const totalWithdrawalFee = (await getFee()) * BigInt(requestCount); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, amounts, { value: totalWithdrawalFee }), - ) - .to.be.revertedWithCustomError(vault, "MismatchedArrayLengths") - .withArgs(requestCount, amounts.length); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, [], { value: totalWithdrawalFee }), - ) - .to.be.revertedWithCustomError(vault, "MismatchedArrayLengths") - .withArgs(requestCount, 0); - }); - - it("Should revert when a full withdrawal amount is included in 'addPartialWithdrawalRequests'", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(2); - const amounts = [1n, 0n]; // Partial and Full withdrawal - const totalWithdrawalFee = (await getFee()) * BigInt(pubkeysHexString.length); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, amounts, { value: totalWithdrawalFee }), - ).to.be.revertedWithCustomError(vault, "PartialWithdrawalRequired"); - }); - - it("Should revert if not enough fee is sent", async function () { - const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(1); - - await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei - - // 1. Should revert if no fee is sent - await expect(vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString)) - .to.be.revertedWithCustomError(vault, "InsufficientFee") - .withArgs(0, 3n); - - await expect( - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts), - ) - .to.be.revertedWithCustomError(vault, "InsufficientFee") - .withArgs(0, 3n); - - // 2. Should revert if fee is less than required - const insufficientFee = 2n; - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: insufficientFee }), - ) - .to.be.revertedWithCustomError(vault, "InsufficientFee") - .withArgs(2n, 3n); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: insufficientFee }), - ) - .to.be.revertedWithCustomError(vault, "InsufficientFee") - .withArgs(2n, 3n); - }); - - it("Should revert if pubkey is not 48 bytes", async function () { - // Invalid pubkey (only 2 bytes) - const invalidPubkeyHexString = "0x1234"; - - const fee = await getFee(); - - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(invalidPubkeyHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); - - await expect( - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(invalidPubkeyHexString, [1n], { value: fee }), - ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); - }); - - it("Should revert if last pubkey not 48 bytes", async function () { - const validPubey = - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; - const invalidPubkey = "1234"; - const pubkeysHexString = `0x${validPubey}${invalidPubkey}`; - - const fee = await getFee(); - - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); - - await expect( - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, [1n, 2n], { value: fee }), - ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); - }); - - it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(1); - const fee = await getFee(); - - // Set mock to fail on add - await withdrawalsPredeployed.mock__setFailOnAddRequest(true); - - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalRequestAdditionFailed"); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalRequestAdditionFailed"); - }); - - it("Should revert when fee read fails", async function () { - await withdrawalsPredeployed.mock__setFailOnGetFee(true); - - const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(2); - const fee = 10n; - - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalFeeReadFailed"); - }); - - ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { - it(`Should revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); - - const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(2); - const fee = 10n; - - await expect( - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); - - await expect( - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: fee }), - ).to.be.revertedWithCustomError(vault, "WithdrawalFeeInvalidData"); - }); - }); - - it("should revert if refund failed", async function () { - const refundFailureTester: RefundFailureTester = await ethers.deployContract("RefundFailureTester", [ - vaultAddress, - ]); - const refundFailureTesterAddress = await refundFailureTester.getAddress(); - - await vault.connect(owner).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, refundFailureTesterAddress); - await vault.connect(owner).grantRole(ADD_PARTIAL_WITHDRAWAL_REQUEST_ROLE, refundFailureTesterAddress); - - const requestCount = 3; - const { pubkeysHexString, partialWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(fee); - const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei - - await expect( - refundFailureTester - .connect(stranger) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + 1n }), - ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); - - await expect( - refundFailureTester.connect(stranger).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee + 1n, - }), - ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); - - await expect( - refundFailureTester - .connect(stranger) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + ethers.parseEther("1") }), - ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); - - await expect( - refundFailureTester.connect(stranger).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee + ethers.parseEther("1"), - }), - ).to.be.revertedWithCustomError(vault, "ExcessFeeRefundFailed"); - }); - - it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(3n); - const expectedTotalWithdrawalFee = 9n; - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee, - }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - // Check extremely high fee - const highFee = ethers.parseEther("10"); - await withdrawalsPredeployed.mock__setFee(highFee); - const expectedLargeTotalWithdrawalFee = ethers.parseEther("30"); - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedLargeTotalWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - highFee, - ); - - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedLargeTotalWithdrawalFee, - }), - pubkeys, - partialWithdrawalAmounts, - highFee, - ); - }); - - it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(fee); - const withdrawalFee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei - - await testEIP7002Mock( - () => vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: withdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: withdrawalFee }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - // Check when the provided fee extremely exceeds the required amount - const largeWithdrawalFee = ethers.parseEther("10"); - - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addFullWithdrawalRequests(pubkeysHexString, { value: largeWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: largeWithdrawalFee }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - }); - - it("Should not affect contract balance", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(fee); - const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei - - const initialBalance = await getWithdrawalCredentialsContractBalance(); - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee, - }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - - const excessTotalWithdrawalFee = 9n + 1n; // 3 requests * 3 gwei (fee) + 1 gwei (extra fee) = 10 gwei - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: excessTotalWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: excessTotalWithdrawalFee, - }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - }); - - it("Should refund excess fee", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(fee); - const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei - const excessFee = 1n; - - let vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - - const { receipt: fullWithdrawalReceipt } = await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + excessFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( - vebInitialBalance - expectedTotalWithdrawalFee - fullWithdrawalReceipt.gasUsed * fullWithdrawalReceipt.gasPrice, - ); - - vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - - const { receipt: partialWithdrawalReceipt } = await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee + excessFee, - }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( - vebInitialBalance - - expectedTotalWithdrawalFee - - partialWithdrawalReceipt.gasUsed * partialWithdrawalReceipt.gasPrice, - ); - }); - - it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(3n); - const expectedTotalWithdrawalFee = 9n; - const excessTotalWithdrawalFee = 9n + 1n; - - let initialBalance = await getWithdrawalsPredeployedContractBalance(); - - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); - - initialBalance = await getWithdrawalsPredeployedContractBalance(); - - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee, - }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); - - initialBalance = await getWithdrawalsPredeployedContractBalance(); - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: excessTotalWithdrawalFee }), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); - - initialBalance = await getWithdrawalsPredeployedContractBalance(); - await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: excessTotalWithdrawalFee, - }), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); - }); - - it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { - const requestCount = 16; - const { pubkeysHexString, pubkeys, partialWithdrawalAmounts, fullWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - const totalWithdrawalFee = 333n; - - const testEncoding = async ( - tx: ContractTransactionResponse, - expectedPubkeys: string[], - expectedAmounts: bigint[], - ) => { - const receipt = await tx.wait(); - - const events = findEIP7002MockEvents(receipt!); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - const encodedRequest = events[i].args[0]; - // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters - expect(encodedRequest.length).to.equal(114); - - expect(encodedRequest.slice(0, 2)).to.equal("0x"); - expect(encodedRequest.slice(2, 98)).to.equal(expectedPubkeys[i]); - expect(encodedRequest.slice(98, 114)).to.equal(expectedAmounts[i].toString(16).padStart(16, "0")); - } - }; - - const txFullWithdrawal = await vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: totalWithdrawalFee }); - - await testEncoding(txFullWithdrawal, pubkeys, fullWithdrawalAmounts); - - const txPartialWithdrawal = await vault - .connect(validatorsExitBus) - .addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { value: totalWithdrawalFee }); - - await testEncoding(txPartialWithdrawal, pubkeys, partialWithdrawalAmounts); - }); - - const testCasesForWithdrawalRequests = [ - { requestCount: 1, extraFee: 0n }, - { requestCount: 1, extraFee: 100n }, - { requestCount: 1, extraFee: 100_000_000_000n }, - { requestCount: 3, extraFee: 0n }, - { requestCount: 3, extraFee: 1n }, - { requestCount: 7, extraFee: 3n }, - { requestCount: 10, extraFee: 0n }, - { requestCount: 10, extraFee: 100_000_000_000n }, - { requestCount: 100, extraFee: 0n }, - ]; - - testCasesForWithdrawalRequests.forEach(({ requestCount, extraFee }) => { - it(`Should successfully add ${requestCount} requests with extra fee ${extraFee}`, async () => { - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); - - const initialBalance = await getWithdrawalCredentialsContractBalance(); - let vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - - const { receipt: receiptFullWithdrawal } = await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addFullWithdrawalRequests(pubkeysHexString, { value: expectedTotalWithdrawalFee + extraFee }), - pubkeys, - fullWithdrawalAmounts, - expectedFee, - ); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( - vebInitialBalance - - expectedTotalWithdrawalFee - - receiptFullWithdrawal.gasUsed * receiptFullWithdrawal.gasPrice, - ); - - vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); - const { receipt: receiptPartialWithdrawal } = await testEIP7002Mock( - () => - vault.connect(validatorsExitBus).addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, { - value: expectedTotalWithdrawalFee + extraFee, - }), - pubkeys, - partialWithdrawalAmounts, - expectedFee, - ); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( - vebInitialBalance - - expectedTotalWithdrawalFee - - receiptPartialWithdrawal.gasUsed * receiptPartialWithdrawal.gasPrice, - ); - }); - }); - }); -}); diff --git a/test/common/lib/triggerableWithdrawals/eip7002Mock.ts b/test/0.8.9/withdrawalVault/eip7002Mock.ts similarity index 100% rename from test/common/lib/triggerableWithdrawals/eip7002Mock.ts rename to test/0.8.9/withdrawalVault/eip7002Mock.ts diff --git a/test/common/lib/triggerableWithdrawals/utils.ts b/test/0.8.9/withdrawalVault/utils.ts similarity index 100% rename from test/common/lib/triggerableWithdrawals/utils.ts rename to test/0.8.9/withdrawalVault/utils.ts diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts new file mode 100644 index 0000000000..11904bc62f --- /dev/null +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -0,0 +1,589 @@ +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; + +import { + EIP7002WithdrawalRequest__Mock, + ERC20__Harness, + ERC721__Harness, + Lido__MockForWithdrawalVault, + WithdrawalVault__Harness, +} from "typechain-types"; + +import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, proxify, streccak } from "lib"; + +import { Snapshot } from "test/suite"; + +import { findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; +import { generateWithdrawalRequestPayload } from "./utils"; + +const PETRIFIED_VERSION = MAX_UINT256; + +const ADD_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_WITHDRAWAL_REQUEST_ROLE"); + +describe("WithdrawalVault.sol", () => { + let owner: HardhatEthersSigner; + let treasury: HardhatEthersSigner; + let validatorsExitBus: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + let originalState: string; + + let lido: Lido__MockForWithdrawalVault; + let lidoAddress: string; + + let withdrawalsPredeployed: EIP7002WithdrawalRequest__Mock; + + let impl: WithdrawalVault__Harness; + let vault: WithdrawalVault__Harness; + let vaultAddress: string; + + before(async () => { + [owner, treasury, validatorsExitBus, stranger] = await ethers.getSigners(); + + withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); + + expect(await withdrawalsPredeployed.getAddress()).to.equal(EIP7002_ADDRESS); + + lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); + lidoAddress = await lido.getAddress(); + + impl = await ethers.deployContract("WithdrawalVault__Harness", [lidoAddress, treasury.address], owner); + + [vault] = await proxify({ impl, admin: owner }); + vaultAddress = await vault.getAddress(); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("Constructor", () => { + it("Reverts if the Lido address is zero", async () => { + await expect( + ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address]), + ).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Reverts if the treasury address is zero", async () => { + await expect(ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress])).to.be.revertedWithCustomError( + vault, + "ZeroAddress", + ); + }); + + it("Sets initial properties", async () => { + expect(await vault.LIDO()).to.equal(lidoAddress, "Lido address"); + expect(await vault.TREASURY()).to.equal(treasury.address, "Treasury address"); + }); + + it("Petrifies the implementation", async () => { + expect(await impl.getContractVersion()).to.equal(PETRIFIED_VERSION); + }); + + it("Returns 0 as the initial contract version", async () => { + expect(await vault.getContractVersion()).to.equal(0n); + }); + }); + + context("initialize", () => { + it("Should revert if the contract is already initialized", async () => { + await vault.initialize(owner); + + await expect(vault.initialize(owner)) + .to.be.revertedWithCustomError(vault, "UnexpectedContractVersion") + .withArgs(2, 0); + }); + + it("Initializes the contract", async () => { + await expect(vault.initialize(owner)).to.emit(vault, "ContractVersionSet").withArgs(2); + }); + + it("Should revert if admin address is zero", async () => { + await expect(vault.initialize(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Should set admin role during initialization", async () => { + const adminRole = await vault.DEFAULT_ADMIN_ROLE(); + expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); + expect(await vault.hasRole(adminRole, owner)).to.equal(false); + + await vault.initialize(owner); + + expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); + expect(await vault.hasRole(adminRole, owner)).to.equal(true); + expect(await vault.hasRole(adminRole, stranger)).to.equal(false); + }); + }); + + context("finalizeUpgrade_v2()", () => { + it("Should revert with UnexpectedContractVersion error when called on implementation", async () => { + await expect(impl.finalizeUpgrade_v2(owner)) + .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") + .withArgs(MAX_UINT256, 1); + }); + + it("Should revert with UnexpectedContractVersion error when called on deployed from scratch WithdrawalVaultV2", async () => { + await vault.initialize(owner); + + await expect(vault.finalizeUpgrade_v2(owner)) + .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") + .withArgs(2, 1); + }); + + context("Simulate upgrade from v1", () => { + beforeEach(async () => { + await vault.harness__initializeContractVersionTo(1); + }); + + it("Should revert if admin address is zero", async () => { + await expect(vault.finalizeUpgrade_v2(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Should set correct contract version", async () => { + expect(await vault.getContractVersion()).to.equal(1); + await vault.finalizeUpgrade_v2(owner); + expect(await vault.getContractVersion()).to.be.equal(2); + }); + + it("Should set admin role during finalization", async () => { + const adminRole = await vault.DEFAULT_ADMIN_ROLE(); + expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); + expect(await vault.hasRole(adminRole, owner)).to.equal(false); + + await vault.finalizeUpgrade_v2(owner); + + expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); + expect(await vault.hasRole(adminRole, owner)).to.equal(true); + expect(await vault.hasRole(adminRole, stranger)).to.equal(false); + }); + }); + }); + + context("Access control", () => { + it("Returns ACL roles", async () => { + expect(await vault.ADD_WITHDRAWAL_REQUEST_ROLE()).to.equal(ADD_WITHDRAWAL_REQUEST_ROLE); + }); + + it("Sets up roles", async () => { + await vault.initialize(owner); + + expect(await vault.getRoleMemberCount(ADD_WITHDRAWAL_REQUEST_ROLE)).to.equal(0); + expect(await vault.hasRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(false); + + await vault.connect(owner).grantRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + + expect(await vault.getRoleMemberCount(ADD_WITHDRAWAL_REQUEST_ROLE)).to.equal(1); + expect(await vault.hasRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(true); + }); + }); + + context("withdrawWithdrawals", () => { + beforeEach(async () => await vault.initialize(owner)); + + it("Reverts if the caller is not Lido", async () => { + await expect(vault.connect(stranger).withdrawWithdrawals(0)).to.be.revertedWithCustomError(vault, "NotLido"); + }); + + it("Reverts if amount is 0", async () => { + await expect(lido.mock_withdrawFromVault(vaultAddress, 0)).to.be.revertedWithCustomError(vault, "ZeroAmount"); + }); + + it("Reverts if not enough funds are available", async () => { + await expect(lido.mock_withdrawFromVault(vaultAddress, 1)) + .to.be.revertedWithCustomError(vault, "NotEnoughEther") + .withArgs(1, 0); + }); + + it("Withdraws the requested amount", async () => { + await setBalance(vaultAddress, 10); + + await expect(lido.mock_withdrawFromVault(vaultAddress, 1)).to.emit(lido, "WithdrawalsReceived").withArgs(1); + }); + }); + + context("recoverERC20", () => { + let token: ERC20__Harness; + let tokenAddress: string; + + before(async () => { + token = await ethers.deployContract("ERC20__Harness", ["Test Token", "TT"]); + + tokenAddress = await token.getAddress(); + }); + + it("Reverts if the token is not a contract", async () => { + await expect(vault.recoverERC20(ZeroAddress, 1)).to.be.revertedWith("Address: call to non-contract"); + }); + + it("Reverts if the recovered amount is 0", async () => { + await expect(vault.recoverERC20(ZeroAddress, 0)).to.be.revertedWithCustomError(vault, "ZeroAmount"); + }); + + it("Transfers the requested amount", async () => { + await token.mint(vaultAddress, 10); + + expect(await token.balanceOf(vaultAddress)).to.equal(10); + expect(await token.balanceOf(treasury.address)).to.equal(0); + + await expect(vault.recoverERC20(tokenAddress, 1)) + .to.emit(vault, "ERC20Recovered") + .withArgs(owner, tokenAddress, 1); + + expect(await token.balanceOf(vaultAddress)).to.equal(9); + expect(await token.balanceOf(treasury.address)).to.equal(1); + }); + }); + + context("recoverERC721", () => { + let token: ERC721__Harness; + let tokenAddress: string; + + before(async () => { + token = await ethers.deployContract("ERC721__Harness", ["Test NFT", "tNFT"]); + + tokenAddress = await token.getAddress(); + }); + + it("Reverts if the token is not a contract", async () => { + await expect(vault.recoverERC721(ZeroAddress, 0)).to.be.reverted; + }); + + it("Transfers the requested token id", async () => { + await token.mint(vaultAddress, 1); + + expect(await token.ownerOf(1)).to.equal(vaultAddress); + expect(await token.ownerOf(1)).to.not.equal(treasury.address); + + await expect(vault.recoverERC721(tokenAddress, 1)) + .to.emit(vault, "ERC721Recovered") + .withArgs(owner, tokenAddress, 1); + + expect(await token.ownerOf(1)).to.equal(treasury.address); + }); + }); + + context("get triggerable withdrawal request fee", () => { + it("Should get fee from the EIP 7002 contract", async function () { + await withdrawalsPredeployed.mock__setFee(333n); + expect( + (await vault.getWithdrawalRequestFee()) == 333n, + "withdrawal request should use fee from the EIP 7002 contract", + ); + }); + + it("Should revert if fee read fails", async function () { + await withdrawalsPredeployed.mock__setFailOnGetFee(true); + await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "FeeReadFailed"); + }); + + ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { + it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); + + await expect(vault.getWithdrawalRequestFee()).to.be.revertedWithCustomError(vault, "FeeInvalidData"); + }); + }); + }); + + async function getFee(): Promise { + const fee = await vault.getWithdrawalRequestFee(); + + return ethers.parseUnits(fee.toString(), "wei"); + } + + async function getWithdrawalCredentialsContractBalance(): Promise { + const contractAddress = await vault.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + + async function getWithdrawalsPredeployedContractBalance(): Promise { + const contractAddress = await withdrawalsPredeployed.getAddress(); + return await ethers.provider.getBalance(contractAddress); + } + + context("add triggerable withdrawal requests", () => { + beforeEach(async () => { + await vault.initialize(owner); + await vault.connect(owner).grantRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + }); + + it("Should revert if the caller is not Validator Exit Bus", async () => { + await expect( + vault.connect(stranger).addWithdrawalRequests("0x1234", [1n]), + ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_WITHDRAWAL_REQUEST_ROLE); + }); + + it("Should revert if empty arrays are provided", async function () { + await expect(vault.connect(validatorsExitBus).addWithdrawalRequests("0x", [], { value: 1n })) + .to.be.revertedWithCustomError(vault, "ZeroArgument") + .withArgs("pubkeys"); + }); + + it("Should revert if array lengths do not match", async function () { + const requestCount = 2; + const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); + const amounts = [1n]; + + const totalWithdrawalFee = (await getFee()) * BigInt(requestCount); + + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, amounts, { value: totalWithdrawalFee }), + ) + .to.be.revertedWithCustomError(vault, "ArraysLengthMismatch") + .withArgs(requestCount, amounts.length); + + await expect( + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, [], { value: totalWithdrawalFee }), + ) + .to.be.revertedWithCustomError(vault, "ArraysLengthMismatch") + .withArgs(requestCount, 0); + }); + + it("Should revert if not enough fee is sent", async function () { + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(1); + + await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei + + // 1. Should revert if no fee is sent + await expect(vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts)) + .to.be.revertedWithCustomError(vault, "IncorrectFee") + .withArgs(0, 3n); + + // 2. Should revert if fee is less than required + const insufficientFee = 2n; + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: insufficientFee }), + ) + .to.be.revertedWithCustomError(vault, "IncorrectFee") + .withArgs(2n, 3n); + }); + + it("Should revert if pubkey is not 48 bytes", async function () { + // Invalid pubkey (only 2 bytes) + const invalidPubkeyHexString = "0x1234"; + + const fee = await getFee(); + await expect( + vault.connect(validatorsExitBus).addWithdrawalRequests(invalidPubkeyHexString, [1n], { value: fee }), + ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); + }); + + it("Should revert if last pubkey not 48 bytes", async function () { + const validPubey = + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; + const invalidPubkey = "1234"; + const pubkeysHexString = `0x${validPubey}${invalidPubkey}`; + + const fee = await getFee(); + + await expect( + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, [1n, 2n], { value: fee }), + ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); + }); + + it("Should revert if addition fails at the withdrawal request contract", async function () { + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(1); + const fee = await getFee(); + + // Set mock to fail on add + await withdrawalsPredeployed.mock__setFailOnAddRequest(true); + + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: fee }), + ).to.be.revertedWithCustomError(vault, "RequestAdditionFailed"); + }); + + it("Should revert when fee read fails", async function () { + await withdrawalsPredeployed.mock__setFailOnGetFee(true); + + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const fee = 10n; + + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: fee }), + ).to.be.revertedWithCustomError(vault, "FeeReadFailed"); + }); + + it("Should revert when the provided fee exceeds the required amount", async function () { + const requestCount = 3; + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.mock__setFee(fee); + const withdrawalFee = 9n + 1n; // 3 request * 3 gwei (fee) + 1 gwei (extra fee)= 10 gwei + + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: withdrawalFee }), + ) + .to.be.revertedWithCustomError(vault, "IncorrectFee") + .withArgs(10n, 9n); + }); + + ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { + it(`Should revert if unexpected fee value ${unexpectedFee} is returned`, async function () { + await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); + + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const fee = 10n; + + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: fee }), + ).to.be.revertedWithCustomError(vault, "FeeInvalidData"); + }); + }); + + it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { + const requestCount = 3; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.mock__setFee(3n); + const expectedTotalWithdrawalFee = 9n; + + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + mixedWithdrawalAmounts, + fee, + ); + + // Check extremely high fee + const highFee = ethers.parseEther("10"); + await withdrawalsPredeployed.mock__setFee(highFee); + const expectedLargeTotalWithdrawalFee = ethers.parseEther("30"); + + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + value: expectedLargeTotalWithdrawalFee, + }), + pubkeys, + mixedWithdrawalAmounts, + highFee, + ); + }); + + it("Should not affect contract balance", async function () { + const requestCount = 3; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.mock__setFee(fee); + const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei + + const initialBalance = await getWithdrawalCredentialsContractBalance(); + + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + mixedWithdrawalAmounts, + fee, + ); + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + }); + + it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { + const requestCount = 3; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.mock__setFee(3n); + const expectedTotalWithdrawalFee = 9n; + + const initialBalance = await getWithdrawalsPredeployedContractBalance(); + await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + mixedWithdrawalAmounts, + fee, + ); + + expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); + }); + + it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { + const requestCount = 16; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const tx = await vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: 16n }); + + const receipt = await tx.wait(); + + const events = findEIP7002MockEvents(receipt!); + expect(events.length).to.equal(requestCount); + + for (let i = 0; i < requestCount; i++) { + const encodedRequest = events[i].args[0]; + // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters + expect(encodedRequest.length).to.equal(114); + + expect(encodedRequest.slice(0, 2)).to.equal("0x"); + expect(encodedRequest.slice(2, 98)).to.equal(pubkeys[i]); + expect(encodedRequest.slice(98, 114)).to.equal(mixedWithdrawalAmounts[i].toString(16).padStart(16, "0")); + } + }); + + const testCasesForWithdrawalRequests = [ + { requestCount: 1 }, + { requestCount: 3 }, + { requestCount: 7 }, + { requestCount: 10 }, + { requestCount: 100 }, + ]; + + testCasesForWithdrawalRequests.forEach(({ requestCount }) => { + it(`Should successfully add ${requestCount} requests`, async () => { + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); + + const initialBalance = await getWithdrawalCredentialsContractBalance(); + const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + + const { receipt: receiptPartialWithdrawal } = await testEIP7002Mock( + () => + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + pubkeys, + mixedWithdrawalAmounts, + expectedFee, + ); + + expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); + expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( + vebInitialBalance - + expectedTotalWithdrawalFee - + receiptPartialWithdrawal.gasUsed * receiptPartialWithdrawal.gasPrice, + ); + }); + }); + }); +}); diff --git a/test/common/contracts/TriggerableWithdrawals__Harness.sol b/test/common/contracts/TriggerableWithdrawals__Harness.sol deleted file mode 100644 index 74b2bb9d1b..0000000000 --- a/test/common/contracts/TriggerableWithdrawals__Harness.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -// for testing purposes only - -pragma solidity 0.8.9; - -import {TriggerableWithdrawals} from "contracts/common/lib/TriggerableWithdrawals.sol"; - -/** - * @notice This is a harness of TriggerableWithdrawals library. - */ -contract TriggerableWithdrawals__Harness { - function addFullWithdrawalRequests(bytes calldata pubkeys, uint256 feePerRequest) external { - TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, feePerRequest); - } - - function addPartialWithdrawalRequests( - bytes calldata pubkeys, - uint64[] calldata amounts, - uint256 feePerRequest - ) external { - TriggerableWithdrawals.addPartialWithdrawalRequests(pubkeys, amounts, feePerRequest); - } - - function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts, uint256 feePerRequest) external { - TriggerableWithdrawals.addWithdrawalRequests(pubkeys, amounts, feePerRequest); - } - - function getWithdrawalRequestFee() external view returns (uint256) { - return TriggerableWithdrawals.getWithdrawalRequestFee(); - } - - function getWithdrawalsContractAddress() public pure returns (address) { - return TriggerableWithdrawals.WITHDRAWAL_REQUEST; - } - - function deposit() external payable {} -} diff --git a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts b/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts deleted file mode 100644 index 86b51a8ba7..0000000000 --- a/test/common/lib/triggerableWithdrawals/triggerableWithdrawals.test.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { expect } from "chai"; -import { ContractTransactionResponse } from "ethers"; -import { ethers } from "hardhat"; - -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; - -import { EIP7002WithdrawalRequest__Mock, TriggerableWithdrawals__Harness } from "typechain-types"; - -import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS } from "lib"; - -import { Snapshot } from "test/suite"; - -import { findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; -import { generateWithdrawalRequestPayload } from "./utils"; - -const EMPTY_PUBKEYS = "0x"; - -describe("TriggerableWithdrawals.sol", () => { - let actor: HardhatEthersSigner; - - let withdrawalsPredeployed: EIP7002WithdrawalRequest__Mock; - let triggerableWithdrawals: TriggerableWithdrawals__Harness; - - let originalState: string; - - async function getWithdrawalCredentialsContractBalance(): Promise { - const contractAddress = await triggerableWithdrawals.getAddress(); - return await ethers.provider.getBalance(contractAddress); - } - - async function getWithdrawalsPredeployedContractBalance(): Promise { - const contractAddress = await withdrawalsPredeployed.getAddress(); - return await ethers.provider.getBalance(contractAddress); - } - - const MAX_UINT64 = (1n << 64n) - 1n; - - before(async () => { - [actor] = await ethers.getSigners(); - - withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); - triggerableWithdrawals = await ethers.deployContract("TriggerableWithdrawals__Harness"); - - expect(await withdrawalsPredeployed.getAddress()).to.equal(EIP7002_ADDRESS); - - await triggerableWithdrawals.connect(actor).deposit({ value: ethers.parseEther("1") }); - }); - - beforeEach(async () => (originalState = await Snapshot.take())); - - afterEach(async () => await Snapshot.restore(originalState)); - - async function getFee(): Promise { - return await triggerableWithdrawals.getWithdrawalRequestFee(); - } - - context("eip 7002 contract", () => { - it("Should return the address of the EIP 7002 contract", async function () { - expect(await triggerableWithdrawals.getWithdrawalsContractAddress()).to.equal(EIP7002_ADDRESS); - }); - }); - - context("get triggerable withdrawal request fee", () => { - it("Should get fee from the EIP 7002 contract", async function () { - await withdrawalsPredeployed.mock__setFee(333n); - expect( - (await triggerableWithdrawals.getWithdrawalRequestFee()) == 333n, - "withdrawal request should use fee from the EIP 7002 contract", - ); - }); - - it("Should revert if fee read fails", async function () { - await withdrawalsPredeployed.mock__setFailOnGetFee(true); - await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( - triggerableWithdrawals, - "WithdrawalFeeReadFailed", - ); - }); - - ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { - it(`Shoud revert if unexpected fee value ${unexpectedFee} is returned`, async function () { - await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); - - await expect(triggerableWithdrawals.getWithdrawalRequestFee()).to.be.revertedWithCustomError( - triggerableWithdrawals, - "WithdrawalFeeInvalidData", - ); - }); - }); - }); - - context("add triggerable withdrawal requests", () => { - it("Should revert if empty arrays are provided", async function () { - await expect(triggerableWithdrawals.addFullWithdrawalRequests(EMPTY_PUBKEYS, 1n)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "NoWithdrawalRequests", - ); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(EMPTY_PUBKEYS, [], 1n), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "NoWithdrawalRequests"); - - await expect(triggerableWithdrawals.addWithdrawalRequests(EMPTY_PUBKEYS, [], 1n)).to.be.revertedWithCustomError( - triggerableWithdrawals, - "NoWithdrawalRequests", - ); - }); - - it("Should revert if array lengths do not match", async function () { - const requestCount = 2; - const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); - const amounts = [1n]; - - const fee = await getFee(); - - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(requestCount, amounts.length); - - await expect(triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, [], fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(requestCount, 0); - - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(requestCount, amounts.length); - - await expect(triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, [], fee)) - .to.be.revertedWithCustomError(triggerableWithdrawals, "MismatchedArrayLengths") - .withArgs(requestCount, 0); - }); - - it("Should revert if not enough fee is sent", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(1); - const amounts = [10n]; - - await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei - - // 2. Should revert if fee is less than required - const insufficientFee = 2n; - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, insufficientFee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, insufficientFee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - - await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, insufficientFee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - }); - - it("Should revert if pubkey is not 48 bytes", async function () { - // Invalid pubkey (only 2 bytes) - const invalidPubkeyHexString = "0x1234"; - const amounts = [10n]; - - const fee = await getFee(); - - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(invalidPubkeyHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(invalidPubkeyHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); - - await expect( - triggerableWithdrawals.addWithdrawalRequests(invalidPubkeyHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); - }); - - it("Should revert if last pubkey not 48 bytes", async function () { - const validPubey = - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; - const invalidPubkey = "1234"; - const pubkeysHexString = `0x${validPubey}${invalidPubkey}`; - - const amounts = [10n]; - - const fee = await getFee(); - - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); - - await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "MalformedPubkeysArray"); - }); - - it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(1); - const amounts = [10n]; - - const fee = await getFee(); - - // Set mock to fail on add - await withdrawalsPredeployed.mock__setFailOnAddRequest(true); - - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - - await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - }); - - it("Should revert when a full withdrawal amount is included in 'addPartialWithdrawalRequests'", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(2); - const amounts = [1n, 0n]; // Partial and Full withdrawal - const fee = await getFee(); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "PartialWithdrawalRequired"); - }); - - it("Should revert when balance is less than total withdrawal fee", async function () { - const keysCount = 2; - const fee = 10n; - const balance = 19n; - - const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(keysCount); - - await withdrawalsPredeployed.mock__setFee(fee); - await setBalance(await triggerableWithdrawals.getAddress(), balance); - - await expect( - triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - - await expect( - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - - await expect( - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - ).to.be.revertedWithCustomError(triggerableWithdrawals, "WithdrawalRequestAdditionFailed"); - }); - - it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - await withdrawalsPredeployed.mock__setFee(fee); - - await testEIP7002Mock( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - pubkeys, - fullWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - pubkeys, - partialWithdrawalAmounts, - fee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - pubkeys, - mixedWithdrawalAmounts, - fee, - ); - - // Check extremely high fee - const highFee = ethers.parseEther("10"); - await withdrawalsPredeployed.mock__setFee(highFee); - - await triggerableWithdrawals.connect(actor).deposit({ value: highFee * BigInt(requestCount) * 3n }); - - await testEIP7002Mock( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, highFee), - pubkeys, - fullWithdrawalAmounts, - highFee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, highFee), - pubkeys, - partialWithdrawalAmounts, - highFee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, highFee), - pubkeys, - mixedWithdrawalAmounts, - highFee, - ); - }); - - it("Should accept withdrawal requests when the provided fee exceeds the required amount", async function () { - const requestCount = 3; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - await withdrawalsPredeployed.mock__setFee(3n); - const excessFee = 4n; - - await testEIP7002Mock( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, excessFee), - pubkeys, - fullWithdrawalAmounts, - excessFee, - ); - - await testEIP7002Mock( - () => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, excessFee), - pubkeys, - partialWithdrawalAmounts, - excessFee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, excessFee), - pubkeys, - mixedWithdrawalAmounts, - excessFee, - ); - - // Check when the provided fee extremely exceeds the required amount - const extremelyHighFee = ethers.parseEther("10"); - await triggerableWithdrawals.connect(actor).deposit({ value: extremelyHighFee * BigInt(requestCount) * 3n }); - - await testEIP7002Mock( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, extremelyHighFee), - pubkeys, - fullWithdrawalAmounts, - extremelyHighFee, - ); - - await testEIP7002Mock( - () => - triggerableWithdrawals.addPartialWithdrawalRequests( - pubkeysHexString, - partialWithdrawalAmounts, - extremelyHighFee, - ), - pubkeys, - partialWithdrawalAmounts, - extremelyHighFee, - ); - - await testEIP7002Mock( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, extremelyHighFee), - pubkeys, - mixedWithdrawalAmounts, - extremelyHighFee, - ); - }); - - it("Should correctly deduct the exact fee amount from the contract balance", async function () { - const requestCount = 3; - const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 4n; - const expectedTotalWithdrawalFee = 12n; // fee * requestCount; - - const testFeeDeduction = async (addRequests: () => Promise) => { - const initialBalance = await getWithdrawalCredentialsContractBalance(); - await addRequests(); - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); - }; - - await testFeeDeduction(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)); - await testFeeDeduction(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - ); - await testFeeDeduction(() => - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - ); - }); - - it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { - const requestCount = 3; - const { pubkeysHexString, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 3n; - const expectedTotalWithdrawalFee = 9n; // fee * requestCount; - - const testFeeTransfer = async (addRequests: () => Promise) => { - const initialBalance = await getWithdrawalsPredeployedContractBalance(); - await addRequests(); - expect(await getWithdrawalsPredeployedContractBalance()).to.equal(initialBalance + expectedTotalWithdrawalFee); - }; - - await testFeeTransfer(() => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee)); - await testFeeTransfer(() => - triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - ); - await testFeeTransfer(() => - triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - ); - }); - - it("Should accept full, partial, and mixed withdrawal requests via 'addWithdrawalRequests' function", async function () { - const { pubkeysHexString, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(3); - const fee = await getFee(); - - await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, fullWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee); - await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee); - }); - - it("Should handle maximum uint64 withdrawal amount in partial withdrawal requests", async function () { - const { pubkeysHexString } = generateWithdrawalRequestPayload(1); - const amounts = [MAX_UINT64]; - - await triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, amounts, 10n); - await triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, amounts, 10n); - }); - - it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { - const requestCount = 16; - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const fee = 333n; - - const testEncoding = async ( - addRequests: () => Promise, - expectedPubKeys: string[], - expectedAmounts: bigint[], - ) => { - const tx = await addRequests(); - const receipt = await tx.wait(); - - const events = findEIP7002MockEvents(receipt!); - expect(events.length).to.equal(requestCount); - - for (let i = 0; i < requestCount; i++) { - const encodedRequest = events[i].args[0]; - // 0x (2 characters) + 48-byte pubkey (96 characters) + 8-byte amount (16 characters) = 114 characters - expect(encodedRequest.length).to.equal(114); - - expect(encodedRequest.slice(0, 2)).to.equal("0x"); - expect(encodedRequest.slice(2, 98)).to.equal(expectedPubKeys[i]); - expect(encodedRequest.slice(98, 114)).to.equal(expectedAmounts[i].toString(16).padStart(16, "0")); - - // double check the amount convertation - expect(BigInt("0x" + encodedRequest.slice(98, 114))).to.equal(expectedAmounts[i]); - } - }; - - await testEncoding( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - pubkeys, - fullWithdrawalAmounts, - ); - await testEncoding( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - pubkeys, - partialWithdrawalAmounts, - ); - await testEncoding( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - pubkeys, - mixedWithdrawalAmounts, - ); - }); - - async function addWithdrawalRequests( - addRequests: () => Promise, - expectedPubkeys: string[], - expectedAmounts: bigint[], - expectedFee: bigint, - expectedTotalWithdrawalFee: bigint, - ) { - const initialBalance = await getWithdrawalCredentialsContractBalance(); - - await testEIP7002Mock(addRequests, expectedPubkeys, expectedAmounts, expectedFee); - - expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance - expectedTotalWithdrawalFee); - } - - const testCasesForWithdrawalRequests = [ - { requestCount: 1, fee: 100n }, - { requestCount: 1, fee: 100_000_000_000n }, - { requestCount: 3, fee: 1n }, - { requestCount: 7, fee: 3n }, - { requestCount: 10, fee: 100_000_000_000n }, - ]; - - testCasesForWithdrawalRequests.forEach(({ requestCount, fee }) => { - it(`Should successfully add ${requestCount} requests with fee ${fee}`, async () => { - const { pubkeysHexString, pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, mixedWithdrawalAmounts } = - generateWithdrawalRequestPayload(requestCount); - - const expectedFee = fee == 0n ? await getFee() : fee; - const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); - - await addWithdrawalRequests( - () => triggerableWithdrawals.addFullWithdrawalRequests(pubkeysHexString, fee), - pubkeys, - fullWithdrawalAmounts, - expectedFee, - expectedTotalWithdrawalFee, - ); - - await addWithdrawalRequests( - () => triggerableWithdrawals.addPartialWithdrawalRequests(pubkeysHexString, partialWithdrawalAmounts, fee), - pubkeys, - partialWithdrawalAmounts, - expectedFee, - expectedTotalWithdrawalFee, - ); - - await addWithdrawalRequests( - () => triggerableWithdrawals.addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, fee), - pubkeys, - mixedWithdrawalAmounts, - expectedFee, - expectedTotalWithdrawalFee, - ); - }); - }); - }); -}); From fa112ca76c27ba5dc4529c265cc103cb98c1b409 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 23 Apr 2025 14:52:25 +0200 Subject: [PATCH 103/405] feat: update NodeOperatorsRegistry initialization to include exit deadline threshold --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 21 ++++++++++++------- .../scratch/deployed-testnet-defaults.json | 6 ++++-- .../0120-initialize-non-aragon-contracts.ts | 2 ++ test/0.4.24/nor/nor.aux.test.ts | 2 +- test/0.4.24/nor/nor.exit.manager.test.ts | 2 +- .../0.4.24/nor/nor.initialize.upgrade.test.ts | 12 +++++------ test/0.4.24/nor/nor.limits.test.ts | 2 +- test/0.4.24/nor/nor.management.flow.test.ts | 2 +- .../nor/nor.rewards.penalties.flow.test.ts | 2 +- test/0.4.24/nor/nor.signing.keys.test.ts | 2 +- test/0.4.24/nor/nor.staking.limit.test.ts | 2 +- 11 files changed, 33 insertions(+), 22 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 46fdad469f..d7e6061ea5 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -237,14 +237,15 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // // METHODS // - function initialize(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) public onlyInit { + function initialize(address _locator, bytes32 _type, uint256 /* _stuckPenaltyDelay */, uint256 _thresholdInSeconds) public onlyInit { // Initializations for v1 --> v2 - _initialize_v2(_locator, _type, _stuckPenaltyDelay); + _initialize_v2(_locator, _type, 0); // Initializations for v2 --> v3 _initialize_v3(); - _initialize_v4(); + // Initializations for v3 --> v4 + _initialize_v4(_thresholdInSeconds); initialized(); } @@ -268,19 +269,25 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.Distributed); } - function _initialize_v4() internal { + function _initialize_v4(uint256 _thresholdInSeconds) internal { _setContractVersion(4); - EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(86400); + EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_thresholdInSeconds); } /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. /// For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L230 - // function finalizeUpgrade_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) external + /// function finalizeUpgrade_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) external /// @notice A function to finalize upgrade to v3 (from v2). Can be called only once. /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L298 - // function finalizeUpgrade_v3() external + /// function finalizeUpgrade_v3() external + + function finalizeUpgrade_v4(uint256 _thresholdInSeconds) external { + require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); + _checkContractVersion(3); + _initialize_v4(_thresholdInSeconds); + } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators /// @param _name Human-readable name diff --git a/scripts/scratch/deployed-testnet-defaults.json b/scripts/scratch/deployed-testnet-defaults.json index 59cab59d30..0a4ead504e 100644 --- a/scripts/scratch/deployed-testnet-defaults.json +++ b/scripts/scratch/deployed-testnet-defaults.json @@ -133,14 +133,16 @@ "deployParameters": { "stakingModuleName": "Curated", "stakingModuleTypeId": "curated-onchain-v1", - "stuckPenaltyDelay": 172800 + "stuckPenaltyDelay": 172800, + "exitDeadlineThresholdInSeconds": 86400 } }, "simpleDvt": { "deployParameters": { "stakingModuleName": "SimpleDVT", "stakingModuleTypeId": "curated-onchain-v1", - "stuckPenaltyDelay": 432000 + "stuckPenaltyDelay": 432000, + "exitDeadlineThresholdInSeconds": 86400 } }, "withdrawalQueueERC721": { diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index bd8eff9eba..e884f8e337 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -51,6 +51,7 @@ export async function main() { lidoLocatorAddress, encodeStakingModuleTypeId(nodeOperatorsRegistryParams.stakingModuleTypeId), nodeOperatorsRegistryParams.stuckPenaltyDelay, + simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, ], { from: deployer }, ); @@ -63,6 +64,7 @@ export async function main() { lidoLocatorAddress, encodeStakingModuleTypeId(simpleDvtRegistryParams.stakingModuleTypeId), simpleDvtRegistryParams.stuckPenaltyDelay, + simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, ], { from: deployer }, ); diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index 9266a7daee..ac6cfc409d 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -119,7 +119,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index e2f3d2713c..72bb351164 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -104,7 +104,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "RewardDistributionStateChanged") .withArgs(RewardDistributionState.Distributed); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 3e2d4442f0..5f2444ce00 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -88,28 +88,28 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { }); it("Reverts if Locator is zero address", async () => { - await expect(nor.initialize(ZeroAddress, moduleType, 43200n)).to.be.reverted; + await expect(nor.initialize(ZeroAddress, moduleType, 43200n, 86400n)).to.be.reverted; }); it("Reverts if stuck penalty delay exceeds MAX_STUCK_PENALTY_DELAY", async () => { const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await expect(nor.initialize(locator, "curated-onchain-v1", MAX_STUCK_PENALTY_DELAY + 1n)); + await expect(nor.initialize(locator, "curated-onchain-v1", MAX_STUCK_PENALTY_DELAY + 1n, 86400n)); }); it("Reverts if was initialized with v1", async () => { const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); await nor.harness__initialize(1n); - await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( + await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY, 86400n)).to.be.revertedWith( "INIT_ALREADY_INITIALIZED", ); }); it("Reverts if already initialized", async () => { const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), MAX_STUCK_PENALTY_DELAY); + await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), MAX_STUCK_PENALTY_DELAY, 86400n); - await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( + await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY, 86400n)).to.be.revertedWith( "INIT_ALREADY_INITIALIZED", ); }); @@ -118,7 +118,7 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { const burnerAddress = await locator.burner(); const latestBlock = BigInt(await time.latestBlock()); - await expect(nor.initialize(locator, moduleType, 86400n)) + await expect(nor.initialize(locator, moduleType, 86400n, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.limits.test.ts b/test/0.4.24/nor/nor.limits.test.ts index 56c97ccd76..1983fd35ea 100644 --- a/test/0.4.24/nor/nor.limits.test.ts +++ b/test/0.4.24/nor/nor.limits.test.ts @@ -122,7 +122,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.management.flow.test.ts b/test/0.4.24/nor/nor.management.flow.test.ts index d7692899f5..7f535b00af 100644 --- a/test/0.4.24/nor/nor.management.flow.test.ts +++ b/test/0.4.24/nor/nor.management.flow.test.ts @@ -135,7 +135,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts index 0bdbcafe42..cd736ffe4f 100644 --- a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts +++ b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts @@ -128,7 +128,7 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.signing.keys.test.ts b/test/0.4.24/nor/nor.signing.keys.test.ts index 5c89d8b150..f1a8ba199f 100644 --- a/test/0.4.24/nor/nor.signing.keys.test.ts +++ b/test/0.4.24/nor/nor.signing.keys.test.ts @@ -149,7 +149,7 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), deployer); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.staking.limit.test.ts b/test/0.4.24/nor/nor.staking.limit.test.ts index 1fc74563d5..e7b14cd93e 100644 --- a/test/0.4.24/nor/nor.staking.limit.test.ts +++ b/test/0.4.24/nor/nor.staking.limit.test.ts @@ -125,7 +125,7 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay)) + await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") From 59eabc419d21bdca9ffc83f83dea6da2563a7a4d Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 23 Apr 2025 15:22:54 +0200 Subject: [PATCH 104/405] refactor: replace stuckPenaltyDelay with exitDeadlineThreshold in NodeOperatorsRegistry --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 16 ++++++++-------- scripts/scratch/deployed-testnet-defaults.json | 2 -- .../0120-initialize-non-aragon-contracts.ts | 2 -- test/0.4.24/nor/nor.aux.test.ts | 4 ++-- test/0.4.24/nor/nor.exit.manager.test.ts | 4 ++-- test/0.4.24/nor/nor.initialize.upgrade.test.ts | 18 ++++++------------ test/0.4.24/nor/nor.limits.test.ts | 4 ++-- test/0.4.24/nor/nor.management.flow.test.ts | 4 ++-- .../nor/nor.rewards.penalties.flow.test.ts | 4 ++-- test/0.4.24/nor/nor.signing.keys.test.ts | 4 ++-- test/0.4.24/nor/nor.staking.limit.test.ts | 4 ++-- 11 files changed, 28 insertions(+), 38 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index d7e6061ea5..00d3f2ccb3 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -237,20 +237,20 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // // METHODS // - function initialize(address _locator, bytes32 _type, uint256 /* _stuckPenaltyDelay */, uint256 _thresholdInSeconds) public onlyInit { + function initialize(address _locator, bytes32 _type, uint256 _exitDeadlineThresholdInSeconds) public onlyInit { // Initializations for v1 --> v2 - _initialize_v2(_locator, _type, 0); + _initialize_v2(_locator, _type); // Initializations for v2 --> v3 _initialize_v3(); // Initializations for v3 --> v4 - _initialize_v4(_thresholdInSeconds); + _initialize_v4(_exitDeadlineThresholdInSeconds); initialized(); } - function _initialize_v2(address _locator, bytes32 _type, uint256 /* _stuckPenaltyDelay */) internal { + function _initialize_v2(address _locator, bytes32 _type) internal { _onlyNonZeroAddress(_locator); LIDO_LOCATOR_POSITION.setStorageAddress(_locator); TYPE_POSITION.setStorageBytes32(_type); @@ -269,9 +269,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.Distributed); } - function _initialize_v4(uint256 _thresholdInSeconds) internal { + function _initialize_v4(uint256 _exitDeadlineThresholdInSeconds) internal { _setContractVersion(4); - EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_thresholdInSeconds); + EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_exitDeadlineThresholdInSeconds); } /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. @@ -283,10 +283,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L298 /// function finalizeUpgrade_v3() external - function finalizeUpgrade_v4(uint256 _thresholdInSeconds) external { + function finalizeUpgrade_v4(uint256 _exitDeadlineThresholdInSeconds) external { require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); _checkContractVersion(3); - _initialize_v4(_thresholdInSeconds); + _initialize_v4(_exitDeadlineThresholdInSeconds); } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators diff --git a/scripts/scratch/deployed-testnet-defaults.json b/scripts/scratch/deployed-testnet-defaults.json index 0a4ead504e..ead6d617e8 100644 --- a/scripts/scratch/deployed-testnet-defaults.json +++ b/scripts/scratch/deployed-testnet-defaults.json @@ -133,7 +133,6 @@ "deployParameters": { "stakingModuleName": "Curated", "stakingModuleTypeId": "curated-onchain-v1", - "stuckPenaltyDelay": 172800, "exitDeadlineThresholdInSeconds": 86400 } }, @@ -141,7 +140,6 @@ "deployParameters": { "stakingModuleName": "SimpleDVT", "stakingModuleTypeId": "curated-onchain-v1", - "stuckPenaltyDelay": 432000, "exitDeadlineThresholdInSeconds": 86400 } }, diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index e884f8e337..2f7fd1e796 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -50,7 +50,6 @@ export async function main() { [ lidoLocatorAddress, encodeStakingModuleTypeId(nodeOperatorsRegistryParams.stakingModuleTypeId), - nodeOperatorsRegistryParams.stuckPenaltyDelay, simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, ], { from: deployer }, @@ -63,7 +62,6 @@ export async function main() { [ lidoLocatorAddress, encodeStakingModuleTypeId(simpleDvtRegistryParams.stakingModuleTypeId), - simpleDvtRegistryParams.stuckPenaltyDelay, simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, ], { from: deployer }, diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index ac6cfc409d..5159daddb5 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -71,7 +71,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const contractVersionV2 = 2n; const contractVersionV3 = 3n; @@ -119,7 +119,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 72bb351164..15edb33ccd 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -56,7 +56,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const testPublicKey = "0x" + "0".repeat(48 * 2); const eligibleToExitInSec = 86400n; // 2 days @@ -104,7 +104,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "RewardDistributionStateChanged") .withArgs(RewardDistributionState.Distributed); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 5f2444ce00..7b0d97913f 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -88,28 +88,22 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { }); it("Reverts if Locator is zero address", async () => { - await expect(nor.initialize(ZeroAddress, moduleType, 43200n, 86400n)).to.be.reverted; - }); - - it("Reverts if stuck penalty delay exceeds MAX_STUCK_PENALTY_DELAY", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await expect(nor.initialize(locator, "curated-onchain-v1", MAX_STUCK_PENALTY_DELAY + 1n, 86400n)); + await expect(nor.initialize(ZeroAddress, moduleType, 86400n)).to.be.reverted; }); it("Reverts if was initialized with v1", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); + const MAX_STUCK_PENALTY_DELAY = await nor.exitDeadlineThreshold(); await nor.harness__initialize(1n); - await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY, 86400n)).to.be.revertedWith( + await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( "INIT_ALREADY_INITIALIZED", ); }); it("Reverts if already initialized", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.MAX_STUCK_PENALTY_DELAY(); - await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), MAX_STUCK_PENALTY_DELAY, 86400n); + await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), 86400n); - await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY, 86400n)).to.be.revertedWith( + await expect(nor.initialize(locator, moduleType, 86400n)).to.be.revertedWith( "INIT_ALREADY_INITIALIZED", ); }); @@ -118,7 +112,7 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { const burnerAddress = await locator.burner(); const latestBlock = BigInt(await time.latestBlock()); - await expect(nor.initialize(locator, moduleType, 86400n, 86400n)) + await expect(nor.initialize(locator, moduleType, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.limits.test.ts b/test/0.4.24/nor/nor.limits.test.ts index 1983fd35ea..052b918f33 100644 --- a/test/0.4.24/nor/nor.limits.test.ts +++ b/test/0.4.24/nor/nor.limits.test.ts @@ -77,7 +77,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const contractVersionV2 = 2n; const contractVersionV3 = 3n; @@ -122,7 +122,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.management.flow.test.ts b/test/0.4.24/nor/nor.management.flow.test.ts index 7f535b00af..ac4d51d28a 100644 --- a/test/0.4.24/nor/nor.management.flow.test.ts +++ b/test/0.4.24/nor/nor.management.flow.test.ts @@ -88,7 +88,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const contractVersion = 2n; before(async () => { @@ -135,7 +135,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts index cd736ffe4f..f1c0868dac 100644 --- a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts +++ b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts @@ -78,7 +78,7 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const contractVersion = 2n; before(async () => { @@ -128,7 +128,7 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.signing.keys.test.ts b/test/0.4.24/nor/nor.signing.keys.test.ts index f1a8ba199f..d2898c4fc6 100644 --- a/test/0.4.24/nor/nor.signing.keys.test.ts +++ b/test/0.4.24/nor/nor.signing.keys.test.ts @@ -98,7 +98,7 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const contractVersion = 2n; const firstNOKeys = new FakeValidatorKeys(5, { kFill: "a", sFill: "b" }); @@ -149,7 +149,7 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), deployer); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.staking.limit.test.ts b/test/0.4.24/nor/nor.staking.limit.test.ts index e7b14cd93e..87f8fedb42 100644 --- a/test/0.4.24/nor/nor.staking.limit.test.ts +++ b/test/0.4.24/nor/nor.staking.limit.test.ts @@ -81,7 +81,7 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { ]; const moduleType = encodeBytes32String("curated-onchain-v1"); - const penaltyDelay = 86400n; + const exitDeadlineThreshold = 86400n; const contractVersion = 2n; before(async () => { @@ -125,7 +125,7 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, penaltyDelay, 86400n)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") From 71b6ed966655f1062d453bae2ce4209af012d33e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 23 Apr 2025 20:30:25 +0400 Subject: [PATCH 105/405] fix: refund receipient added & exit type & extract val requests parsing & tests --- .../0.8.9/interfaces/IValidatorExitBus.sol | 15 +- contracts/0.8.9/lib/ExitLimitUtils.sol | 10 +- contracts/0.8.9/lib/ReportExitLimitUtils.sol | 147 ------- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 388 +++++++++++------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 15 +- ...ator-exit-bus-oracle.triggerExits.test.ts} | 59 ++- ...it-bus-oracle.triggerExitsDirectly.test.ts | 35 +- 7 files changed, 311 insertions(+), 358 deletions(-) delete mode 100644 contracts/0.8.9/lib/ReportExitLimitUtils.sol rename test/0.8.9/oracle/{validator-exit-bus-oracle.triggerExitHashVerify.test.ts => validator-exit-bus-oracle.triggerExits.test.ts} (91%) diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorExitBus.sol index 385da1b3c8..a0b0daae68 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorExitBus.sol @@ -24,9 +24,18 @@ interface IValidatorsExitBus { function emitExitEvents(ExitRequestData calldata request) external; - function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable; - - function triggerExitsDirectly(DirectExitData calldata exitData) external payable returns (uint256); + function triggerExits( + ExitRequestData calldata request, + uint256[] calldata keyIndexes, + address refundRecipient, + uint8 exitType + ) external payable; + + function triggerExitsDirectly( + DirectExitData calldata exitData, + address refundRecipient, + uint8 exitType + ) external payable; function setExitRequestLimit(uint256 exitsDailyLimit, uint256 twExitsDailyLimit) external; diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index 8d9311568a..a4d18cb90b 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -75,6 +75,10 @@ library ExitLimitUtils { uint256 updatedCount = uint256(data.dailyExitCount) + newCount; require(updatedCount <= type(uint96).max, "DAILY_EXIT_COUNT_OVERFLOW"); + if (data.dailyLimit != 0) { + require(updatedCount <= data.dailyLimit, "DAILY_LIMIT_REACHED"); + } + data.dailyExitCount = uint96(updatedCount); return data; @@ -106,12 +110,6 @@ library ExitLimitUtils { data.dailyLimit = uint96(limit); - if (day == data.currentDay && data.dailyExitCount > data.dailyLimit) { - data.dailyExitCount = data.dailyLimit; - } - - // other values doesnt look like we need to set here in other cases - return data; } } diff --git a/contracts/0.8.9/lib/ReportExitLimitUtils.sol b/contracts/0.8.9/lib/ReportExitLimitUtils.sol deleted file mode 100644 index 7a273b84e9..0000000000 --- a/contracts/0.8.9/lib/ReportExitLimitUtils.sol +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.9; - -import {UnstructuredStorage} from "./UnstructuredStorage.sol"; - -// MSB ------------------------------------------------------------------------------> LSB -// 256______________160____________________________128_______________32____________________________ 0 -// |_________________|______________________________|_________________|_____________________________| -// | maxExitRequests | maxExitRequestsGrowthBlocks | prevExitRequests | prevExitRequestsBlockNumber | -// |<--- 96 bits --->|<---------- 32 bits -------->|<--- 96 bits ---->|<----- 32 bits ------------->| -// - -// TODO: maybe we need smaller type for maxExitRequestsLimit -struct ExitRequestLimitData { - uint32 prevExitRequestsBlockNumber; // block number of the previous exit requests - // Remaining portion of the limit available after the previous request. - // Always less than or equal to `maxExitRequestsLimit`. - uint96 prevExitRequestsLimit; - // Number of block to regenerate limit from 0 to maxExitRequestsLimit - uint32 maxExitRequestsLimitGrowthBlocks; - // TODO: maybe use uint16 type - uint96 maxExitRequestsLimit; // maximum exit requests limit value -} - -library ReportExitLimitUtilsStorage { - using UnstructuredStorage for bytes32; - - uint256 internal constant MAX_EXIT_REQUESTS_LIMIT_OFFSET = 160; - uint256 internal constant MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET = 128; - uint256 internal constant PREV_EXIT_REQUESTS_LIMIT_OFFSET = 32; - uint256 internal constant PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET = 0; - - function getStorageExitRequestLimit(bytes32 _position) internal view returns (ExitRequestLimitData memory data) { - uint256 slotValue = _position.getStorageUint256(); - - data.prevExitRequestsBlockNumber = uint32(slotValue >> PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET); - data.prevExitRequestsLimit = uint96(slotValue >> PREV_EXIT_REQUESTS_LIMIT_OFFSET); - data.maxExitRequestsLimitGrowthBlocks = uint32(slotValue >> MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET); - data.maxExitRequestsLimit = uint96(slotValue >> MAX_EXIT_REQUESTS_LIMIT_OFFSET); - } - - function setStorageExitRequestLimit(bytes32 _position, ExitRequestLimitData memory _data) internal { - _position.setStorageUint256( - (uint256(_data.prevExitRequestsBlockNumber) << PREV_EXIT_REQUESTS_BLOCK_NUMBER_OFFSET) | - (uint256(_data.prevExitRequestsLimit) << PREV_EXIT_REQUESTS_LIMIT_OFFSET) | - (uint256(_data.maxExitRequestsLimitGrowthBlocks) << MAX_EXIT_REQUESTS_LIMIT_GROWTH_BLOCKS_OFFSET) | - (uint256(_data.maxExitRequestsLimit) << MAX_EXIT_REQUESTS_LIMIT_OFFSET) - ); - } -} - -library ReportExitLimitUtils { - /** - * @notice Calculate exit requests limit - * @dev using `_constGasMin` to make gas consumption independent of the current block number - */ - function calculateCurrentExitRequestLimit(ExitRequestLimitData memory _data) internal view returns (uint256 limit) { - uint256 exitRequestLimitIncPerBlock; - if (_data.maxExitRequestsLimitGrowthBlocks != 0) { - exitRequestLimitIncPerBlock = _data.maxExitRequestsLimit / _data.maxExitRequestsLimitGrowthBlocks; - } - - uint256 blocksPassed = block.number - _data.prevExitRequestsBlockNumber; - uint256 projectedLimit = _data.prevExitRequestsLimit + blocksPassed * exitRequestLimitIncPerBlock; - - limit = _constGasMin(projectedLimit, _data.maxExitRequestsLimit); - } - - /** - * @notice update exit requests limit repr after exit request - * @dev input `_data` param is mutated and the func returns effectively the same pointer - * @param _data exit request limit struct - * @param _newPrevExitRequestsLimit new value for the `prevExitRequests` field - */ - function updatePrevExitRequestsLimit( - ExitRequestLimitData memory _data, - uint256 _newPrevExitRequestsLimit - ) internal view returns (ExitRequestLimitData memory) { - _data.prevExitRequestsLimit = uint96(_newPrevExitRequestsLimit); - _data.prevExitRequestsBlockNumber = uint32(block.number); - - return _data; - } - - /** - * @notice update exit request limit repr with the desired limits - * @dev input `_data` param is mutated and the func returns effectively the same pointer - * @param _data exit request limit struct - * @param _maxExitRequestsLimit exit request limit max value - * @param _exitRequestsLimitIncreasePerBlock exit request limit increase (restoration) per block - */ - function setExitRequestLimit( - ExitRequestLimitData memory _data, - uint256 _maxExitRequestsLimit, - uint256 _exitRequestsLimitIncreasePerBlock - ) internal view returns (ExitRequestLimitData memory) { - require(_maxExitRequestsLimit != 0, "ZERO_MAX_EXIT_REQUESTS_LIMIT"); - require(_maxExitRequestsLimit <= type(uint96).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); - require(_maxExitRequestsLimit >= _exitRequestsLimitIncreasePerBlock, "TOO_LARGE_LIMIT_INCREASE"); - require( - (_exitRequestsLimitIncreasePerBlock == 0) || - (_maxExitRequestsLimit / _exitRequestsLimitIncreasePerBlock <= type(uint32).max), - "TOO_SMALL_LIMIT_INCREASE" - ); - - if ( - _data.prevExitRequestsBlockNumber == 0 || - _data.maxExitRequestsLimit == 0 || - _maxExitRequestsLimit < _data.prevExitRequestsLimit - ) { - _data.prevExitRequestsLimit = uint96(_maxExitRequestsLimit); - } - _data.maxExitRequestsLimitGrowthBlocks = _exitRequestsLimitIncreasePerBlock != 0 - ? uint32(_maxExitRequestsLimit / _exitRequestsLimitIncreasePerBlock) - : 0; - - _data.maxExitRequestsLimit = uint96(_maxExitRequestsLimit); - - if (_data.prevExitRequestsBlockNumber != 0) { - _data.prevExitRequestsBlockNumber = uint32(block.number); - } - - return _data; - } - - /** - * @notice check if max exit request limit is set. Otherwise there are no limits on exits - */ - function isExitRequestLimitSet(ExitRequestLimitData memory _data) internal pure returns (bool) { - return _data.maxExitRequestsLimit != 0; - } - - /** - * @notice find a minimum of two numbers with a constant gas consumption - * @dev doesn't use branching logic inside - * @param _lhs left hand side value - * @param _rhs right hand side value - */ - function _constGasMin(uint256 _lhs, uint256 _rhs) internal pure returns (uint256 min) { - uint256 lhsIsLess; - assembly { - lhsIsLess := lt(_lhs, _rhs) // lhsIsLess = (_lhs < _rhs) ? 1 : 0 - } - min = (_lhs * lhsIsLess) + (_rhs * (1 - lhsIsLess)); - } -} diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 102aa71bd2..b6a20f4852 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -32,24 +32,61 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa using ExitLimitUtils for ExitRequestLimitData; /// @dev Errors - error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); - error ZeroAddress(); - error InsufficientPayment(uint256 withdrawalFeePerRequest, uint256 requestCount, uint256 msgValue); - error TriggerableWithdrawalRefundFailed(); - error ExitHashWasNotSubmitted(); - error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + /** + * @notice Thrown when an invalid zero value is passed + * @param name Name of the argument that was zero + */ + error ZeroArgument(string name); + + /** + * @notice Thrown when exit request passed to method contain wrong DATA_FORMAT + * @param format code of format, currently only DATA_FORMAT=1 is supported in the contract + */ error UnsupportedRequestsDataFormat(uint256 format); + /** + * @notice Thrown when exit request has wrong length + */ error InvalidRequestsDataLength(); + + /** + * @notice Thrown than module id equal to zero + */ error InvalidRequestsData(); - error RequestsAlreadyDelivered(); - error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); - error TWExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + + /** + * @notice + */ + error InvalidRequestsDataSortOrder(); error InvalidPubkeysArray(); + error NoExitRequestProvided(); - error InvalidRequestsDataSortOrder(); + error ExitHashWasNotSubmitted(); + + error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + error RequestsAlreadyDelivered(); + error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); + + /** + * @notice Thrown when remaining exit requests limit is not enough to cover sender requests + * @param requestsCount Amount of requests that were sent for processing + * @param remainingLimit Amount of requests that still can be processed at current day + */ + error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + + /** + * @notice Thrown when a withdrawal fee insufficient + * @param feeRequired Amount of fee required to cover withdrawal request + * @param passedValue Amount of fee sent to cover withdrawal request + */ + error InsufficientWithdrawalFee(uint256 feeRequired, uint256 passedValue); + + /** + * @notice Thrown when a withdrawal fee refund failed + */ + error TriggerableWithdrawalFeeRefundFailed(); /// @dev Events - event MadeRefund(address sender, uint256 refundValue); + event MadeRefund(address sender, uint256 refundValue); // maybe we dont need it event StoredExitRequestHash(bytes32 exitRequestHash); event ValidatorExitRequest( uint256 indexed stakingModuleId, @@ -58,13 +95,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorPubkey, uint256 timestamp ); - event ExitRequestsLimitSet(uint256 exitRequestsLimit, uint256 twExitRequestsLimit); + event ExitRequestsLimitSet(uint256 exitRequestsLimit, uint256 ExitRequestsLimit); event DirectExitRequest( uint256 indexed stakingModuleId, uint256 indexed nodeOperatorId, bytes validatoPubkey, - uint256 timestamp + uint256 timestamp, + address indexed refundRecipient ); struct RequestStatus { // Total items count in report (by default type(uint32).max, update on first report delivery) @@ -76,6 +114,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa DeliveryHistory[] deliverHistory; } + struct ValidatorData { + uint256 nodeOpId; + uint256 moduleId; + uint256 valIndex; + bytes pubkey; + } + bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); bytes32 public constant DIRECT_EXIT_ROLE = keccak256("DIRECT_EXIT_ROLE"); bytes32 public constant EXIT_REPORT_LIMIT_ROLE = keccak256("EXIT_REPORT_LIMIT_ROLE"); @@ -125,6 +170,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa LOCATOR = ILidoLocator(lidoLocator); } + /// @notice Method for submitting request hash by trusted entities + /// @param exitReportHash Request hash + /// @dev After request was stored anyone can deliver it via emitExitEvents method below function submitReportHash(bytes32 exitReportHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); @@ -164,25 +212,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert RequestsAlreadyDelivered(); } - ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - uint256 requestsToDeliver; - - if (exitRequestLimitData.isExitDailyLimitSet()) { - uint256 day = _getTimestamp() / 1 days; - uint256 limit = exitRequestLimitData.remainingLimit(day); - - if (limit == 0) { - revert ExitRequestsLimit(undeliveredItemsCount, limit); - } - - requestsToDeliver = undeliveredItemsCount > limit ? limit : undeliveredItemsCount; - - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(day, requestsToDeliver) - ); - } else { - requestsToDeliver = undeliveredItemsCount; - } + uint256 requestsToDeliver = _applyExitLimitOrRevert(EXIT_REQUEST_LIMIT_POSITION, undeliveredItemsCount); _processExitRequestsList(request.data, deliveredItemsCount, requestsToDeliver); @@ -192,13 +222,27 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa requestStatus.deliveredItemsCount += requestsToDeliver; } - /// @notice Triggers exits on the EL via the Withdrawal Vault contract after + /// @notice Triggers exits on the EL via the Withdrawal Vault contract + /// @param request Exit request data struct + /// @param keyIndexes Array of indexes of requests in request.data + /// @param refundRecipient Address to return extra fee on TW (eip-7002) exit + /// @param exitType type of request. 0 - non-refundable, 1 - require refund /// @dev This function verifies that the hash of the provided exit request data exists in storage // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. + // Verify that keyIndexes amount fits within the limits function triggerExits( ExitRequestData calldata request, - uint256[] calldata keyIndexes + uint256[] calldata keyIndexes, + address refundRecipient, + uint8 exitType ) external payable whenResumed preservesEthBalance { + if (msg.value == 0) revert ZeroArgument("msg.value"); + + // If the refund recipient is not set, use the sender as the refund recipient + if (refundRecipient == address(0)) { + refundRecipient = msg.sender; + } + bytes calldata data = request.data; RequestStatus storage requestStatus = _storageExitRequestsHashes()[ keccak256(abi.encode(data, request.dataFormat)) @@ -218,96 +262,69 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkContractVersion(requestStatus.contractVersion); - address withdrawalVaultAddr = LOCATOR.withdrawalVault(); - uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee(); + uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); if (msg.value < keyIndexes.length * withdrawalFee) { - revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value); + revert InsufficientWithdrawalFee(keyIndexes.length * withdrawalFee, msg.value); } - ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - - if (exitRequestLimitData.isExitDailyLimitSet()) { - uint256 day = _getTimestamp() / 1 days; - uint256 limit = exitRequestLimitData.remainingLimit(day); - if (keyIndexes.length > limit) { - revert TWExitRequestsLimit(keyIndexes.length, limit); - } - - TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(day, keyIndexes.length) - ); - } - - uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1; + _checkAndUpdateDailyExitLimit(TW_EXIT_REQUEST_LIMIT_POSITION, keyIndexes.length); bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); - // TODO: create library for reading DATA for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); } - if (keyIndexes[i] > lastDeliveredKeyIndex) { - revert KeyWasNotDelivered(keyIndexes[i], lastDeliveredKeyIndex); + if (keyIndexes[i] > (requestStatus.deliveredItemsCount - 1)) { + revert KeyWasNotDelivered(keyIndexes[i], requestStatus.deliveredItemsCount - 1); } - uint256 itemOffset; - uint256 dataWithoutPubkey; - uint256 index = keyIndexes[i]; - - assembly { - // Compute the start of this packed request (item) - itemOffset := add(data.offset, mul(PACKED_REQUEST_LENGTH, index)) - - // Load the first 16 bytes which contain moduleId (24 bits), - // nodeOpId (40 bits), and valIndex (64 bits). - dataWithoutPubkey := shr(128, calldataload(itemOffset)) - } - - // dataWithoutPubkey format (128 bits total): - // MSB <-------------------- 128 bits --------------------> LSB - // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - - uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); - uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - if (moduleId == 0) { - revert InvalidRequestsData(); - } + ValidatorData memory validatorData = getValidatorData(data, keyIndexes[i]); + if (validatorData.moduleId == 0) revert InvalidRequestsData(); + pubkey = validatorData.pubkey; assembly { - let pubkeyCalldataOffset := add(itemOffset, 16) let pubkeyMemPtr := add(pubkey, 32) let dest := add(pubkeys, add(32, mul(PUBLIC_KEY_LENGTH, i))) - - calldatacopy(dest, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) - calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + mstore(dest, mload(pubkeyMemPtr)) + mstore(add(dest, 32), mload(add(pubkeyMemPtr, 32))) } IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( - moduleId, - nodeOpId, + validatorData.moduleId, + validatorData.nodeOpId, pubkey, withdrawalFee, - 0 + exitType ); } - IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( + IWithdrawalVault(LOCATOR.withdrawalVault()).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( pubkeys ); - _refundFee(keyIndexes.length * withdrawalFee); + _refundFee(keyIndexes.length * withdrawalFee, refundRecipient); } + /// @notice Directly emit exit events and request validators through the TW to exit them without delivering hashes and any proving + /// @param exitData Direct exit request data struct + /// @param refundRecipient Address to return extra fee on TW (eip-7002) exit + /// @param exitType type of request. 0 - non-refundable, 1 - require refund + /// @dev Verify that requests amount fits within the limits function triggerExitsDirectly( - DirectExitData calldata exitData - ) external payable whenResumed onlyRole(DIRECT_EXIT_ROLE) preservesEthBalance returns (uint256) { - address withdrawalVault = LOCATOR.withdrawalVault(); - uint256 withdrawalFee = IWithdrawalVault(withdrawalVault).getWithdrawalRequestFee(); + DirectExitData calldata exitData, + address refundRecipient, + uint8 exitType + ) external payable whenResumed onlyRole(DIRECT_EXIT_ROLE) preservesEthBalance { + if (msg.value == 0) revert ZeroArgument("msg.value"); + + // If the refund recipient is not set, use the sender as the refund recipient + if (refundRecipient == address(0)) { + refundRecipient = msg.sender; + } if (exitData.validatorsPubkeys.length == 0) { revert NoExitRequestProvided(); @@ -318,54 +335,41 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; + uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); if (msg.value < withdrawalFee * requestsCount) { - revert InsufficientPayment(withdrawalFee, requestsCount, msg.value); + revert InsufficientWithdrawalFee(withdrawalFee * requestsCount, msg.value); } - ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - uint256 timestamp = _getTimestamp(); - - if (exitRequestLimitData.isExitDailyLimitSet()) { - uint256 day = timestamp / 1 days; - uint256 limit = exitRequestLimitData.remainingLimit(day); - - if (requestsCount > limit) { - revert TWExitRequestsLimit(requestsCount, limit); - } - - TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(day, requestsCount) - ); - } - - bytes calldata data = exitData.validatorsPubkeys; + _checkAndUpdateDailyExitLimit(TW_EXIT_REQUEST_LIMIT_POSITION, requestsCount); for (uint256 i = 0; i < requestsCount; i++) { bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); - assembly { - let offset := add(data.offset, mul(i, PUBLIC_KEY_LENGTH)) - let dest := add(pubkey, 0x20) - calldatacopy(dest, offset, PUBLIC_KEY_LENGTH) - } + pubkey = getPubkey(exitData.validatorsPubkeys, i); IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( exitData.stakingModuleId, exitData.nodeOperatorId, pubkey, withdrawalFee, - 0 + exitType ); - emit DirectExitRequest(exitData.stakingModuleId, exitData.nodeOperatorId, pubkey, timestamp); + emit DirectExitRequest( + exitData.stakingModuleId, + exitData.nodeOperatorId, + pubkey, + _getTimestamp(), + refundRecipient + ); } - IWithdrawalVault(withdrawalVault).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( + IWithdrawalVault(LOCATOR.withdrawalVault()).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( exitData.validatorsPubkeys ); - return _refundFee(requestsCount * withdrawalFee); + _refundFee(requestsCount * withdrawalFee, refundRecipient); } function setExitRequestLimit( @@ -416,39 +420,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); } - uint256 itemOffset; - uint256 dataWithoutPubkey; - - assembly { - // Compute the start of this packed request (item) - itemOffset := add(exitRequests.offset, mul(PACKED_REQUEST_LENGTH, index)) - - // Load the first 16 bytes which contain moduleId (24 bits), - // nodeOpId (40 bits), and valIndex (64 bits). - dataWithoutPubkey := shr(128, calldataload(itemOffset)) - } - - // dataWithoutPubkey format (128 bits total): - // MSB <-------------------- 128 bits --------------------> LSB - // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - - valIndex = uint64(dataWithoutPubkey); - nodeOpId = uint40(dataWithoutPubkey >> 64); - moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - // Allocate a new bytes array in memory for the pubkey - pubkey = new bytes(PUBLIC_KEY_LENGTH); - - assembly { - // Starting offset in calldata for the pubkey part - let pubkeyCalldataOffset := add(itemOffset, 16) - - // Memory location of the 'pubkey' bytes array data - let pubkeyMemPtr := add(pubkey, 32) + ValidatorData memory validatorData = getValidatorData(exitRequests, index); - // Copy the 48 bytes of the pubkey from calldata into memory - calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) - } + valIndex = validatorData.valIndex; + nodeOpId = validatorData.nodeOpId; + moduleId = validatorData.moduleId; + pubkey = validatorData.pubkey; return (pubkey, nodeOpId, moduleId, valIndex); } @@ -484,6 +461,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Internal functions + /** + * @notice + * @dev We have sorting order for both + */ function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { uint256 offset; uint256 offsetPastEnd; @@ -531,14 +512,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _refundFee(uint256 fee) internal returns (uint256) { + function _refundFee(uint256 fee, address recipient) internal returns (uint256) { uint256 refund = msg.value - fee; if (refund > 0) { - (bool success, ) = msg.sender.call{value: refund}(""); + (bool success, ) = recipient.call{value: refund}(""); if (!success) { - revert TriggerableWithdrawalRefundFailed(); + revert TriggerableWithdrawalFeeRefundFailed(); } emit MadeRefund(msg.sender, refund); @@ -547,11 +528,50 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa return refund; } + function _checkAndUpdateDailyExitLimit(bytes32 _exitRequestPosition, uint256 requestsCount) internal { + ExitRequestLimitData memory exitRequestLimitData = _exitRequestPosition.getStorageExitRequestLimit(); + + if (!exitRequestLimitData.isExitDailyLimitSet()) { + return; + } + + uint256 day = _getTimestamp() / 1 days; + uint256 limit = exitRequestLimitData.remainingLimit(day); + + if (requestsCount > limit) { + revert ExitRequestsLimit(requestsCount, limit); + } + + _exitRequestPosition.setStorageExitRequestLimit(exitRequestLimitData.updateRequestsCounter(day, requestsCount)); + } + + function _applyExitLimitOrRevert(bytes32 _exitRequestPosition, uint256 requestsCount) internal returns (uint256) { + ExitRequestLimitData memory exitRequestLimitData = _exitRequestPosition.getStorageExitRequestLimit(); + + if (!exitRequestLimitData.isExitDailyLimitSet()) { + return requestsCount; + } + + uint256 day = _getTimestamp() / 1 days; + uint256 limit = exitRequestLimitData.remainingLimit(day); + + if (limit == 0) { + revert ExitRequestsLimit(requestsCount, limit); + } + + uint256 requestsToDeliver = requestsCount > limit ? limit : requestsCount; + + _exitRequestPosition.setStorageExitRequestLimit( + exitRequestLimitData.updateRequestsCounter(day, requestsToDeliver) + ); + + return requestsToDeliver; + } + function _getTimestamp() internal view virtual returns (uint256) { return block.timestamp; // solhint-disable-line not-rely-on-time } - // this method function _storeExitRequestHash( bytes32 exitRequestHash, uint256 totalItemsCount, @@ -574,6 +594,66 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa emit StoredExitRequestHash(exitRequestHash); } + /// Methods for reading data from tightly packed validator exit requests + /// Format DATA_FORMAT_LIST = 1; + + /** + * @notice Method for reading node operator id, module id and validator index from validator exit request data + * @param exitRequestData Validator exit requests data. DATA_FORMAT = 1 + * @param index index of request in array above + * @return validatorData Validator data including node operator id, module id, validator index + */ + function getValidatorData( + bytes calldata exitRequestData, + uint256 index + ) internal pure returns (ValidatorData memory validatorData) { + uint256 itemOffset; + uint256 dataWithoutPubkey; + + assembly { + // Compute the start of this packed request (item) + itemOffset := add(exitRequestData.offset, mul(PACKED_REQUEST_LENGTH, index)) + + // Load the first 16 bytes which contain moduleId (24 bits), + // nodeOpId (40 bits), and valIndex (64 bits). + dataWithoutPubkey := shr(128, calldataload(itemOffset)) + } + + // dataWithoutPubkey format (128 bits total): + // MSB <-------------------- 128 bits --------------------> LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + + validatorData.valIndex = uint64(dataWithoutPubkey); + validatorData.nodeOpId = uint40(dataWithoutPubkey >> 64); + validatorData.moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); + assembly { + itemOffset := add(exitRequestData.offset, mul(PACKED_REQUEST_LENGTH, index)) + let pubkeyCalldataOffset := add(itemOffset, 16) + let pubkeyMemPtr := add(pubkey, 32) + calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) + } + + validatorData.pubkey = pubkey; + } + + /** + * @notice Method for reading public key value from pubkeys list + * @param pubkeys Concatenated list of pubkeys + * @param index index of pubkey in array above + * @return pubkey Validator public key + */ + function getPubkey(bytes calldata pubkeys, uint256 index) internal pure returns (bytes memory pubkey) { + pubkey = new bytes(PUBLIC_KEY_LENGTH); + + assembly { + let offset := add(pubkeys.offset, mul(index, PUBLIC_KEY_LENGTH)) + let dest := add(pubkey, 0x20) + calldatacopy(dest, offset, PUBLIC_KEY_LENGTH) + } + } + /// Storage helpers function _storageExitRequestsHashes() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { bytes32 position = EXIT_REQUESTS_HASHES_POSITION; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 08f80053e7..3f4f5e62e3 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -242,20 +242,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { .checkExitBusOracleReport(data.requestsCount); // Check VEB common limit - ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - - if (exitRequestLimitData.isExitDailyLimitSet()) { - uint256 day = _getTimestamp() / 1 days; - uint256 limit = exitRequestLimitData.remainingLimit(day); - - if (data.requestsCount > limit) { - revert ExitRequestsLimit(data.requestsCount, limit); - } - - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(day, data.requestsCount) - ); - } + _checkAndUpdateDailyExitLimit(EXIT_REQUEST_LIMIT_POSITION, data.requestsCount); if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts similarity index 91% rename from test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts rename to test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 408d08d4ce..042a440b4f 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitHashVerify.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -2,7 +2,6 @@ import { expect } from "chai"; import { ZeroHash } from "ethers"; import { ethers } from "hardhat"; -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { @@ -31,6 +30,8 @@ const PUBKEYS = [ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ]; +const ZERO_ADDRESS = ethers.ZeroAddress; + describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; @@ -233,6 +234,8 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 2, 3], + ZERO_ADDRESS, + 0, { value: 4 }, ); @@ -260,9 +263,15 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }); it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { - const tx = await oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { - value: 10, - }); + const tx = await oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 3], + ZERO_ADDRESS, + 0, + { + value: 10, + }, + ); const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); @@ -282,17 +291,17 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[2], 1, 0); - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 7); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(admin, 7); }); it("Not enough fee", async () => { await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1], { + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1], ZERO_ADDRESS, 0, { value: 1, }), ) - .to.be.revertedWithCustomError(oracle, "InsufficientPayment") - .withArgs(1, 2, 1); + .to.be.revertedWithCustomError(oracle, "InsufficientWithdrawalFee") + .withArgs(2, 1); }); it("Should trigger withdrawals only for validators that were requested for voluntary exit by trusted entities earlier", async () => { @@ -303,6 +312,8 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { dataFormat: reportFields.dataFormat, }, [0], + ZERO_ADDRESS, + 0, { value: 2 }, ), ).to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted"); @@ -310,7 +321,9 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { it("Requested index out of range", async () => { await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], { value: 2 }), + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], ZERO_ADDRESS, 0, { + value: 2, + }), ) .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") .withArgs(5, 4); @@ -318,11 +331,17 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { - value: 10, - }), + oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 3], + ZERO_ADDRESS, + 0, + { + value: 10, + }, + ), ) - .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") + .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") .withArgs(3, 1); }); @@ -331,9 +350,15 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }); it("Limit regenerated in a day", async () => { - const tx = await oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], { - value: 10, - }); + const tx = await oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 3], + ZERO_ADDRESS, + 0, + { + value: 10, + }, + ); const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); @@ -353,6 +378,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[2], 1, 0); - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 7); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(admin, 7); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts index 78969669aa..122c4f3390 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts @@ -1,7 +1,6 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { @@ -21,6 +20,8 @@ const PUBKEYS = [ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ]; +const ZERO_ADDRESS = ethers.ZeroAddress; + describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; @@ -86,7 +87,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { }; await expect( - oracle.connect(stranger).triggerExitsDirectly(exitData, { + oracle.connect(stranger).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { value: 4, }), ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_ROLE()); @@ -100,31 +101,31 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { it("Not enough fee", async () => { await expect( - oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { value: 2, }), ) - .to.be.revertedWithCustomError(oracle, "InsufficientPayment") - .withArgs(1, 3, 2); + .to.be.revertedWithCustomError(oracle, "InsufficientWithdrawalFee") + .withArgs(3, 2); }); it("Emit ValidatorExit event and should trigger withdrawals", async () => { - const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { value: 4, }); const timestamp = await oracle.getTime(); await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(authorizedEntity, 1); await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp, authorizedEntity); await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp, authorizedEntity); await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp, authorizedEntity); await expect(tx) .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") @@ -141,11 +142,11 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { it("Out of tw exit request limit", async () => { await expect( - oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { value: 4, }), ) - .to.be.revertedWithCustomError(oracle, "TWExitRequestsLimit") + .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") .withArgs(3, 1); }); @@ -154,23 +155,23 @@ describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { }); it("Limit regenerated in a day", async () => { - const tx = oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, { + const tx = oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { value: 4, }); const timestamp = await oracle.getTime(); await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(anyValue, 1); + await expect(tx).to.emit(oracle, "MadeRefund").withArgs(authorizedEntity, 1); await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp, authorizedEntity); await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp, authorizedEntity); await expect(tx) .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp); + .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp, authorizedEntity); await expect(tx) .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") From 307c8062cf5975fc8fd2e80a30ee000ddcd3223b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 24 Apr 2025 10:48:22 +0200 Subject: [PATCH 106/405] feat: add events for withdrawal and consolidation request --- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 7 ++++++ test/0.8.9/withdrawalVault/eip7002Mock.ts | 2 +- .../withdrawalVault/withdrawalVault.test.ts | 23 ++++++++++++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index ac9795a511..02225b900f 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -23,6 +23,9 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt uint256 internal constant PUBLIC_KEY_LENGTH = 48; + event WithdrawalRequestAdded(bytes request); + event ConsolidationRequestAdded(bytes request); + error ZeroArgument(string name); error MalformedPubkeysArray(); error ArraysLengthMismatch(uint256 firstArrayLength, uint256 secondArrayLength); @@ -121,6 +124,8 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt if (!success) { revert RequestAdditionFailed(request); } + + emit WithdrawalRequestAdded(request); } } @@ -174,6 +179,8 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt if (!success) { revert RequestAdditionFailed(request); } + + emit ConsolidationRequestAdded(request); } } diff --git a/test/0.8.9/withdrawalVault/eip7002Mock.ts b/test/0.8.9/withdrawalVault/eip7002Mock.ts index 807fbae43c..88eb7a2454 100644 --- a/test/0.8.9/withdrawalVault/eip7002Mock.ts +++ b/test/0.8.9/withdrawalVault/eip7002Mock.ts @@ -8,7 +8,7 @@ const eventName = "RequestAdded__Mock"; const eip7002MockEventABI = [`event ${eventName}(bytes request, uint256 fee)`]; const eip7002MockInterface = new ethers.Interface(eip7002MockEventABI); -function encodeEIP7002Payload(pubkey: string, amount: bigint): string { +export function encodeEIP7002Payload(pubkey: string, amount: bigint): string { return `0x${pubkey}${amount.toString(16).padStart(16, "0")}`; } diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 11904bc62f..2d7e163fa4 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -17,7 +17,7 @@ import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, p import { Snapshot } from "test/suite"; -import { findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; +import { encodeEIP7002Payload, findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; import { generateWithdrawalRequestPayload } from "./utils"; const PETRIFIED_VERSION = MAX_UINT256; @@ -482,6 +482,27 @@ describe("WithdrawalVault.sol", () => { ); }); + it("Should emit withdrawal event", async function () { + const requestCount = 3; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + + const fee = 3n; + await withdrawalsPredeployed.mock__setFee(fee); + const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei + + await expect( + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), + ) + .to.emit(vault, "WithdrawalRequestAdded") + .withArgs(encodeEIP7002Payload(pubkeys[0], mixedWithdrawalAmounts[0])) + .and.to.emit(vault, "WithdrawalRequestAdded") + .withArgs(encodeEIP7002Payload(pubkeys[1], mixedWithdrawalAmounts[1])) + .and.to.emit(vault, "WithdrawalRequestAdded") + .withArgs(encodeEIP7002Payload(pubkeys[2], mixedWithdrawalAmounts[2])); + }); + it("Should not affect contract balance", async function () { const requestCount = 3; const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); From c36ee32b70c0d1e17aeb7dc1ff81d935781b87d4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 24 Apr 2025 13:19:13 +0400 Subject: [PATCH 107/405] fix: addFullWithdrawalRequests -> addWithdrawalRequests in veb --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 13 ++++++++----- .../0.8.9/contracts/WithdrawalValut_MockForVebo.sol | 5 ++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index b6a20f4852..32c796f44e 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -11,7 +11,7 @@ import {PausableUntil} from "../utils/PausableUntil.sol"; import {IValidatorsExitBus} from "../interfaces/IValidatorExitBus.sol"; interface IWithdrawalVault { - function addFullWithdrawalRequests(bytes calldata pubkeys) external payable; + function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; function getWithdrawalRequestFee() external view returns (uint256); } @@ -302,8 +302,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } - IWithdrawalVault(LOCATOR.withdrawalVault()).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( - pubkeys + IWithdrawalVault(LOCATOR.withdrawalVault()).addWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( + pubkeys, + new uint64[](keyIndexes.length) ); _refundFee(keyIndexes.length * withdrawalFee, refundRecipient); @@ -365,8 +366,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } - IWithdrawalVault(LOCATOR.withdrawalVault()).addFullWithdrawalRequests{value: withdrawalFee * requestsCount}( - exitData.validatorsPubkeys + uint64[] memory amount = new uint64[](requestsCount); + IWithdrawalVault(LOCATOR.withdrawalVault()).addWithdrawalRequests{value: withdrawalFee * requestsCount}( + exitData.validatorsPubkeys, + amount ); _refundFee(requestsCount * withdrawalFee, refundRecipient); diff --git a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol index f9810b2314..be66bc98f3 100644 --- a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol +++ b/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol @@ -1,14 +1,13 @@ pragma solidity 0.8.9; contract WithdrawalVault__MockForVebo { - event AddFullWithdrawalRequestsCalled(bytes pubkeys); - function addFullWithdrawalRequests(bytes calldata pubkeys) external payable { + function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amount) external payable { emit AddFullWithdrawalRequestsCalled(pubkeys); } function getWithdrawalRequestFee() external view returns (uint256) { return 1; } -} \ No newline at end of file +} From 8ea44659a652cdf24b420754294ec55df778e600 Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 11:39:52 +0200 Subject: [PATCH 108/405] chore: update logs --- contracts/0.8.9/LidoLocator.sol | 4 ++ lib/state-file.ts | 1 + .../tw-deploy-params.ts | 0 scripts/triggerable-withdrawals/tw-deploy.ts | 54 ++++++++++++++++++- scripts/triggerable-withdrawals/tw-verify.ts | 26 ++++++++- 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 scripts/triggerable-withdrawals/tw-deploy-params.ts diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index 07392a2809..2f57353669 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -43,9 +43,11 @@ contract LidoLocator is ILidoLocator { address public immutable stakingRouter; address public immutable treasury; address public immutable validatorsExitBusOracle; + address public immutable validatorsExitBus; address public immutable withdrawalQueue; address public immutable withdrawalVault; address public immutable oracleDaemonConfig; + address public immutable exitBusVerifier; /** * @notice declare service locations @@ -64,9 +66,11 @@ contract LidoLocator is ILidoLocator { stakingRouter = _assertNonZero(_config.stakingRouter); treasury = _assertNonZero(_config.treasury); validatorsExitBusOracle = _assertNonZero(_config.validatorsExitBusOracle); + validatorsExitBus = validatorsExitBusOracle; withdrawalQueue = _assertNonZero(_config.withdrawalQueue); withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); + exitBusVerifier = _assertNonZero(_config.exitBusVerifier); } function coreComponents() external view returns( diff --git a/lib/state-file.ts b/lib/state-file.ts index 71332794f3..e0cbc951c3 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -87,6 +87,7 @@ export enum Sk { scratchDeployGasUsed = "scratchDeployGasUsed", minFirstAllocationStrategy = "minFirstAllocationStrategy", triggerableWithdrawals = "triggerableWithdrawals", + validatorExitVerifier = "validatorExitVerifier", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/triggerable-withdrawals/tw-deploy-params.ts b/scripts/triggerable-withdrawals/tw-deploy-params.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 22329ff3da..060d7d000c 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -84,14 +84,64 @@ async function main() { }); log.success(`NOR implementation address: ${NOR.address}`); + log.emptyLine(); + const validatorExitVerifierArgs = [ + locator.address, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + 1, // uint64 firstSupportedSlot, + 1, // uint64 pivotSlot, + 32, // uint32 slotsPerEpoch, + 12, // uint32 secondsPerSlot, + genesisTime, // uint64 genesisTime, + 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds + ]; + + const validatorExitVerifier = await deployImplementation( + Sk.validatorExitVerifier, + "ValidatorExitVerifier", + deployer, + validatorExitVerifierArgs, + ); + log.success(`ValidatorExitVerifier implementation address: ${NOR.address}`); log.emptyLine(); + // Update lido locator + const locatorImplContract = await loadContract("LidoLocator", INTERMEDIATE_LOCATOR_IMPL); + // fetch contract addresses that will not changed + const locatorConfig = [ + [ + await locatorImplContract.accountingOracle(), + await locatorImplContract.depositSecurityModule(), + await locatorImplContract.elRewardsVault(), + await locatorImplContract.legacyOracle(), + await locatorImplContract.lido(), + await locatorImplContract.oracleReportSanityChecker(), + await locatorImplContract.postTokenRebaseReceiver(), + await locatorImplContract.burner(), + await locatorImplContract.stakingRouter(), + await locatorImplContract.treasury(), + await locatorImplContract.validatorsExitBusOracle(), + await locatorImplContract.withdrawalQueue(), + await locatorImplContract.withdrawalVault(), + await locatorImplContract.oracleDaemonConfig(), + ], + ]; + + const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, locatorConfig); + log(`Configuration for voting script:`); - log(`VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle}" + log(` +LIDO_LOCATOR = "${lidoLocator.address}" +VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle}" WITHDRAWAL_VAULT_IMPL = "${withdrawalVault}" STAKING_ROUTER_IMPL = "${stakingRouterAddress}" -NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}"`); +NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" +VALIDATOR_EXIT_VERIFIER = "${validatorExitVerifier.address}" +`); } main() diff --git a/scripts/triggerable-withdrawals/tw-verify.ts b/scripts/triggerable-withdrawals/tw-verify.ts index 87e4c03d45..f175b4a41a 100644 --- a/scripts/triggerable-withdrawals/tw-verify.ts +++ b/scripts/triggerable-withdrawals/tw-verify.ts @@ -44,8 +44,20 @@ async function main() { const TREASURY_PROXY = await locator.treasury(); const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; - const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; + const validatorExitVerifierArgs = [ + locator.address, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + 1, // uint64 firstSupportedSlot, + 1, // uint64 pivotSlot, + 32, // uint32 slotsPerEpoch, + 12, // uint32 secondsPerSlot, + genesisTime, // uint64 genesisTime, + 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds + ]; await run("verify:verify", { address: state[Sk.withdrawalVault].implementation.address, @@ -58,6 +70,18 @@ async function main() { constructorArguments: validatorsExitBusOracleArgs, contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", }); + + await run("verify:verify", { + address: state[Sk.validatorExitVerifier].implementation.address, + constructorArguments: validatorExitVerifierArgs, + contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", + }); + + await run("verify:verify", { + address: state[Sk.lidoLocator].implementation.address, + constructorArguments: locatorConfig, + contract: "contracts/0.8.9/LidoLocator.sol:LidoLocator", + }); } main() From eb600de060c857132213307d99fc9ed75b9f06b5 Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 12:24:17 +0200 Subject: [PATCH 109/405] chore: lido locator update --- contracts/0.8.9/LidoLocator.sol | 7 +- .../tw-deploy-params.ts | 0 scripts/triggerable-withdrawals/tw-deploy.ts | 82 ++++++++++--------- 3 files changed, 47 insertions(+), 42 deletions(-) delete mode 100644 scripts/triggerable-withdrawals/tw-deploy-params.ts diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index 2f57353669..c8c02026a5 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -28,6 +28,7 @@ contract LidoLocator is ILidoLocator { address withdrawalQueue; address withdrawalVault; address oracleDaemonConfig; + address validatorExitVerifier; } error ZeroAddress(); @@ -43,11 +44,10 @@ contract LidoLocator is ILidoLocator { address public immutable stakingRouter; address public immutable treasury; address public immutable validatorsExitBusOracle; - address public immutable validatorsExitBus; address public immutable withdrawalQueue; address public immutable withdrawalVault; address public immutable oracleDaemonConfig; - address public immutable exitBusVerifier; + address public immutable validatorExitVerifier; /** * @notice declare service locations @@ -66,11 +66,10 @@ contract LidoLocator is ILidoLocator { stakingRouter = _assertNonZero(_config.stakingRouter); treasury = _assertNonZero(_config.treasury); validatorsExitBusOracle = _assertNonZero(_config.validatorsExitBusOracle); - validatorsExitBus = validatorsExitBusOracle; withdrawalQueue = _assertNonZero(_config.withdrawalQueue); withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); - exitBusVerifier = _assertNonZero(_config.exitBusVerifier); + validatorExitVerifier = _assertNonZero(_config.validatorExitVerifier); } function coreComponents() external view returns( diff --git a/scripts/triggerable-withdrawals/tw-deploy-params.ts b/scripts/triggerable-withdrawals/tw-deploy-params.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 060d7d000c..933375bb72 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -48,23 +48,24 @@ async function main() { // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; - const validatorsExitBusOracle = ( - await deployImplementation( - Sk.validatorsExitBusOracle, - "ValidatorsExitBusOracle", - deployer, - validatorsExitBusOracleArgs, - ) - ).address; - log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle}`); + const validatorsExitBusOracle = await deployImplementation( + Sk.validatorsExitBusOracle, + "ValidatorsExitBusOracle", + deployer, + validatorsExitBusOracleArgs, + ); + log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle.address}`); log.emptyLine(); const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; - const withdrawalVault = ( - await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, withdrawalVaultArgs) - ).address; - log.success(`WithdrawalVault address implementation: ${withdrawalVault}`); + const withdrawalVault = await deployImplementation( + Sk.withdrawalVault, + "WithdrawalVault", + deployer, + withdrawalVaultArgs, + ); + log.success(`WithdrawalVault address implementation: ${withdrawalVault.address}`); const minFirstAllocationStrategyAddress = state[Sk.minFirstAllocationStrategy].address; const libraries = { @@ -73,11 +74,15 @@ async function main() { const DEPOSIT_CONTRACT_ADDRESS = state[Sk.chainSpec].depositContract; log(`Deposit contract address: ${DEPOSIT_CONTRACT_ADDRESS}`); - const stakingRouterAddress = ( - await deployImplementation(Sk.stakingRouter, "StakingRouter", deployer, [DEPOSIT_CONTRACT_ADDRESS], { libraries }) - ).address; + const stakingRouterAddress = await deployImplementation( + Sk.stakingRouter, + "StakingRouter", + deployer, + [DEPOSIT_CONTRACT_ADDRESS], + { libraries }, + ); - log(`StakingRouter implementation address: ${stakingRouterAddress}`); + log(`StakingRouter implementation address: ${stakingRouterAddress.address}`); const NOR = await deployImplementation(Sk.appNodeOperatorsRegistry, "NodeOperatorsRegistry", deployer, [], { libraries, @@ -109,36 +114,37 @@ async function main() { log.success(`ValidatorExitVerifier implementation address: ${NOR.address}`); log.emptyLine(); - // Update lido locator - const locatorImplContract = await loadContract("LidoLocator", INTERMEDIATE_LOCATOR_IMPL); // fetch contract addresses that will not changed const locatorConfig = [ [ - await locatorImplContract.accountingOracle(), - await locatorImplContract.depositSecurityModule(), - await locatorImplContract.elRewardsVault(), - await locatorImplContract.legacyOracle(), - await locatorImplContract.lido(), - await locatorImplContract.oracleReportSanityChecker(), - await locatorImplContract.postTokenRebaseReceiver(), - await locatorImplContract.burner(), - await locatorImplContract.stakingRouter(), - await locatorImplContract.treasury(), - await locatorImplContract.validatorsExitBusOracle(), - await locatorImplContract.withdrawalQueue(), - await locatorImplContract.withdrawalVault(), - await locatorImplContract.oracleDaemonConfig(), + await locator.accountingOracle(), + await locator.depositSecurityModule(), + await locator.elRewardsVault(), + await locator.legacyOracle(), + await locator.lido(), + await locator.oracleReportSanityChecker(), + await locator.postTokenRebaseReceiver(), + await locator.burner(), + await locator.stakingRouter(), + await locator.treasury(), + await locator.validatorsExitBusOracle(), + await locator.withdrawalQueue(), + await locator.withdrawalVault(), + await locator.oracleDaemonConfig(), ], ]; - const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, locatorConfig); + const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [ + ...locatorConfig, + validatorExitVerifier.address, + ]); log(`Configuration for voting script:`); log(` -LIDO_LOCATOR = "${lidoLocator.address}" -VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle}" -WITHDRAWAL_VAULT_IMPL = "${withdrawalVault}" -STAKING_ROUTER_IMPL = "${stakingRouterAddress}" +LIDO_LOCATOR_IMPL = "${lidoLocator.address}" +VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}" +WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" +STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" VALIDATOR_EXIT_VERIFIER = "${validatorExitVerifier.address}" `); From 9a94fb153b22f8b225132a5c4180f8f0549d7043 Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 12:50:44 +0200 Subject: [PATCH 110/405] chore: fix linters --- deployed-hoodi.json | 86 ++++++++++++-------- scripts/scratch/steps/0130-grant-roles.ts | 2 +- scripts/triggerable-withdrawals/tw-deploy.ts | 36 ++++---- scripts/triggerable-withdrawals/tw-verify.ts | 2 +- test/deploy/locator.ts | 1 + 5 files changed, 72 insertions(+), 55 deletions(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 86431d526a..71ab7a1fef 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -144,7 +144,7 @@ "app:node-operators-registry": { "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x749b29ed0A41A431A69C3E1b0432dc1df13408E7", + "address": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", "constructorArgs": [] }, "aragonApp": { @@ -183,6 +183,16 @@ ] } }, + "app:sandbox": { + "aragonApp": { + "name": "sandbox", + "fullName": "sandbox.lidopm.eth" + }, + "proxy": { + "address": "0x682E94d2630846a503BDeE8b6810DF71C9806891", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, "app:simple-dvt": { "aragonApp": { "name": "simple-dvt", @@ -199,16 +209,6 @@ ] } }, - "app:sandbox": { - "aragonApp": { - "name": "sandbox", - "fullName": "sandbox.lidopm.eth" - }, - "proxy": { - "address": "0x682E94d2630846a503BDeE8b6810DF71C9806891", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, "aragon-acl": { "implementation": { "contract": "@aragon/os/contracts/acl/ACL.sol", @@ -480,24 +480,25 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x3C20EA1Bd0A838a7E4bE7CE47917DEF0c2E190FD", + "address": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042", "constructorArgs": [ - { - "accountingOracle": "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", - "depositSecurityModule": "0x2F0303F20E0795E6CCd17BD5efE791A586f28E03", - "elRewardsVault": "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", - "legacyOracle": "0x5B70b650B7E14136eb141b5Bf46a52f962885752", - "lido": "0x3508A952176b3c15387C97BE809eaffB1982176a", - "oracleReportSanityChecker": "0x26AED10459e1096d242ABf251Ff55f8DEaf52348", - "postTokenRebaseReceiver": "0x5B70b650B7E14136eb141b5Bf46a52f962885752", - "burner": "0x4e9A9ea2F154bA34BE919CD16a4A953DCd888165", - "stakingRouter": "0xCc820558B39ee15C7C45B59390B503b83fb499A8", - "treasury": "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", - "validatorsExitBusOracle": "0x8664d394C2B3278F26A1B44B967aEf99707eeAB2", - "withdrawalQueue": "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", - "withdrawalVault": "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", - "oracleDaemonConfig": "0x2a833402e3F46fFC1ecAb3598c599147a78731a9" - } + [ + "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", + "0x2F0303F20E0795E6CCd17BD5efE791A586f28E03", + "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", + "0x5B70b650B7E14136eb141b5Bf46a52f962885752", + "0x3508A952176b3c15387C97BE809eaffB1982176a", + "0x26AED10459e1096d242ABf251Ff55f8DEaf52348", + "0x5B70b650B7E14136eb141b5Bf46a52f962885752", + "0x4e9A9ea2F154bA34BE919CD16a4A953DCd888165", + "0xCc820558B39ee15C7C45B59390B503b83fb499A8", + "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", + "0x8664d394C2B3278F26A1B44B967aEf99707eeAB2", + "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", + "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", + "0x2a833402e3F46fFC1ecAb3598c599147a78731a9", + "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" + ] ] } }, @@ -572,7 +573,7 @@ [9000, 43200, 1000, 50, 600, 8, 24, 128, 750000, 1000, 101, 50] ] }, - "scratchDeployGasUsed": "126084566", + "scratchDeployGasUsed": "181127994", "simpleDvt": { "deployParameters": { "stakingModuleTypeId": "curated-onchain-v1", @@ -591,10 +592,29 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x7637A8Afd3B464E4481c67d51Cfe234f64903fd3", + "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] } }, + "validatorExitVerifier": { + "implementation": { + "contract": "contracts/0.8.25/ValidatorExitVerifier.sol", + "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "constructorArgs": [ + "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", + "0x0000000000000000000000000000000000000000000000000096000000000028", + "0x0000000000000000000000000000000000000000000000000096000000000028", + "0x000000000000000000000000000000000000000000000000000000000161c004", + "0x000000000000000000000000000000000000000000000000000000000161c004", + 1, + 1, + 32, + 12, + 1639659600, + 98304 + ] + } + }, "validatorsExitBusOracle": { "deployParameters": { "consensusVersion": 2 @@ -610,8 +630,8 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0x0e71BeD56B76E8ED96af5Bd5CDceE6F7f72201B1", - "constructorArgs": [12, 1742213400, "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", + "constructorArgs": [12, 1639659600, "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] } }, "vestingParams": { @@ -649,7 +669,7 @@ "withdrawalVault": { "implementation": { "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0x0f262D9A5Ada76C31cE638bA7AcAA8BA55827483", + "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", "constructorArgs": ["0x3508A952176b3c15387C97BE809eaffB1982176a", "0x0534aA41907c9631fae990960bCC72d75fA7cfeD"] }, "proxy": { diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index f332bc8403..ff611c85df 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -90,7 +90,7 @@ export async function main() { await makeTx( withdrawalVault, "grantRole", - [await withdrawalVault.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(), validatorsExitBusOracleAddress], + [await withdrawalVault.ADD_WITHDRAWAL_REQUEST_ROLE(), validatorsExitBusOracleAddress], { from: deployer, }, diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 933375bb72..eb0baf6d06 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -116,28 +116,24 @@ async function main() { // fetch contract addresses that will not changed const locatorConfig = [ - [ - await locator.accountingOracle(), - await locator.depositSecurityModule(), - await locator.elRewardsVault(), - await locator.legacyOracle(), - await locator.lido(), - await locator.oracleReportSanityChecker(), - await locator.postTokenRebaseReceiver(), - await locator.burner(), - await locator.stakingRouter(), - await locator.treasury(), - await locator.validatorsExitBusOracle(), - await locator.withdrawalQueue(), - await locator.withdrawalVault(), - await locator.oracleDaemonConfig(), - ], + await locator.accountingOracle(), + await locator.depositSecurityModule(), + await locator.elRewardsVault(), + await locator.legacyOracle(), + await locator.lido(), + await locator.oracleReportSanityChecker(), + await locator.postTokenRebaseReceiver(), + await locator.burner(), + await locator.stakingRouter(), + await locator.treasury(), + await locator.validatorsExitBusOracle(), + await locator.withdrawalQueue(), + await locator.withdrawalVault(), + await locator.oracleDaemonConfig(), + validatorExitVerifier.address, ]; - const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [ - ...locatorConfig, - validatorExitVerifier.address, - ]); + const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); log(`Configuration for voting script:`); log(` diff --git a/scripts/triggerable-withdrawals/tw-verify.ts b/scripts/triggerable-withdrawals/tw-verify.ts index f175b4a41a..b8cde5de49 100644 --- a/scripts/triggerable-withdrawals/tw-verify.ts +++ b/scripts/triggerable-withdrawals/tw-verify.ts @@ -79,7 +79,7 @@ async function main() { await run("verify:verify", { address: state[Sk.lidoLocator].implementation.address, - constructorArguments: locatorConfig, + constructorArguments: [], // TBD contract: "contracts/0.8.9/LidoLocator.sol:LidoLocator", }); } diff --git a/test/deploy/locator.ts b/test/deploy/locator.ts index 84e63a22e6..eb30806e2f 100644 --- a/test/deploy/locator.ts +++ b/test/deploy/locator.ts @@ -28,6 +28,7 @@ async function deployDummyLocator(config?: Partial, de validatorsExitBusOracle: certainAddress("dummy-locator:validatorsExitBusOracle"), withdrawalQueue: certainAddress("dummy-locator:withdrawalQueue"), withdrawalVault: certainAddress("dummy-locator:withdrawalVault"), + validatorExitVerifier: certainAddress("dummy-locator:validatorExitVerifier"), ...config, }); From 6a155f41453310b9dfea3bccd1534a441eab9e3c Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 13:49:35 +0200 Subject: [PATCH 111/405] chore: fix units --- lib/deploy.ts | 1 + test/0.8.9/lidoLocator.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/deploy.ts b/lib/deploy.ts index 24aa589c64..a7f991fd02 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -246,6 +246,7 @@ async function getLocatorConfig(locatorAddress: string) { "withdrawalQueue", "withdrawalVault", "oracleDaemonConfig", + "validatorExitVerifier", ] as (keyof LidoLocator.ConfigStruct)[]; const configPromises = addresses.map((name) => locator[name]()); diff --git a/test/0.8.9/lidoLocator.test.ts b/test/0.8.9/lidoLocator.test.ts index 711869b439..0fd20a2e4e 100644 --- a/test/0.8.9/lidoLocator.test.ts +++ b/test/0.8.9/lidoLocator.test.ts @@ -21,6 +21,7 @@ const services = [ "withdrawalQueue", "withdrawalVault", "oracleDaemonConfig", + "validatorExitVerifier", ] as const; type ArrayToUnion = A[number]; From c1776b3547dd01e83c84c640b13430a731b56f60 Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 15:12:06 +0200 Subject: [PATCH 112/405] chore: fix scratch deploy --- .../steps/0090-deploy-non-aragon-contracts.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 93e4426ad8..31affb3267 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -12,6 +12,15 @@ import { import { log } from "lib/log"; import { readNetworkState, Sk, updateObjectInState } from "lib/state-file"; +function getEnvVariable(name: string, defaultValue?: string): string { + const value = process.env[name] ?? defaultValue; + if (value === undefined) { + throw new Error(`Environment variable ${name} is required`); + } + log(`${name} = ${value}`); + return value; +} + export async function main() { const deployer = (await ethers.provider.getSigner()).address; const state = readNetworkState({ deployer }); @@ -191,6 +200,22 @@ export async function main() { burnerParams.totalNonCoverSharesBurnt, ]); + // Deploy ValidatorExitVerifier + const validatorExitVerifier = await deployWithoutProxy(Sk.validatorExitVerifier, "validatorExitVerifier", deployer, [ + locator.address, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + 1, // uint64 firstSupportedSlot, + 1, // uint64 pivotSlot, + chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, + chainSpec.secondsPerSlot, // uint32 secondsPerSlot, + parseInt(getEnvVariable("GENESIS_TIME")), // uint64 genesisTime, + // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 + 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds + ]); + // Update LidoLocator with valid implementation const locatorConfig: string[] = [ accountingOracle.address, @@ -207,6 +232,7 @@ export async function main() { withdrawalQueueERC721.address, withdrawalVaultAddress, oracleDaemonConfig.address, + validatorExitVerifier.address, ]; await updateProxyImplementation(Sk.lidoLocator, "LidoLocator", locator.address, proxyContractsOwner, [locatorConfig]); } From d452e7bfce6565a91a79673c0950d7198bacde96 Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 15:12:06 +0200 Subject: [PATCH 113/405] chore: fix scratch deploy --- .../steps/0090-deploy-non-aragon-contracts.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 93e4426ad8..31affb3267 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -12,6 +12,15 @@ import { import { log } from "lib/log"; import { readNetworkState, Sk, updateObjectInState } from "lib/state-file"; +function getEnvVariable(name: string, defaultValue?: string): string { + const value = process.env[name] ?? defaultValue; + if (value === undefined) { + throw new Error(`Environment variable ${name} is required`); + } + log(`${name} = ${value}`); + return value; +} + export async function main() { const deployer = (await ethers.provider.getSigner()).address; const state = readNetworkState({ deployer }); @@ -191,6 +200,22 @@ export async function main() { burnerParams.totalNonCoverSharesBurnt, ]); + // Deploy ValidatorExitVerifier + const validatorExitVerifier = await deployWithoutProxy(Sk.validatorExitVerifier, "validatorExitVerifier", deployer, [ + locator.address, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + 1, // uint64 firstSupportedSlot, + 1, // uint64 pivotSlot, + chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, + chainSpec.secondsPerSlot, // uint32 secondsPerSlot, + parseInt(getEnvVariable("GENESIS_TIME")), // uint64 genesisTime, + // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 + 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds + ]); + // Update LidoLocator with valid implementation const locatorConfig: string[] = [ accountingOracle.address, @@ -207,6 +232,7 @@ export async function main() { withdrawalQueueERC721.address, withdrawalVaultAddress, oracleDaemonConfig.address, + validatorExitVerifier.address, ]; await updateProxyImplementation(Sk.lidoLocator, "LidoLocator", locator.address, proxyContractsOwner, [locatorConfig]); } From b57c0550457c60c8fe7698aa52e496d74d40bb4a Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 24 Apr 2025 16:02:50 +0200 Subject: [PATCH 114/405] fix: raw deploy --- scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 31affb3267..2bdfe12cf8 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -201,7 +201,7 @@ export async function main() { ]); // Deploy ValidatorExitVerifier - const validatorExitVerifier = await deployWithoutProxy(Sk.validatorExitVerifier, "validatorExitVerifier", deployer, [ + const validatorExitVerifier = await deployWithoutProxy(Sk.validatorExitVerifier, "ValidatorExitVerifier", deployer, [ locator.address, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, From d3430af6f468fe14ae678a77f4e30160b7911abb Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 25 Apr 2025 21:02:11 +0400 Subject: [PATCH 115/405] fix: limits refactoring --- contracts/0.8.9/lib/ExitLimitUtils.sol | 111 ++++++-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 268 +++++++++--------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 7 +- .../contracts/ValidatorsExitBus__Harness.sol | 8 +- ...tor-exit-bus-oracle.emitExitEvents.test.ts | 8 - 5 files changed, 221 insertions(+), 181 deletions(-) diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index a4d18cb90b..491d2a8b4e 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -41,71 +41,122 @@ library ExitLimitUtilsStorage { } } +// TODO: description +// dailyLimit 0 - exits unlimited library ExitLimitUtils { /** - * @notice Returns the current limit for the current day - * @param data Exit request limit struct - * @param day Full days since the Unix epoch (block.timestamp / 1 days) + * @notice Thrown when remaining exit requests limit is not enough to cover sender requests + * @param requestsCount Amount of requests that were sent for processing + * @param remainingLimit Amount of requests that still can be processed at current day */ - function remainingLimit(ExitRequestLimitData memory data, uint256 day) internal pure returns (uint256) { - // TODO: uint64? - if (data.currentDay != day) { - return data.dailyLimit; + error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + + /** + * Method check limit and return how much can be processed + * @param requestsCount Amount of requests for processing + * @param currentTimestamp Block timestamp + * @return limit Amount of requests that can be processed + */ + function consumeLimit( + ExitRequestLimitData memory data, + uint256 requestsCount, + uint256 currentTimestamp + ) internal pure returns (uint256 limit) { + uint64 currentDay = uint64(currentTimestamp / 1 days); + + // exits unlimited + if (data.dailyLimit == 0) { + return requestsCount; } - return data.dailyExitCount >= data.dailyLimit ? 0 : data.dailyLimit - data.dailyExitCount; + if (data.currentDay != currentDay) { + return data.dailyLimit >= requestsCount ? requestsCount : data.dailyLimit; + } + + if (data.dailyExitCount >= data.dailyLimit) { + revert ExitRequestsLimit(requestsCount, 0); + } + + uint256 remainingLimit = data.dailyLimit - data.dailyExitCount; + return remainingLimit >= requestsCount ? requestsCount : remainingLimit; + } + + /** + * Method check limit and revert if requests amount is more than limit + * @param requestsCount Amount of requests for processing + * @param currentTimestamp Block timestamp + */ + function checkLimit( + ExitRequestLimitData memory data, + uint256 requestsCount, + uint256 currentTimestamp + ) internal pure { + uint64 currentDay = uint64(currentTimestamp / 1 days); + + // exits unlimited + if (data.dailyLimit == 0) return; + + if (data.currentDay != currentDay) return; + + if (data.dailyExitCount >= data.dailyLimit) { + revert ExitRequestsLimit(requestsCount, 0); + } + + uint256 remainingLimit = data.dailyLimit - data.dailyExitCount; + + if (requestsCount > remainingLimit) { + revert ExitRequestsLimit(requestsCount, remainingLimit); + } } /** * @notice Updates the current request counter and day in the exit limit data * @param data Exit request limit struct - * @param currentDay Full days since the Unix epoch (block.timestamp / 1 days) * @param newCount New requests amount spent during the day + * @param currentTimestamp Block timestamp */ function updateRequestsCounter( ExitRequestLimitData memory data, - uint256 currentDay, - uint256 newCount + uint256 newCount, + uint256 currentTimestamp ) internal pure returns (ExitRequestLimitData memory) { + require(newCount <= type(uint96).max, "TOO_LARGE_REQUESTS_COUNT_LIMIT"); + + // TODO: Should we count requests when exits are unlimited? + // If a limit is set after a period of unlimited exits, should we account for the requests that already occurred? + // if (data.dailyLimit == 0) return; + + uint64 currentDay = uint64(currentTimestamp / 1 days); + if (data.currentDay != currentDay) { - data.currentDay = uint64(currentDay); + data.currentDay = currentDay; data.dailyExitCount = 0; } + require(data.dailyLimit == 0 || newCount <= data.dailyLimit - data.dailyExitCount , "REQUESTS_COUNT_EXCEED_LIMIT"); + uint256 updatedCount = uint256(data.dailyExitCount) + newCount; require(updatedCount <= type(uint96).max, "DAILY_EXIT_COUNT_OVERFLOW"); - if (data.dailyLimit != 0) { - require(updatedCount <= data.dailyLimit, "DAILY_LIMIT_REACHED"); - } - data.dailyExitCount = uint96(updatedCount); return data; } - /** - * @notice check if max daily exit request limit is set. Otherwise there are no limits on exits - */ - function isExitDailyLimitSet(ExitRequestLimitData memory data) internal pure returns (bool) { - return data.dailyLimit != 0; - } - /** * @notice Update daily limit * @param data Exit request limit struct * @param limit Exit request limit per day - * @dev TODO: maybe we need use here uin96 - * what will happen if method got argument with bigger value than uint96? + * @param currentTimestamp Block timestamp */ function setExitDailyLimit( ExitRequestLimitData memory data, - uint256 limit - ) internal view returns (ExitRequestLimitData memory) { - require(limit != 0, "ZERO_EXIT_REQUESTS_LIMIT"); - require(limit <= type(uint96).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); + uint256 limit, + uint256 currentTimestamp + ) internal pure returns (ExitRequestLimitData memory) { + require(limit <= type(uint96).max, "TOO_LARGE_DAILY_LIMIT"); - uint64 day = uint64(block.timestamp / 1 days); + uint64 day = uint64(currentTimestamp / 1 days); require(data.currentDay <= day, "INVALID_TIMESTAMP_BACKWARD"); data.dailyLimit = uint96(limit); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 32c796f44e..f90c89161b 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -43,6 +43,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param format code of format, currently only DATA_FORMAT=1 is supported in the contract */ error UnsupportedRequestsDataFormat(uint256 format); + /** * @notice Thrown when exit request has wrong length */ @@ -54,24 +55,27 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error InvalidRequestsData(); /** - * @notice + * TODO: maybe this part will be deleted */ error InvalidRequestsDataSortOrder(); + + /** + * @notice Thrown when pubkeys of invalid length are provided + */ error InvalidPubkeysArray(); - error NoExitRequestProvided(); + /** + * Thrown when there are attempt to send exit events for request that was not submitted earlier by trusted entities + */ error ExitHashWasNotSubmitted(); - error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); - error RequestsAlreadyDelivered(); - error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); - /** - * @notice Thrown when remaining exit requests limit is not enough to cover sender requests - * @param requestsCount Amount of requests that were sent for processing - * @param remainingLimit Amount of requests that still can be processed at current day + * TODO: do we need this error ? + * @notice Throw when in emitExitEvents all requests were already delivered */ - error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + error RequestsAlreadyDelivered(); + + error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); /** * @notice Thrown when a withdrawal fee insufficient @@ -80,6 +84,11 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ error InsufficientWithdrawalFee(uint256 feeRequired, uint256 passedValue); + /** + * @notice Index in + */ + error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + /** * @notice Thrown when a withdrawal fee refund failed */ @@ -95,7 +104,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorPubkey, uint256 timestamp ); - event ExitRequestsLimitSet(uint256 exitRequestsLimit, uint256 ExitRequestsLimit); + event ExitRequestsLimitSet(uint256 exitRequestsLimit, uint256 twExitRequestsLimit); event DirectExitRequest( uint256 indexed stakingModuleId, @@ -178,6 +187,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); } + /// @notice Method to emit exit events by providing report data, the hash of which was previously stored + /// @param request Exit request data struct function emitExitEvents(ExitRequestData calldata request) external whenResumed { bytes calldata data = request.data; @@ -185,18 +196,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa keccak256(abi.encode(data, request.dataFormat)) ]; - if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); - } - - if (request.dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(request.dataFormat); - } - - if (request.data.length % PACKED_REQUEST_LENGTH != 0) { - revert InvalidRequestsDataLength(); - } - + _checkExitWasSubmitted(requestStatus); + _checkExitRequestData(request); _checkContractVersion(requestStatus.contractVersion); // By default, totalItemsCount is set to type(uint256).max. @@ -205,19 +206,29 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; } - uint256 deliveredItemsCount = requestStatus.deliveredItemsCount; - uint256 undeliveredItemsCount = requestStatus.totalItemsCount - deliveredItemsCount; + uint256 undeliveredItemsCount = requestStatus.totalItemsCount - requestStatus.deliveredItemsCount; if (undeliveredItemsCount == 0) { revert RequestsAlreadyDelivered(); } - uint256 requestsToDeliver = _applyExitLimitOrRevert(EXIT_REQUEST_LIMIT_POSITION, undeliveredItemsCount); + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + uint256 requestsToDeliver = exitRequestLimitData.consumeLimit(undeliveredItemsCount, _getTimestamp()); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updateRequestsCounter(requestsToDeliver, _getTimestamp()) + ); + + require( + requestStatus.totalItemsCount >= requestStatus.deliveredItemsCount + requestsToDeliver, + "INDEX_OUT_OF_RANGE" + ); + + _processExitRequestsList(request.data, requestStatus.deliveredItemsCount, requestsToDeliver); - _processExitRequestsList(request.data, deliveredItemsCount, requestsToDeliver); + require(requestStatus.deliveredItemsCount + requestsToDeliver - 1 >= 0, "WRONG_REQUESTS_TO_DELIVER_VALUE"); requestStatus.deliverHistory.push( - DeliveryHistory(deliveredItemsCount + requestsToDeliver - 1, _getTimestamp()) + DeliveryHistory(requestStatus.deliveredItemsCount + requestsToDeliver - 1, _getTimestamp()) ); requestStatus.deliveredItemsCount += requestsToDeliver; } @@ -243,23 +254,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa refundRecipient = msg.sender; } - bytes calldata data = request.data; + // bytes calldata data = request.data; RequestStatus storage requestStatus = _storageExitRequestsHashes()[ - keccak256(abi.encode(data, request.dataFormat)) + keccak256(abi.encode(request.data, request.dataFormat)) ]; - if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); - } - - if (request.dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(request.dataFormat); - } - - if (request.data.length % PACKED_REQUEST_LENGTH != 0) { - revert InvalidRequestsDataLength(); - } - + _checkExitWasSubmitted(requestStatus); + _checkExitRequestData(request); _checkContractVersion(requestStatus.contractVersion); uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); @@ -268,7 +269,11 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InsufficientWithdrawalFee(keyIndexes.length * withdrawalFee, msg.value); } - _checkAndUpdateDailyExitLimit(TW_EXIT_REQUEST_LIMIT_POSITION, keyIndexes.length); + ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + exitRequestLimitData.checkLimit(keyIndexes.length, _getTimestamp()); + TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updateRequestsCounter(keyIndexes.length, _getTimestamp()) + ); bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); @@ -282,7 +287,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyWasNotDelivered(keyIndexes[i], requestStatus.deliveredItemsCount - 1); } - ValidatorData memory validatorData = getValidatorData(data, keyIndexes[i]); + ValidatorData memory validatorData = _getValidatorData(request.data, keyIndexes[i]); if (validatorData.moduleId == 0) revert InvalidRequestsData(); pubkey = validatorData.pubkey; @@ -328,7 +333,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } if (exitData.validatorsPubkeys.length == 0) { - revert NoExitRequestProvided(); + revert ZeroArgument("exitData.validatorsPubkeys"); } if (exitData.validatorsPubkeys.length % PUBLIC_KEY_LENGTH != 0) { @@ -342,12 +347,16 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert InsufficientWithdrawalFee(withdrawalFee * requestsCount, msg.value); } - _checkAndUpdateDailyExitLimit(TW_EXIT_REQUEST_LIMIT_POSITION, requestsCount); + ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + exitRequestLimitData.checkLimit(requestsCount, _getTimestamp()); + TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updateRequestsCounter(requestsCount, _getTimestamp()) + ); for (uint256 i = 0; i < requestsCount; i++) { bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); - pubkey = getPubkey(exitData.validatorsPubkeys, i); + pubkey = _getPubkey(exitData.validatorsPubkeys, i); IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( exitData.stakingModuleId, @@ -383,12 +392,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa require(twExitsDailyLimit != 0, "ZERO_MAX_TW_EXIT_REQUEST_LIMIT"); require(exitsDailyLimit >= twExitsDailyLimit, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + uint256 timestamp = _getTimestamp(); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(exitsDailyLimit) + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(exitsDailyLimit, timestamp) ); TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(twExitsDailyLimit) + TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(twExitsDailyLimit, timestamp) ); emit ExitRequestsLimitSet(exitsDailyLimit, twExitsDailyLimit); @@ -399,9 +410,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; - if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); - } + _checkExitWasSubmitted(requestStatus); return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); } @@ -423,7 +432,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); } - ValidatorData memory validatorData = getValidatorData(exitRequests, index); + ValidatorData memory validatorData = _getValidatorData(exitRequests, index); valIndex = validatorData.valIndex; nodeOpId = validatorData.nodeOpId; @@ -464,54 +473,20 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Internal functions - /** - * @notice - * @dev We have sorting order for both - */ - function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { - uint256 offset; - uint256 offsetPastEnd; - uint256 lastDataWithoutPubkey = 0; - uint256 timestamp = _getTimestamp(); - - assembly { - offset := add(data.offset, mul(startIndex, PACKED_REQUEST_LENGTH)) - offsetPastEnd := add(offset, mul(count, PACKED_REQUEST_LENGTH)) + // TODO: fixed to be used in unpackExitRequest too + function _checkExitRequestData(ExitRequestData calldata request) internal pure { + if (request.dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(request.dataFormat); } - bytes calldata pubkey; - - assembly { - pubkey.length := 48 + if (request.data.length % PACKED_REQUEST_LENGTH != 0) { + revert InvalidRequestsDataLength(); } + } - while (offset < offsetPastEnd) { - uint256 dataWithoutPubkey; - assembly { - // 16 most significant bytes are taken by module id, node op id, and val index - dataWithoutPubkey := shr(128, calldataload(offset)) - // the next 48 bytes are taken by the pubkey - pubkey.offset := add(offset, 16) - // totalling to 64 bytes - offset := add(offset, 64) - } - // dataWithoutPubkey - // MSB <---------------------------------------------------------------------- LSB - // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - if (dataWithoutPubkey <= lastDataWithoutPubkey) { - revert InvalidRequestsDataSortOrder(); - } - - uint64 valIndex = uint64(dataWithoutPubkey); - uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); - uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - if (moduleId == 0) { - revert InvalidRequestsData(); - } - - lastDataWithoutPubkey = dataWithoutPubkey; - emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); + function _checkExitWasSubmitted(RequestStatus storage requestStatus) internal view { + if (requestStatus.contractVersion == 0) { + revert ExitHashWasNotSubmitted(); } } @@ -531,46 +506,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa return refund; } - function _checkAndUpdateDailyExitLimit(bytes32 _exitRequestPosition, uint256 requestsCount) internal { - ExitRequestLimitData memory exitRequestLimitData = _exitRequestPosition.getStorageExitRequestLimit(); - - if (!exitRequestLimitData.isExitDailyLimitSet()) { - return; - } - - uint256 day = _getTimestamp() / 1 days; - uint256 limit = exitRequestLimitData.remainingLimit(day); - - if (requestsCount > limit) { - revert ExitRequestsLimit(requestsCount, limit); - } - - _exitRequestPosition.setStorageExitRequestLimit(exitRequestLimitData.updateRequestsCounter(day, requestsCount)); - } - - function _applyExitLimitOrRevert(bytes32 _exitRequestPosition, uint256 requestsCount) internal returns (uint256) { - ExitRequestLimitData memory exitRequestLimitData = _exitRequestPosition.getStorageExitRequestLimit(); - - if (!exitRequestLimitData.isExitDailyLimitSet()) { - return requestsCount; - } - - uint256 day = _getTimestamp() / 1 days; - uint256 limit = exitRequestLimitData.remainingLimit(day); - - if (limit == 0) { - revert ExitRequestsLimit(requestsCount, limit); - } - - uint256 requestsToDeliver = requestsCount > limit ? limit : requestsCount; - - _exitRequestPosition.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(day, requestsToDeliver) - ); - - return requestsToDeliver; - } - function _getTimestamp() internal view virtual returns (uint256) { return block.timestamp; // solhint-disable-line not-rely-on-time } @@ -606,7 +541,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param index index of request in array above * @return validatorData Validator data including node operator id, module id, validator index */ - function getValidatorData( + function _getValidatorData( bytes calldata exitRequestData, uint256 index ) internal pure returns (ValidatorData memory validatorData) { @@ -647,7 +582,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param index index of pubkey in array above * @return pubkey Validator public key */ - function getPubkey(bytes calldata pubkeys, uint256 index) internal pure returns (bytes memory pubkey) { + function _getPubkey(bytes calldata pubkeys, uint256 index) internal pure returns (bytes memory pubkey) { pubkey = new bytes(PUBLIC_KEY_LENGTH); assembly { @@ -657,6 +592,61 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } + /** + * This method read report data (DATA_FORMAT=1) within a range + * check dataWithoutPubkey <= lastDataWithoutPubkey needs to prevent duplicates + * However, it seems that duplicates are no longer an issue. + * But this logic prevent use of _getValidatorData method here + * + * check what will happen if startIndex bigger than length of data + */ + function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { + uint256 offset; + uint256 offsetPastEnd; + uint256 lastDataWithoutPubkey = 0; + uint256 timestamp = _getTimestamp(); + + assembly { + offset := add(data.offset, mul(startIndex, PACKED_REQUEST_LENGTH)) + offsetPastEnd := add(offset, mul(count, PACKED_REQUEST_LENGTH)) + } + + bytes calldata pubkey; + + assembly { + pubkey.length := 48 + } + + while (offset < offsetPastEnd) { + uint256 dataWithoutPubkey; + assembly { + // 16 most significant bytes are taken by module id, node op id, and val index + dataWithoutPubkey := shr(128, calldataload(offset)) + // the next 48 bytes are taken by the pubkey + pubkey.offset := add(offset, 16) + // totalling to 64 bytes + offset := add(offset, 64) + } + // dataWithoutPubkey + // MSB <---------------------------------------------------------------------- LSB + // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | + if (dataWithoutPubkey <= lastDataWithoutPubkey) { + revert InvalidRequestsDataSortOrder(); + } + + uint64 valIndex = uint64(dataWithoutPubkey); + uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); + uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + if (moduleId == 0) { + revert InvalidRequestsData(); + } + + lastDataWithoutPubkey = dataWithoutPubkey; + emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); + } + } + /// Storage helpers function _storageExitRequestsHashes() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { bytes32 position = EXIT_REQUESTS_HASHES_POSITION; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 3f4f5e62e3..71380cee73 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -242,7 +242,12 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { .checkExitBusOracleReport(data.requestsCount); // Check VEB common limit - _checkAndUpdateDailyExitLimit(EXIT_REQUEST_LIMIT_POSITION, data.requestsCount); + + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + exitRequestLimitData.checkLimit(data.requestsCount, _getTimestamp()); + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updateRequestsCounter(data.requestsCount, _getTimestamp()) + ); if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 56bcdff561..e435b02e8f 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -28,12 +28,14 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { function _getTime() internal view override returns (uint256) { address consensus = CONSENSUS_CONTRACT_POSITION.getStorageAddress(); - return ITimeProvider(consensus).getTime(); + uint256 time = ITimeProvider(consensus).getTime(); + + return time; } + // Method used in VEB function _getTimestamp() internal view override returns (uint256) { - address consensus = CONSENSUS_CONTRACT_POSITION.getStorageAddress(); - return ITimeProvider(consensus).getTime(); + return _getTime(); } function getDataProcessingState() external view returns (DataProcessingState memory) { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts index b2c8b78a8c..e79cf2f5d4 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts @@ -196,10 +196,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); const emitTx = await oracle.emitExitEvents(exitRequest); - const receipt = await emitTx.wait(); - - expect(receipt?.logs.length).to.eq(2); - const timestamp = await oracle.getTime(); for (const request of exitRequests) { @@ -229,10 +225,6 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); const emitTx = await oracle.emitExitEvents(exitRequest); - const receipt = await emitTx.wait(); - - expect(receipt?.logs.length).to.eq(3); - const timestamp = await oracle.getTime(); for (let i = 0; i < 3; i++) { From e2a11ff2cc2632328337573c8081c722bc1bebb9 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 28 Apr 2025 15:35:25 +0200 Subject: [PATCH 116/405] fix: update eligibleToExit parameter type from bytes to uint256 --- contracts/0.8.9/StakingRouter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 8774660e42..f4a95ed57f 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1482,7 +1482,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, - bytes calldata _eligibleToExitInSec + uint256 _eligibleToExitInSec ) external onlyRole(REPORT_EXITED_VALIDATORS_STATUS_ROLE) From e964aff80e106d989f3f2ae6c51b04cfe46c4544 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 28 Apr 2025 15:42:51 +0200 Subject: [PATCH 117/405] fix: update eligibleToExit parameter type from bytes to uint256 in IStakingModule interface and mock contract --- contracts/0.8.9/interfaces/IStakingModule.sol | 2 +- test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 1291e16982..1a6e4b3c8c 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -27,7 +27,7 @@ interface IStakingModule { uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, - bytes calldata _eligibleToExitInSec + uint256 _eligibleToExitInSec ) external; /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index f03accccb8..0115d48f38 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -15,7 +15,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKeys, - bytes eligibleToExitInSec + uint256 eligibleToExitInSec ); event Mock__onValidatorExitTriggered( @@ -272,7 +272,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKeys, - bytes calldata _eligibleToExitInSec + uint256 _eligibleToExitInSec ) external { emit Mock__reportValidatorExitDelay( _nodeOperatorId, From 73ba72363b70823b1e0a1faa0fd277a0750b68d4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 30 Apr 2025 00:25:40 +0400 Subject: [PATCH 118/405] fix: covered ExitLimitUtils with tests --- contracts/0.8.9/lib/ExitLimitUtils.sol | 3 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 3 - .../contracts/ExitLimitUtils__Harness.sol | 62 +++++ test/0.8.9/lib/exitLimitUtils.test.ts | 220 ++++++++++++++++++ 4 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 test/0.8.9/contracts/ExitLimitUtils__Harness.sol create mode 100644 test/0.8.9/lib/exitLimitUtils.test.ts diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index 491d2a8b4e..bc3fcaa884 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -133,10 +133,9 @@ library ExitLimitUtils { data.dailyExitCount = 0; } - require(data.dailyLimit == 0 || newCount <= data.dailyLimit - data.dailyExitCount , "REQUESTS_COUNT_EXCEED_LIMIT"); - uint256 updatedCount = uint256(data.dailyExitCount) + newCount; require(updatedCount <= type(uint96).max, "DAILY_EXIT_COUNT_OVERFLOW"); + require(data.dailyLimit == 0 || updatedCount <= data.dailyLimit, "REQUESTS_COUNT_EXCEED_LIMIT"); data.dailyExitCount = uint96(updatedCount); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index f90c89161b..950f0e7466 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -84,9 +84,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ error InsufficientWithdrawalFee(uint256 feeRequired, uint256 passedValue); - /** - * @notice Index in - */ error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); /** diff --git a/test/0.8.9/contracts/ExitLimitUtils__Harness.sol b/test/0.8.9/contracts/ExitLimitUtils__Harness.sol new file mode 100644 index 0000000000..ff05392c87 --- /dev/null +++ b/test/0.8.9/contracts/ExitLimitUtils__Harness.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only +pragma solidity 0.8.9; + +import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "contracts/0.8.9/lib/ExitLimitUtils.sol"; +import {UnstructuredStorage} from "contracts/0.8.9/lib/UnstructuredStorage.sol"; + +contract ExitLimitUtilsStorage__Harness { + using ExitLimitUtilsStorage for bytes32; + + bytes32 public constant TEST_POSITION = keccak256("exit.limit.test.position"); + + function getStorageExitRequestLimit() external view returns (ExitRequestLimitData memory data) { + return TEST_POSITION.getStorageExitRequestLimit(); + } + + function setStorageExitRequestLimit(ExitRequestLimitData memory _data) external { + TEST_POSITION.setStorageExitRequestLimit(_data); + } +} + +contract ExitLimitUtils__Harness { + using ExitLimitUtils for ExitRequestLimitData; + + event CheckLimitDone(); + + ExitRequestLimitData public state; + + function harness_setState(uint96 dailyLimit, uint96 dailyExitCount, uint64 currentDay) external { + state.dailyLimit = dailyLimit; + state.dailyExitCount = dailyExitCount; + state.currentDay = currentDay; + } + + function harness_getState() external view returns (ExitRequestLimitData memory) { + return ExitRequestLimitData(state.dailyLimit, state.dailyExitCount, state.currentDay); + } + + function consumeLimit(uint256 requestsCount, uint256 currentTimestamp) external view returns (uint256 limit) { + return state.consumeLimit(requestsCount, currentTimestamp); + } + + function checkLimit(uint256 requestsCount, uint256 currentTimestamp) external { + state.checkLimit(requestsCount, currentTimestamp); + + emit CheckLimitDone(); + } + + function updateRequestsCounter( + uint256 newCount, + uint256 currentTimestamp + ) external view returns (ExitRequestLimitData memory) { + return state.updateRequestsCounter(newCount, currentTimestamp); + } + + function setExitDailyLimit( + uint256 limit, + uint256 currentTimestamp + ) external view returns (ExitRequestLimitData memory) { + return state.setExitDailyLimit(limit, currentTimestamp); + } +} diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts new file mode 100644 index 0000000000..218d08eeac --- /dev/null +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -0,0 +1,220 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { ExitLimitUtils__Harness, ExitLimitUtilsStorage__Harness } from "typechain-types"; + +interface ExitRequestLimitData { + dailyLimit: bigint; + dailyExitCount: bigint; + currentDay: bigint; +} + +const DAY = 86400; + +describe("ExitLimitUtils.sol", () => { + let exitLimitStorage: ExitLimitUtilsStorage__Harness; + let exitLimit: ExitLimitUtils__Harness; + + before(async () => { + exitLimitStorage = await ethers.deployContract("ExitLimitUtilsStorage__Harness"); + exitLimit = await ethers.deployContract("ExitLimitUtils__Harness"); + }); + + context("ExitLimitUtilsStorage", () => { + let data: ExitRequestLimitData; + + it("Min possible values", async () => { + data = { + dailyLimit: 0n, + dailyExitCount: 0n, + currentDay: 0n, + }; + + await exitLimitStorage.setStorageExitRequestLimit(data); + + const result = await exitLimitStorage.getStorageExitRequestLimit(); + expect(result.dailyLimit).to.equal(0n); + expect(result.dailyExitCount).to.equal(0n); + expect(result.currentDay).to.equal(0n); + }); + + it("Max possible values", async () => { + const MAX_UINT96 = 2n ** 96n - 1n; + const MAX_UINT64 = 2n ** 64n - 1n; + + data = { + dailyLimit: MAX_UINT96, + dailyExitCount: MAX_UINT96, + currentDay: MAX_UINT64, + }; + + await exitLimitStorage.setStorageExitRequestLimit(data); + + const result = await exitLimitStorage.getStorageExitRequestLimit(); + expect(result.dailyLimit).to.equal(MAX_UINT96); + expect(result.dailyExitCount).to.equal(MAX_UINT96); + expect(result.currentDay).to.equal(MAX_UINT64); + }); + + it("Some random values", async () => { + const dailyLimit = 100n; + const dailyExitCount = 50n; + const currentDay = 2n; + + data = { + dailyLimit, + dailyExitCount, + currentDay, + }; + + await exitLimitStorage.setStorageExitRequestLimit(data); + + const result = await exitLimitStorage.getStorageExitRequestLimit(); + expect(result.dailyLimit).to.equal(dailyLimit); + expect(result.dailyExitCount).to.equal(dailyExitCount); + expect(result.currentDay).to.equal(currentDay); + }); + }); + + context("ExitLimitUtils", () => { + context("consumeLimit", () => { + it("should allow unlimited exits when dailyLimit was not set", async () => { + await exitLimit.harness_setState(0, 0, 0); + const result = await exitLimit.consumeLimit(100, 0); + expect(result).to.equal(100); + }); + + it("should reset on new day and return requestsCount if under limit", async () => { + await exitLimit.harness_setState(10, 5, 1); + const result = await exitLimit.consumeLimit(8, 2n * BigInt(DAY)); + expect(result).to.equal(8); + }); + + it("should cap requests to remaining limit", async () => { + await exitLimit.harness_setState(10, 8, 0); + const result = await exitLimit.consumeLimit(5, 0); + expect(result).to.equal(2); + }); + + it("should revert if no limit left", async () => { + await exitLimit.harness_setState(5, 5, 0); + await expect(exitLimit.consumeLimit(1, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + }); + + it("should respect new dailyLimit after changing from unlimited", async () => { + await exitLimit.harness_setState(0, 50, 0); + const newData = await exitLimit.setExitDailyLimit(60, 0); + await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); + const result = await exitLimit.consumeLimit(11, 0); + expect(result).to.equal(10); + }); + + it("should revert if after new dailyLimit dailyEXitCount exceed accepted amount", async () => { + await exitLimit.harness_setState(0, 50, 0); + const newData = await exitLimit.setExitDailyLimit(50, 0); + await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); + await expect(exitLimit.consumeLimit(1, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + }); + + it("should process new amount of requests if new day come", async () => { + await exitLimit.harness_setState(0, 50, 1); + const newData = await exitLimit.setExitDailyLimit(50, BigInt(DAY)); + await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); + const result = await exitLimit.consumeLimit(1, 2n * BigInt(DAY)); + expect(result).to.equal(1); + }); + }); + + context("checkLimit", () => { + it("should allow unlimited exits when dailyLimit was not set", async () => { + await exitLimit.harness_setState(0, 0, 0); + const tx = await exitLimit.checkLimit(100, 0); + await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + }); + + it("should reset on new day and pass checks if under limit", async () => { + await exitLimit.harness_setState(10, 5, 1); + const tx = await exitLimit.checkLimit(8, 2n * BigInt(DAY)); + await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + }); + + it("should revert if limit doesnt cover required amount of requests", async () => { + await exitLimit.harness_setState(10, 8, 0); + await expect(exitLimit.checkLimit(5, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + }); + + it("should revert if no limit left", async () => { + await exitLimit.harness_setState(5, 5, 0); + await expect(exitLimit.checkLimit(1, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + }); + + it("should respect new dailyLimit after changing from unlimited", async () => { + await exitLimit.harness_setState(0, 50, 0); + const newData = await exitLimit.setExitDailyLimit(60, 0); + await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); + const tx = await exitLimit.checkLimit(10, 0); + await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + }); + + it("should revert if after new dailyLimit dailyEXitCount exceed accepted amount", async () => { + await exitLimit.harness_setState(0, 50, 0); + const newData = await exitLimit.setExitDailyLimit(60, 0); + await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); + await expect(exitLimit.checkLimit(11, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + }); + + it("should process new amount of requests if new day come", async () => { + await exitLimit.harness_setState(0, 50, 1); + const newData = await exitLimit.setExitDailyLimit(50, BigInt(DAY)); + await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); + const tx = await exitLimit.checkLimit(1, 2n * BigInt(DAY)); + await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + }); + }); + + context("updateRequestsCounter", () => { + it("should revert if newCount exceed uint96", async () => { + await exitLimit.harness_setState(0, 0, 0); + + await expect(exitLimit.updateRequestsCounter(2n ** 96n, 2n * BigInt(DAY))).to.be.revertedWith( + "TOO_LARGE_REQUESTS_COUNT_LIMIT", + ); + }); + + it("should reset dailyExitLimit and currentDay on new day", async () => { + await exitLimit.harness_setState(0, 50, 1); + + const result = await exitLimit.updateRequestsCounter(30, 2n * BigInt(DAY)); + + expect(result.currentDay).to.equal(2); + expect(result.dailyExitCount).to.equal(30); + expect(result.dailyLimit).to.equal(0); + }); + + it("should revert if new dailyExitCount exceed uint96", async () => { + await exitLimit.harness_setState(0, 100, 0); + + await expect(exitLimit.updateRequestsCounter(2n ** 96n - 1n, 0)).to.be.revertedWith( + "DAILY_EXIT_COUNT_OVERFLOW", + ); + }); + + it("should revert if new dailyExitCount more than dailyLimit", async () => { + await exitLimit.harness_setState(100, 50, 0); + + await expect(exitLimit.updateRequestsCounter(2n ** 96n - 1n, 0)).to.be.revertedWith( + "DAILY_EXIT_COUNT_OVERFLOW", + ); + }); + + it("should accumulate dailyExitCount even if requests are unlimited", async () => { + await exitLimit.harness_setState(0, 50, 1); + + const result = await exitLimit.updateRequestsCounter(30, BigInt(DAY)); + expect(result.currentDay).to.equal(1); + expect(result.dailyExitCount).to.equal(80); + expect(result.dailyLimit).to.equal(0); + }); + }); + }); +}); From 3b562b6ae382456b3adf70855104c4e3aa26d132 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 6 May 2025 11:45:12 +0200 Subject: [PATCH 119/405] feat: add validator exit process status tracking and related functions in NodeOperatorsRegistry --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 41 ++++++++++- test/0.4.24/nor/nor.exit.manager.test.ts | 72 +++++++++---------- 2 files changed, 76 insertions(+), 37 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 00d3f2ccb3..776d43f61a 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -84,6 +84,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Distributed // Reward distributed among operators } + /// @notice Enum to represent the state of a validator exit process + enum ValidatorExitProcessStatus { + NotProcessed, // Default state, validator exit not yet started + Reported, // Exit delay reported but not yet triggered + Triggered // Exit has been triggered + } + // // ACL // @@ -234,6 +241,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { mapping(uint256 => NodeOperator) internal _nodeOperators; NodeOperatorSummary internal _nodeOperatorSummary; + /// @dev Mapping of Node Operator exit delay keys + mapping(bytes32 => ValidatorExitProcessStatus) internal _validatorExitProcessedKeys; + // // METHODS // @@ -1052,6 +1062,24 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } + function _getValidatorExitingKeyProcessStatus(bytes32 _keyHash) internal view returns (ValidatorExitProcessStatus) { + return _validatorExitProcessedKeys[_keyHash]; + } + + function _markValidatorExitingKeyAsReported(bytes32 _keyHash) internal { + // Require that key is currently NotProcessed + require(_validatorExitProcessedKeys[_keyHash] == ValidatorExitProcessStatus.NotProcessed, + "VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + _validatorExitProcessedKeys[_keyHash] = ValidatorExitProcessStatus.Reported; + } + + function _markValidatorExitingKeyAsTriggered(bytes32 _keyHash) internal { + // Require that key is currently Reported + require(_validatorExitProcessedKeys[_keyHash] == ValidatorExitProcessStatus.Reported, + "VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + _validatorExitProcessedKeys[_keyHash] = ValidatorExitProcessStatus.Triggered; + } + function exitDeadlineThreshold() public view returns (uint256) { return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); } @@ -1071,15 +1099,23 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { ) external { _auth(STAKING_ROUTER_ROLE); + bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + _markValidatorExitingKeyAsTriggered(processedKeyHash); + emit ValidatorExitTriggered(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } function isValidatorExitDelayPenaltyApplicable( uint256, // _nodeOperatorId uint256, // _proofSlotTimestamp - bytes, // _publicKey + bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool) { + bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + ValidatorExitProcessStatus status = _getValidatorExitingKeyProcessStatus(processedKeyHash); + if (status != ValidatorExitProcessStatus.NotProcessed) { + return false; + } return _eligibleToExitInSec >= exitDeadlineThreshold(); } @@ -1095,6 +1131,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Check if exit delay exceeds the threshold require(_eligibleToExitInSec >= exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); + bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + _markValidatorExitingKeyAsReported(processedKeyHash); + emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 15edb33ccd..b7bcb91b68 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -52,7 +52,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { stuckValidatorsCount: 0n, refundedValidatorsCount: 0n, stuckPenaltyEndAt: 0n, - } + }, ]; const moduleType = encodeBytes32String("curated-onchain-v1"); @@ -65,8 +65,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { const exitType = 1n; before(async () => { - [deployer, user, stakingRouter, nodeOperatorsManager, signingKeysManager, stranger] = - await ethers.getSigners(); + [deployer, user, stakingRouter, nodeOperatorsManager, signingKeysManager, stranger] = await ethers.getSigners(); ({ lido, dao, acl } = await deployLidoDao({ rootAccount: deployer, @@ -125,12 +124,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .false; await expect( - nor.connect(stranger).reportValidatorExitDelay( - firstNodeOperatorId, - proofSlotTimestamp, - testPublicKey, - eligibleToExitInSec - ) + nor + .connect(stranger) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec), ).to.be.revertedWith("APP_AUTH_FAILED"); }); @@ -139,12 +135,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .to.be.true; await expect( - nor.connect(stakingRouter).reportValidatorExitDelay( - firstNodeOperatorId, - proofSlotTimestamp, - testPublicKey, - eligibleToExitInSec - ) + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec), ) .and.to.emit(nor, "ValidatorExitStatusUpdated") .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); @@ -152,12 +145,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("reverts when public key is empty", async () => { await expect( - nor.connect(stakingRouter).reportValidatorExitDelay( - firstNodeOperatorId, - proofSlotTimestamp, - "0x", - eligibleToExitInSec - ) + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, "0x", eligibleToExitInSec), ).to.be.revertedWith("INVALID_PUBLIC_KEY"); }); }); @@ -168,12 +158,17 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .false; await expect( - nor.connect(stranger).onValidatorExitTriggered( - firstNodeOperatorId, - testPublicKey, - withdrawalRequestPaidFee, - exitType - ) + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec), + ) + .and.to.emit(nor, "ValidatorExitStatusUpdated") + .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); + + await expect( + nor + .connect(stranger) + .onValidatorExitTriggered(firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, exitType), ).to.be.revertedWith("APP_AUTH_FAILED"); }); @@ -182,12 +177,17 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .to.be.true; await expect( - nor.connect(stakingRouter).onValidatorExitTriggered( - firstNodeOperatorId, - testPublicKey, - withdrawalRequestPaidFee, - exitType - ) + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec), + ) + .and.to.emit(nor, "ValidatorExitStatusUpdated") + .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); + + await expect( + nor + .connect(stakingRouter) + .onValidatorExitTriggered(firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, exitType), ) .to.emit(nor, "ValidatorExitTriggered") .withArgs(firstNodeOperatorId, testPublicKey, withdrawalRequestPaidFee, exitType); @@ -207,7 +207,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { firstNodeOperatorId, proofSlotTimestamp, testPublicKey, - 172800n // Equal to the threshold + 172800n, // Equal to the threshold ); expect(shouldPenalize).to.be.true; @@ -215,7 +215,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { firstNodeOperatorId, proofSlotTimestamp, testPublicKey, - 172801n // Greater than the threshold + 172801n, // Greater than the threshold ); expect(shouldPenalizeMore).to.be.true; }); @@ -225,7 +225,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { firstNodeOperatorId, proofSlotTimestamp, testPublicKey, - 1n // Less than the threshold + 1n, // Less than the threshold ); expect(shouldPenalize).to.be.false; }); From 390dcd3b4b831ea50aa67cc3e40920c0c0b45570 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 7 May 2025 12:46:26 +0200 Subject: [PATCH 120/405] refactor: simplify validator exit process status handling by removing enum and using boolean mapping --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 776d43f61a..962db16761 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -84,13 +84,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { Distributed // Reward distributed among operators } - /// @notice Enum to represent the state of a validator exit process - enum ValidatorExitProcessStatus { - NotProcessed, // Default state, validator exit not yet started - Reported, // Exit delay reported but not yet triggered - Triggered // Exit has been triggered - } - // // ACL // @@ -242,7 +235,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { NodeOperatorSummary internal _nodeOperatorSummary; /// @dev Mapping of Node Operator exit delay keys - mapping(bytes32 => ValidatorExitProcessStatus) internal _validatorExitProcessedKeys; + mapping(bytes32 => bool) internal _validatorExitProcessedKeys; // // METHODS @@ -1062,22 +1055,15 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } - function _getValidatorExitingKeyProcessStatus(bytes32 _keyHash) internal view returns (ValidatorExitProcessStatus) { + function _isValidatorExitingKeyReported(bytes32 _keyHash) internal view returns (bool) { return _validatorExitProcessedKeys[_keyHash]; } function _markValidatorExitingKeyAsReported(bytes32 _keyHash) internal { // Require that key is currently NotProcessed - require(_validatorExitProcessedKeys[_keyHash] == ValidatorExitProcessStatus.NotProcessed, + require(_validatorExitProcessedKeys[_keyHash] == false, "VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); - _validatorExitProcessedKeys[_keyHash] = ValidatorExitProcessStatus.Reported; - } - - function _markValidatorExitingKeyAsTriggered(bytes32 _keyHash) internal { - // Require that key is currently Reported - require(_validatorExitProcessedKeys[_keyHash] == ValidatorExitProcessStatus.Reported, - "VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); - _validatorExitProcessedKeys[_keyHash] = ValidatorExitProcessStatus.Triggered; + _validatorExitProcessedKeys[_keyHash] = true; } function exitDeadlineThreshold() public view returns (uint256) { @@ -1099,9 +1085,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { ) external { _auth(STAKING_ROUTER_ROLE); - bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); - _markValidatorExitingKeyAsTriggered(processedKeyHash); - emit ValidatorExitTriggered(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } @@ -1112,8 +1095,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 _eligibleToExitInSec ) external view returns (bool) { bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); - ValidatorExitProcessStatus status = _getValidatorExitingKeyProcessStatus(processedKeyHash); - if (status != ValidatorExitProcessStatus.NotProcessed) { + // Check if the key is already reported + if (_isValidatorExitingKeyReported(processedKeyHash)) { return false; } return _eligibleToExitInSec >= exitDeadlineThreshold(); From b107c721530dc15ff4d242158e8039f89bcc1082 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 7 May 2025 12:55:36 +0200 Subject: [PATCH 121/405] feat: add natspec --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 962db16761..197240fa4c 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1066,10 +1066,14 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _validatorExitProcessedKeys[_keyHash] = true; } + /// @notice Returns the number of seconds after which a validator is considered late. + /// @return uint256 The exit deadline threshold in seconds for all node operators. function exitDeadlineThreshold() public view returns (uint256) { return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); } + /// @notice Sets the number of seconds after which a validator is considered late for exit. + /// @param _threshold The new exit deadline threshold in seconds. function setExitDeadlineThreshold(uint256 _threshold) external { _auth(MANAGE_NODE_OPERATOR_ROLE); require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); @@ -1077,6 +1081,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit ExitDeadlineThresholdChanged(_threshold); } + /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. + /// @dev This function is called by the StakingRouter when a validator is exited using the triggerable + /// exit request on the Execution Layer (EL). + /// @param _nodeOperatorId The ID of the node operator. + /// @param _publicKey The public key of the validator being reported. + /// @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the Execution Layer (EL). + /// @param _exitType The type of exit being performed. function onValidatorExitTriggered( uint256 _nodeOperatorId, bytes _publicKey, @@ -1088,6 +1099,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { emit ValidatorExitTriggered(_nodeOperatorId, _publicKey, _withdrawalRequestPaidFee, _exitType); } + /// @notice Determines whether a validator's exit status should be updated and will have an effect on the Node Operator. + /// @param _publicKey The public key of the validator. + /// @param _eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. + /// @return True if the validator has exceeded the exit deadline threshold and hasn't been reported yet. function isValidatorExitDelayPenaltyApplicable( uint256, // _nodeOperatorId uint256, // _proofSlotTimestamp @@ -1102,6 +1117,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return _eligibleToExitInSec >= exitDeadlineThreshold(); } + /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. + /// @dev This function is called by the StakingRouter to report the current exit-related status of a validator + /// belonging to a specific node operator. It marks the validator as processed to avoid duplicate reports. + /// @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. + /// @param _publicKey The public key of the validator being reported. + /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, From 0f699059455d4bde57170f9d7b3ab8319dca1cde Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 7 May 2025 15:36:29 +0200 Subject: [PATCH 122/405] fix: update key hashing method to use direct input for improved efficiency --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 197240fa4c..33dba70a32 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1109,7 +1109,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool) { - bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + bytes32 processedKeyHash = keccak256(_publicKey); // Check if the key is already reported if (_isValidatorExitingKeyReported(processedKeyHash)) { return false; @@ -1136,7 +1136,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Check if exit delay exceeds the threshold require(_eligibleToExitInSec >= exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); - bytes32 processedKeyHash = keccak256(abi.encode(_publicKey)); + bytes32 processedKeyHash = keccak256(_publicKey); _markValidatorExitingKeyAsReported(processedKeyHash); emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); From 9276c5a256e6617ce109f2601074782f93e90282 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 12 May 2025 02:24:02 +0400 Subject: [PATCH 123/405] feat: added TriggerableWithdrawalGateway contract --- contracts/0.8.9/LidoLocator.sol | 3 + .../0.8.9/TriggerableWithdrawalGateway.sol | 241 ++++++++++++++ ...atorExitBus.sol => IValidatorsExitBus.sol} | 10 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 309 +++++++----------- contracts/common/interfaces/ILidoLocator.sol | 1 + .../LidoLocator__MockForSanityChecker.sol | 30 +- ...orVEB.sol => StakingRouter_MockForTWG.sol} | 2 +- ...iggerableWithdrawalGateway__MockForVEB.sol | 13 + ...ebo.sol => WithdrawalValut_MockForTWG.sol} | 2 +- test/0.8.9/lidoLocator.test.ts | 1 + ...ator-exit-bus-oracle.accessControl.test.ts | 6 +- .../validator-exit-bus-oracle.deploy.test.ts | 11 +- .../validator-exit-bus-oracle.gas.test.ts | 5 +- ...alidator-exit-bus-oracle.happyPath.test.ts | 5 +- ...bus-oracle.submitExitRequestsData.test.ts} | 35 +- ...r-exit-bus-oracle.submitReportData.test.ts | 14 +- ...dator-exit-bus-oracle.triggerExits.test.ts | 212 ++---------- ...it-bus-oracle.triggerExitsDirectly.test.ts | 188 ----------- .../oracle/validator-exit-bus.helpers.test.ts | 2 +- .../oracleReportSanityChecker.misc.test.ts | 1 + ...eportSanityChecker.negative-rebase.test.ts | 1 + ...awalGateway.triggerFullWithdrawals.test.ts | 124 +++++++ test/deploy/locator.ts | 2 + test/deploy/validatorExitBusOracle.ts | 26 +- 24 files changed, 575 insertions(+), 669 deletions(-) create mode 100644 contracts/0.8.9/TriggerableWithdrawalGateway.sol rename contracts/0.8.9/interfaces/{IValidatorExitBus.sol => IValidatorsExitBus.sol} (82%) rename test/0.8.9/contracts/{StakingRouter_MockForVEB.sol => StakingRouter_MockForTWG.sol} (94%) create mode 100644 test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol rename test/0.8.9/contracts/{WithdrawalValut_MockForVebo.sol => WithdrawalValut_MockForTWG.sol} (89%) rename test/0.8.9/oracle/{validator-exit-bus-oracle.emitExitEvents.test.ts => validator-exit-bus-oracle.submitExitRequestsData.test.ts} (89%) delete mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts create mode 100644 test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index c8c02026a5..9c20bd10b6 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -29,6 +29,7 @@ contract LidoLocator is ILidoLocator { address withdrawalVault; address oracleDaemonConfig; address validatorExitVerifier; + address triggerableWithdrawalGateway; } error ZeroAddress(); @@ -48,6 +49,7 @@ contract LidoLocator is ILidoLocator { address public immutable withdrawalVault; address public immutable oracleDaemonConfig; address public immutable validatorExitVerifier; + address public immutable triggerableWithdrawalGateway; /** * @notice declare service locations @@ -70,6 +72,7 @@ contract LidoLocator is ILidoLocator { withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); validatorExitVerifier = _assertNonZero(_config.validatorExitVerifier); + triggerableWithdrawalGateway = _assertNonZero(_config.triggerableWithdrawalGateway); } function coreComponents() external view returns( diff --git a/contracts/0.8.9/TriggerableWithdrawalGateway.sol b/contracts/0.8.9/TriggerableWithdrawalGateway.sol new file mode 100644 index 0000000000..9007e23eb4 --- /dev/null +++ b/contracts/0.8.9/TriggerableWithdrawalGateway.sol @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.9; + +import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; +import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; + +interface IWithdrawalVault { + function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; + + function getWithdrawalRequestFee() external view returns (uint256); +} + +interface IStakingRouter { + function onValidatorExitTriggered( + uint256 _stakingModuleId, + uint256 _nodeOperatorId, + bytes calldata _publicKey, + uint256 _withdrawalRequestPaidFee, + uint256 _exitType + ) external; +} + +/** + * @title TriggerableWithdrawalGateway + * @notice TriggerableWithdrawalGateway contract is one entrypoint for all triggerable withdrawal requests (TWRs) in protocol. + * This contract is responsible for limiting TWRs, checking ADD_FULL_WITHDRAWAL_REQUEST_ROLE role before it gets to Withdrawal Vault. + */ +contract TriggerableWithdrawalGateway is AccessControlEnumerable { + /// @dev Errors + /** + * @notice Thrown when an invalid zero value is passed + * @param name Name of the argument that was zero + */ + error ZeroArgument(string name); + /** + * @notice Thrown when attempting to set the admin address to zero + */ + error AdminCannotBeZero(); + /** + * @notice Thrown when exit request has wrong length + */ + error InvalidRequestsDataLength(); + /** + * @notice Thrown when a withdrawal fee insufficient + * @param feeRequired Amount of fee required to cover withdrawal request + * @param passedValue Amount of fee sent to cover withdrawal request + */ + error InsufficientWithdrawalFee(uint256 feeRequired, uint256 passedValue); + /** + * @notice Thrown when a withdrawal fee refund failed + */ + error TriggerableWithdrawalFeeRefundFailed(); + + /** + * @notice Emitted when someone with ADD_FULL_WITHDRAWAL_REQUEST_ROLE role request to process TWR. + * @param stakingModuleId Module id + * @param nodeOperatorId Operator id + * @param validatorPubkey Validator public key + * @param timestamp Block timestamp + */ + event TriggerableExitRequest( + uint256 indexed stakingModuleId, + uint256 indexed nodeOperatorId, + bytes validatorPubkey, + uint256 timestamp + ); + + struct ValidatorData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; + } + + bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); + + /// Length in bytes of packed triggerable exit request + uint256 internal constant PACKED_EXIT_REQUEST_LENGTH = 56; + uint256 internal constant PUBLIC_KEY_LENGTH = 48; + + ILidoLocator internal immutable LOCATOR; + + /// @dev Ensures the contract’s ETH balance is unchanged. + modifier preservesEthBalance() { + uint256 balanceBeforeCall = address(this).balance - msg.value; + _; + assert(address(this).balance == balanceBeforeCall); + } + + constructor(address lidoLocator) { + LOCATOR = ILidoLocator(lidoLocator); + } + + function initialize(address admin) external { + if (admin == address(0)) revert AdminCannotBeZero(); + _setupRole(DEFAULT_ADMIN_ROLE, admin); + } + + /** + * @dev Submits Triggerable Withdrawal Requests to the Withdrawal Vault as full withdrawal requests + * for the specified validator public keys. + * + * @param triggerableExitData A packed byte array containing one or more 56-byte items, each representing: + * MSB <-------------------------------------------------- LSB + * | 3 bytes | 5 bytes | 48 bytes | + * | stakingModuleId | nodeOperatorId | validatorPubkey | + * + * @param refundRecipient The address that will receive any excess ETH sent for fees. + * @param exitType A parameter indicating the type of exit, passed to the Staking Module. + * + * Emits `TriggerableExitRequest` event for each validator in list. + * + * @notice Reverts if: + * - The caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE` + * - The total fee value sent is insufficient to cover all provided TW requests. + * - There is not enough limit quota left in the current frame to process all requests. + */ + function triggerFullWithdrawals( + bytes calldata triggerableExitData, + address refundRecipient, + uint8 exitType + ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + if (msg.value == 0) revert ZeroArgument("msg.value"); + + // If the refund recipient is not set, use the sender as the refund recipient + if (refundRecipient == address(0)) { + refundRecipient = msg.sender; + } + + _checkExitRequestData(triggerableExitData); + + uint256 requestsCount = triggerableExitData.length / PACKED_EXIT_REQUEST_LENGTH; + uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); + + _checkFee(requestsCount, withdrawalFee); + + // TODO: this method will be covered with limits + bytes memory pubkeys = new bytes(requestsCount * PUBLIC_KEY_LENGTH); + + for (uint256 i = 0; i < requestsCount; ++i) { + ValidatorData memory data = _parseExitRequestData(triggerableExitData, i); + + _copyPubkey(data.pubkey, pubkeys, i); + + // TODO: is it correct to send here withdrawalFee? + _notifyStakingModule(data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalFee, exitType); + + emit TriggerableExitRequest(data.stakingModuleId, data.nodeOperatorId, data.pubkey, block.timestamp); + } + + _addWithdrawalRequest(requestsCount, withdrawalFee, pubkeys, refundRecipient); + } + + /// Internal functions + + function _checkExitRequestData(bytes calldata triggerableExitData) internal pure { + if (triggerableExitData.length % PACKED_EXIT_REQUEST_LENGTH != 0) { + revert InvalidRequestsDataLength(); + } + } + + function _checkFee(uint256 requestsCount, uint256 withdrawalFee) internal { + if (msg.value < requestsCount * withdrawalFee) { + revert InsufficientWithdrawalFee(requestsCount * withdrawalFee, msg.value); + } + } + + function _parseExitRequestData( + bytes calldata request, + uint256 requestNumber + ) internal pure returns (ValidatorData memory data) { + uint256 dataWithoutPubkey; + uint256 offset; + bytes calldata pubkey; + + assembly { + offset := add(request.offset, mul(requestNumber, PACKED_EXIT_REQUEST_LENGTH)) + dataWithoutPubkey := shr(192, calldataload(offset)) + pubkey.length := 48 + // 8 bytes = 3 bytes (module id) + 5 bytes (operator id) + pubkey.offset := add(offset, 8) + } + + data.nodeOperatorId = uint40(dataWithoutPubkey); + data.stakingModuleId = uint24(dataWithoutPubkey >> 40); + data.pubkey = pubkey; + } + + function _copyPubkey(bytes memory pubkey, bytes memory pubkeys, uint256 index) internal pure { + assembly { + let pubkeyMemPtr := add(pubkey, 32) + let pubkeysOffset := add(pubkeys, add(32, mul(PUBLIC_KEY_LENGTH, index))) + mstore(pubkeysOffset, mload(pubkeyMemPtr)) + mstore(add(pubkeysOffset, 32), mload(add(pubkeyMemPtr, 32))) + } + } + + function _notifyStakingModule( + uint256 stakingModuleId, + uint256 nodeOperatorId, + bytes memory pubkey, + uint256 withdrawalRequestPaidFee, + uint8 exitType + ) internal { + IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( + stakingModuleId, + nodeOperatorId, + pubkey, + withdrawalRequestPaidFee, + exitType + ); + } + + function _addWithdrawalRequest( + uint256 requestsCount, + uint256 withdrawalFee, + bytes memory pubkeys, + address refundRecipient + ) internal { + IWithdrawalVault(LOCATOR.withdrawalVault()).addWithdrawalRequests{value: requestsCount * withdrawalFee}( + pubkeys, + new uint64[](requestsCount) + ); + + _refundFee(requestsCount * withdrawalFee, refundRecipient); + } + + function _refundFee(uint256 fee, address recipient) internal returns (uint256) { + uint256 refund = msg.value - fee; + + if (refund > 0) { + (bool success, ) = recipient.call{value: refund}(""); + + if (!success) { + revert TriggerableWithdrawalFeeRefundFailed(); + } + } + + return refund; + } +} diff --git a/contracts/0.8.9/interfaces/IValidatorExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol similarity index 82% rename from contracts/0.8.9/interfaces/IValidatorExitBus.sol rename to contracts/0.8.9/interfaces/IValidatorsExitBus.sol index a0b0daae68..bc43fd90d2 100644 --- a/contracts/0.8.9/interfaces/IValidatorExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -20,9 +20,9 @@ interface IValidatorsExitBus { uint256 timestamp; } - function submitReportHash(bytes32 exitReportHash) external; + function submitExitRequestsHash(bytes32 exitReportHash) external; - function emitExitEvents(ExitRequestData calldata request) external; + function submitExitRequestsData(ExitRequestData calldata request) external; function triggerExits( ExitRequestData calldata request, @@ -31,12 +31,6 @@ interface IValidatorsExitBus { uint8 exitType ) external payable; - function triggerExitsDirectly( - DirectExitData calldata exitData, - address refundRecipient, - uint8 exitType - ) external payable; - function setExitRequestLimit(uint256 exitsDailyLimit, uint256 twExitsDailyLimit) external; function getExitRequestsDeliveryHistory( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 950f0e7466..a3baf94b80 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -8,22 +8,15 @@ import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; import {Versioned} from "../utils/Versioned.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; import {PausableUntil} from "../utils/PausableUntil.sol"; -import {IValidatorsExitBus} from "../interfaces/IValidatorExitBus.sol"; +import {IValidatorsExitBus} from "../interfaces/IValidatorsExitBus.sol"; +import "hardhat/console.sol"; -interface IWithdrawalVault { - function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; - - function getWithdrawalRequestFee() external view returns (uint256); -} - -interface IStakingRouter { - function onValidatorExitTriggered( - uint256 _stakingModuleId, - uint256 _nodeOperatorId, - bytes calldata _publicKey, - uint256 _withdrawalRequestPaidFee, - uint256 _exitType - ) external; +interface ITriggerableWithdrawalGateway { + function triggerFullWithdrawals( + bytes calldata triggerableExitData, + address refundRecipient, + uint8 exitType + ) external payable; } contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { @@ -67,22 +60,19 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /** * Thrown when there are attempt to send exit events for request that was not submitted earlier by trusted entities */ - error ExitHashWasNotSubmitted(); + error ExitHashNotSubmitted(); /** - * TODO: do we need this error ? - * @notice Throw when in emitExitEvents all requests were already delivered + * Thrown when there are attempt to store exit hash that was already submitted */ - error RequestsAlreadyDelivered(); - - error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); + error ExitHashAlreadySubmitted(); /** - * @notice Thrown when a withdrawal fee insufficient - * @param feeRequired Amount of fee required to cover withdrawal request - * @param passedValue Amount of fee sent to cover withdrawal request + * @notice Throw when in submitExitRequestsData all requests were already delivered */ - error InsufficientWithdrawalFee(uint256 feeRequired, uint256 passedValue); + error RequestsAlreadyDelivered(); + + error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); @@ -92,7 +82,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error TriggerableWithdrawalFeeRefundFailed(); /// @dev Events - event MadeRefund(address sender, uint256 refundValue); // maybe we dont need it event StoredExitRequestHash(bytes32 exitRequestHash); event ValidatorExitRequest( uint256 indexed stakingModuleId, @@ -140,6 +129,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 internal constant PUBLIC_KEY_LENGTH = 48; + uint256 internal constant PACKED_TWG_EXIT_REQUEST_LENGTH = 56; + ILidoLocator internal immutable LOCATOR; /// @notice The list format of the validator exit requests data. Used when all @@ -163,37 +154,54 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.exitDailyLimit"); - bytes32 public constant TW_EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.twExitDailyLimit"); - - /// @dev Ensures the contract’s ETH balance is unchanged. - modifier preservesEthBalance() { - uint256 balanceBeforeCall = address(this).balance - msg.value; - _; - assert(address(this).balance == balanceBeforeCall); - } constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); } - /// @notice Method for submitting request hash by trusted entities - /// @param exitReportHash Request hash - /// @dev After request was stored anyone can deliver it via emitExitEvents method below - function submitReportHash(bytes32 exitReportHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { + /** + * @notice Submit a hash of the exit requests data. + * + * @dev Reverts if: + * - The contract is paused. + * - The caller does not have the `SUBMIT_REPORT_HASH_ROLE`. + * - The hash has already been submitted. + * + * Emits `RequestsHashSubmitted` event; + * + * @param exitRequestsHash - keccak256 hash of the encoded validators list + */ + function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { + RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; + _checkExitNotSubmitted(requestStatus); + uint256 contractVersion = getContractVersion(); - _storeExitRequestHash(exitReportHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); + _storeExitRequestHash(exitRequestsHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); } - /// @notice Method to emit exit events by providing report data, the hash of which was previously stored - /// @param request Exit request data struct - function emitExitEvents(ExitRequestData calldata request) external whenResumed { + /** + * @notice Method for submitting exit requests data + * + * @dev Reverts if: + * - The contract is paused. + * - The keccak256 hash of `requestsData` does not exist in storage (i.e., was not submitted). + * - The provided Exit Requests Data has already been fully unpacked. + * - The contract version does not match the version at the time of report submission. + * - The data format is not supported. + * - There is no remaining quota available for the current limits. + * + * Emits `ValidatorExitRequest` events; + * + * @param request - The exit requests structure. + */ + function submitExitRequestsData(ExitRequestData calldata request) external whenResumed { bytes calldata data = request.data; RequestStatus storage requestStatus = _storageExitRequestsHashes()[ keccak256(abi.encode(data, request.dataFormat)) ]; - _checkExitWasSubmitted(requestStatus); + _checkExitSubmitted(requestStatus); _checkExitRequestData(request); _checkContractVersion(requestStatus.contractVersion); @@ -230,20 +238,25 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa requestStatus.deliveredItemsCount += requestsToDeliver; } - /// @notice Triggers exits on the EL via the Withdrawal Vault contract - /// @param request Exit request data struct - /// @param keyIndexes Array of indexes of requests in request.data - /// @param refundRecipient Address to return extra fee on TW (eip-7002) exit - /// @param exitType type of request. 0 - non-refundable, 1 - require refund - /// @dev This function verifies that the hash of the provided exit request data exists in storage - // and ensures that the events for the requests specified in the `keyIndexes` array have already been delivered. - // Verify that keyIndexes amount fits within the limits + /** + * @notice Submits Triggerable Withdrawal Requests to the Triggerable Withdrawals Gateway. + * + * @param requestsData The report data previously unpacked and emitted by the VEB. + * @param keyIndexes Array of indexes pointing to validators in `requestsData.data` + * to be exited via TWR. + * @param refundRecipient Address to return extra fee on TW (eip-7002) exit + * @param exitType type of request. 0 - non-refundable, 1 - require refund + * + * @dev Reverts if: + * - The hash of `requestsData` was not previously submitted in the VEB. + * - Any of the provided `keyIndexes` refers to a validator that was not yet unpacked (i.e., exit requiest not emitted). + */ function triggerExits( - ExitRequestData calldata request, + ExitRequestData calldata requestsData, uint256[] calldata keyIndexes, address refundRecipient, uint8 exitType - ) external payable whenResumed preservesEthBalance { + ) external payable { if (msg.value == 0) revert ZeroArgument("msg.value"); // If the refund recipient is not set, use the sender as the refund recipient @@ -253,27 +266,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa // bytes calldata data = request.data; RequestStatus storage requestStatus = _storageExitRequestsHashes()[ - keccak256(abi.encode(request.data, request.dataFormat)) + keccak256(abi.encode(requestsData.data, requestsData.dataFormat)) ]; - _checkExitWasSubmitted(requestStatus); - _checkExitRequestData(request); + _checkExitSubmitted(requestStatus); + _checkExitRequestData(requestsData); _checkContractVersion(requestStatus.contractVersion); - uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); - - if (msg.value < keyIndexes.length * withdrawalFee) { - revert InsufficientWithdrawalFee(keyIndexes.length * withdrawalFee, msg.value); - } - - ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - exitRequestLimitData.checkLimit(keyIndexes.length, _getTimestamp()); - TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(keyIndexes.length, _getTimestamp()) - ); - - bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH); - bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); + bytes memory exits = new bytes(keyIndexes.length * PACKED_TWG_EXIT_REQUEST_LENGTH); for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { @@ -284,101 +284,17 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyWasNotDelivered(keyIndexes[i], requestStatus.deliveredItemsCount - 1); } - ValidatorData memory validatorData = _getValidatorData(request.data, keyIndexes[i]); + ValidatorData memory validatorData = _getValidatorData(requestsData.data, keyIndexes[i]); if (validatorData.moduleId == 0) revert InvalidRequestsData(); - pubkey = validatorData.pubkey; - - assembly { - let pubkeyMemPtr := add(pubkey, 32) - let dest := add(pubkeys, add(32, mul(PUBLIC_KEY_LENGTH, i))) - mstore(dest, mload(pubkeyMemPtr)) - mstore(add(dest, 32), mload(add(pubkeyMemPtr, 32))) - } - - IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( - validatorData.moduleId, - validatorData.nodeOpId, - pubkey, - withdrawalFee, - exitType - ); - } - - IWithdrawalVault(LOCATOR.withdrawalVault()).addWithdrawalRequests{value: keyIndexes.length * withdrawalFee}( - pubkeys, - new uint64[](keyIndexes.length) - ); - - _refundFee(keyIndexes.length * withdrawalFee, refundRecipient); - } - - /// @notice Directly emit exit events and request validators through the TW to exit them without delivering hashes and any proving - /// @param exitData Direct exit request data struct - /// @param refundRecipient Address to return extra fee on TW (eip-7002) exit - /// @param exitType type of request. 0 - non-refundable, 1 - require refund - /// @dev Verify that requests amount fits within the limits - function triggerExitsDirectly( - DirectExitData calldata exitData, - address refundRecipient, - uint8 exitType - ) external payable whenResumed onlyRole(DIRECT_EXIT_ROLE) preservesEthBalance { - if (msg.value == 0) revert ZeroArgument("msg.value"); - - // If the refund recipient is not set, use the sender as the refund recipient - if (refundRecipient == address(0)) { - refundRecipient = msg.sender; - } - - if (exitData.validatorsPubkeys.length == 0) { - revert ZeroArgument("exitData.validatorsPubkeys"); - } - if (exitData.validatorsPubkeys.length % PUBLIC_KEY_LENGTH != 0) { - revert InvalidPubkeysArray(); + _copyValidatorData(validatorData, exits, i); } - uint256 requestsCount = exitData.validatorsPubkeys.length / PUBLIC_KEY_LENGTH; - uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); - - if (msg.value < withdrawalFee * requestsCount) { - revert InsufficientWithdrawalFee(withdrawalFee * requestsCount, msg.value); - } - - ExitRequestLimitData memory exitRequestLimitData = TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - exitRequestLimitData.checkLimit(requestsCount, _getTimestamp()); - TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(requestsCount, _getTimestamp()) + ITriggerableWithdrawalGateway(LOCATOR.triggerableWithdrawalGateway()).triggerFullWithdrawals{value: msg.value}( + exits, + refundRecipient, + exitType ); - - for (uint256 i = 0; i < requestsCount; i++) { - bytes memory pubkey = new bytes(PUBLIC_KEY_LENGTH); - - pubkey = _getPubkey(exitData.validatorsPubkeys, i); - - IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( - exitData.stakingModuleId, - exitData.nodeOperatorId, - pubkey, - withdrawalFee, - exitType - ); - - emit DirectExitRequest( - exitData.stakingModuleId, - exitData.nodeOperatorId, - pubkey, - _getTimestamp(), - refundRecipient - ); - } - - uint64[] memory amount = new uint64[](requestsCount); - IWithdrawalVault(LOCATOR.withdrawalVault()).addWithdrawalRequests{value: withdrawalFee * requestsCount}( - exitData.validatorsPubkeys, - amount - ); - - _refundFee(requestsCount * withdrawalFee, refundRecipient); } function setExitRequestLimit( @@ -395,19 +311,22 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(exitsDailyLimit, timestamp) ); - TW_EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - TW_EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(twExitsDailyLimit, timestamp) - ); - emit ExitRequestsLimitSet(exitsDailyLimit, twExitsDailyLimit); } + /** + * @notice Returns unpacking history and current status for specific exitRequestsData + * + * @dev Reverts if such exitRequestsHash was not submited. + * + * @param exitRequestsHash - The exit requests hash. + */ function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; - _checkExitWasSubmitted(requestStatus); + _checkExitSubmitted(requestStatus); return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); } @@ -481,26 +400,16 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _checkExitWasSubmitted(RequestStatus storage requestStatus) internal view { + function _checkExitSubmitted(RequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion == 0) { - revert ExitHashWasNotSubmitted(); + revert ExitHashNotSubmitted(); } } - function _refundFee(uint256 fee, address recipient) internal returns (uint256) { - uint256 refund = msg.value - fee; - - if (refund > 0) { - (bool success, ) = recipient.call{value: refund}(""); - - if (!success) { - revert TriggerableWithdrawalFeeRefundFailed(); - } - - emit MadeRefund(msg.sender, refund); + function _checkExitNotSubmitted(RequestStatus storage requestStatus) internal view { + if (requestStatus.contractVersion != 0) { + revert ExitHashAlreadySubmitted(); } - - return refund; } function _getTimestamp() internal view virtual returns (uint256) { @@ -573,22 +482,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa validatorData.pubkey = pubkey; } - /** - * @notice Method for reading public key value from pubkeys list - * @param pubkeys Concatenated list of pubkeys - * @param index index of pubkey in array above - * @return pubkey Validator public key - */ - function _getPubkey(bytes calldata pubkeys, uint256 index) internal pure returns (bytes memory pubkey) { - pubkey = new bytes(PUBLIC_KEY_LENGTH); - - assembly { - let offset := add(pubkeys.offset, mul(index, PUBLIC_KEY_LENGTH)) - let dest := add(pubkey, 0x20) - calldatacopy(dest, offset, PUBLIC_KEY_LENGTH) - } - } - /** * This method read report data (DATA_FORMAT=1) within a range * check dataWithoutPubkey <= lastDataWithoutPubkey needs to prevent duplicates @@ -644,6 +537,30 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } + /// Methods for working with TWG exit data type + /// | MSB <------------------------------------------------ LSB + /// | 3 bytes | 5 bytes | 48 bytes | + /// | stakingModuleId | nodeOperatorId | validatorPubkey | + + function _copyValidatorData( + ValidatorData memory validatorData, + bytes memory exitData, + uint256 index + ) internal pure { + uint256 nodeOpId = validatorData.nodeOpId; + uint256 moduleId = validatorData.moduleId; + bytes memory pubkey = validatorData.pubkey; + + assembly { + let exitDataOffset := add(exitData, add(32, mul(56, index))) + let id := or(shl(40, moduleId), nodeOpId) + mstore(exitDataOffset, shl(192, id)) + let pubkeyOffset := add(pubkey, 32) + mstore(add(exitDataOffset, 8), mload(pubkeyOffset)) + mstore(add(exitDataOffset, 40), mload(add(pubkeyOffset, 32))) + } + } + /// Storage helpers function _storageExitRequestsHashes() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { bytes32 position = EXIT_REQUESTS_HASHES_POSITION; diff --git a/contracts/common/interfaces/ILidoLocator.sol b/contracts/common/interfaces/ILidoLocator.sol index a2bdc764d9..f381f57ac7 100644 --- a/contracts/common/interfaces/ILidoLocator.sol +++ b/contracts/common/interfaces/ILidoLocator.sol @@ -20,6 +20,7 @@ interface ILidoLocator { function withdrawalVault() external view returns(address); function postTokenRebaseReceiver() external view returns(address); function oracleDaemonConfig() external view returns(address); + function triggerableWithdrawalGateway() external view returns (address); function coreComponents() external view returns( address elRewardsVault, address oracleReportSanityChecker, diff --git a/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol b/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol index 8aa909a619..a5f8620343 100644 --- a/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol +++ b/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol @@ -22,6 +22,7 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { address withdrawalVault; address postTokenRebaseReceiver; address oracleDaemonConfig; + address triggerableWithdrawalGateway; } address public immutable lido; @@ -38,10 +39,9 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { address public immutable withdrawalVault; address public immutable postTokenRebaseReceiver; address public immutable oracleDaemonConfig; + address public immutable triggerableWithdrawalGateway; - constructor ( - ContractAddresses memory addresses - ) { + constructor(ContractAddresses memory addresses) { lido = addresses.lido; depositSecurityModule = addresses.depositSecurityModule; elRewardsVault = addresses.elRewardsVault; @@ -56,28 +56,18 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { withdrawalVault = addresses.withdrawalVault; postTokenRebaseReceiver = addresses.postTokenRebaseReceiver; oracleDaemonConfig = addresses.oracleDaemonConfig; + triggerableWithdrawalGateway = addresses.triggerableWithdrawalGateway; } function coreComponents() external view returns (address, address, address, address, address, address) { - return ( - elRewardsVault, - oracleReportSanityChecker, - stakingRouter, - treasury, - withdrawalQueue, - withdrawalVault - ); + return (elRewardsVault, oracleReportSanityChecker, stakingRouter, treasury, withdrawalQueue, withdrawalVault); } - function oracleReportComponentsForLido() external view returns ( - address, - address, - address, - address, - address, - address, - address - ) { + function oracleReportComponentsForLido() + external + view + returns (address, address, address, address, address, address, address) + { return ( accountingOracle, elRewardsVault, diff --git a/test/0.8.9/contracts/StakingRouter_MockForVEB.sol b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol similarity index 94% rename from test/0.8.9/contracts/StakingRouter_MockForVEB.sol rename to test/0.8.9/contracts/StakingRouter_MockForTWG.sol index c009d4c7f4..734ba2db36 100644 --- a/test/0.8.9/contracts/StakingRouter_MockForVEB.sol +++ b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol @@ -1,6 +1,6 @@ pragma solidity 0.8.9; -contract StakingRouter__MockForVebo { +contract StakingRouter__MockForTWG { event Mock__onValidatorExitTriggered( uint256 _stakingModuleId, uint256 _nodeOperatorId, diff --git a/test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol b/test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol new file mode 100644 index 0000000000..d828a75337 --- /dev/null +++ b/test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol @@ -0,0 +1,13 @@ +pragma solidity 0.8.9; + +contract TriggerableWithdrawalGateway__MockForVEB { + event Mock__triggerFullWithdrawalsTriggered(bytes triggerableExitData, address refundRecipient, uint8 exitType); + + function triggerFullWithdrawals( + bytes calldata triggerableExitData, + address refundRecipient, + uint8 exitType + ) external payable { + emit Mock__triggerFullWithdrawalsTriggered(triggerableExitData, refundRecipient, exitType); + } +} diff --git a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol b/test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol similarity index 89% rename from test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol rename to test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol index be66bc98f3..7493aaa090 100644 --- a/test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol +++ b/test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol @@ -1,6 +1,6 @@ pragma solidity 0.8.9; -contract WithdrawalVault__MockForVebo { +contract WithdrawalVault__MockForTWG { event AddFullWithdrawalRequestsCalled(bytes pubkeys); function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amount) external payable { diff --git a/test/0.8.9/lidoLocator.test.ts b/test/0.8.9/lidoLocator.test.ts index 0fd20a2e4e..ca1d17559e 100644 --- a/test/0.8.9/lidoLocator.test.ts +++ b/test/0.8.9/lidoLocator.test.ts @@ -22,6 +22,7 @@ const services = [ "withdrawalVault", "oracleDaemonConfig", "validatorExitVerifier", + "triggerableWithdrawalGateway", ] as const; type ArrayToUnion = A[number]; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index 507ea6ba4e..087427fae6 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -22,7 +22,6 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; let originalState: string; - let withdrawalVault: WithdrawalVault__MockForVebo; let initTx: ContractTransactionResponse; let oracleVersion: bigint; @@ -73,9 +72,8 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; - withdrawalVault = deployed.withdrawalVault; - initTx = await initVEBO({ admin: admin.address, oracle, consensus, withdrawalVault, resumeAfterDeploy: true }); + initTx = await initVEBO({ admin: admin.address, oracle, consensus, resumeAfterDeploy: true }); oracleVersion = await oracle.getContractVersion(); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts index e8691852ea..3690c032c5 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts @@ -4,12 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { - HashConsensus__Harness, - ValidatorsExitBus__Harness, - ValidatorsExitBusOracle, - WithdrawalVault__MockForVebo, -} from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness, ValidatorsExitBusOracle } from "typechain-types"; import { CONSENSUS_VERSION, SECONDS_PER_SLOT } from "lib"; @@ -43,15 +38,13 @@ describe("ValidatorsExitBusOracle.sol:deploy", () => { context("deployment and init finishes successfully (default setup)", async () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; - let withdrawalVault: WithdrawalVault__MockForVebo; before(async () => { const deployed = await deployVEBO(admin.address); - withdrawalVault = deployed.withdrawalVault; + await initVEBO({ admin: admin.address, oracle: deployed.oracle, - withdrawalVault, consensus: deployed.consensus, }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts index 8a2b4cbb6e..c8b7c524b0 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.gas.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -30,7 +30,6 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; - let withdrawalVault: WithdrawalVault__MockForVebo; let oracleVersion: bigint; @@ -80,12 +79,10 @@ describe("ValidatorsExitBusOracle.sol:gas", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; - withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, - withdrawalVault, consensus, resumeAfterDeploy: true, }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts index 5da082d184..b10bb3ddd8 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.happyPath.test.ts @@ -4,7 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -29,7 +29,6 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; - let withdrawalVault: WithdrawalVault__MockForVebo; let oracleVersion: bigint; let exitRequests: ExitRequest[]; @@ -80,13 +79,11 @@ describe("ValidatorsExitBusOracle.sol:happyPath", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; - withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, consensus, - withdrawalVault, resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts similarity index 89% rename from test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts rename to test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index e79cf2f5d4..88bafbf065 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.emitExitEvents.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -3,7 +3,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { HashConsensus__Harness, ValidatorsExitBus__Harness, WithdrawalVault__MockForVebo } from "typechain-types"; +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; import { de0x, numberToHex } from "lib"; @@ -17,11 +17,10 @@ const PUBKEYS = [ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ]; -describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { +describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; - let withdrawalVault: WithdrawalVault__MockForVebo; let exitRequests: ExitRequest[]; let exitRequestHash: string; @@ -57,13 +56,11 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; consensus = deployed.consensus; - withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, consensus, - withdrawalVault, resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); @@ -85,8 +82,8 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { exitRequest = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; - await expect(oracle.emitExitEvents(exitRequest)) - .to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted") + await expect(oracle.submitExitRequestsData(exitRequest)) + .to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted") .withArgs(); }); @@ -94,7 +91,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { const request = [exitRequest.data, exitRequest.dataFormat]; const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["(bytes, uint256)"], [request])); - await expect(oracle.connect(stranger).submitReportHash(hash)).to.be.revertedWithOZAccessControlError( + await expect(oracle.connect(stranger).submitExitRequestsHash(hash)).to.be.revertedWithOZAccessControlError( await stranger.getAddress(), await oracle.SUBMIT_REPORT_HASH_ROLE(), ); @@ -109,13 +106,13 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), ); - const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(exitRequestHash); }); it("Emit ValidatorExit event", async () => { - const emitTx = await oracle.emitExitEvents(exitRequest); + const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); await expect(emitTx) @@ -162,10 +159,10 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { it("Should revert if wrong DATA_FORMAT", async () => { const request = [exitRequest.data, 2]; const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], request)); - const submitTx = await oracle.connect(authorizedEntity).submitReportHash(hash); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(hash); exitRequest = { dataFormat: 2, data: encodeExitRequestsDataList(exitRequests) }; - await expect(oracle.emitExitEvents(exitRequest)) + await expect(oracle.submitExitRequestsData(exitRequest)) .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") .withArgs(2); }); @@ -194,8 +191,8 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), ); - await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); - const emitTx = await oracle.emitExitEvents(exitRequest); + await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); + const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); for (const request of exitRequests) { @@ -223,8 +220,8 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), ); - await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); - const emitTx = await oracle.emitExitEvents(exitRequest); + await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); + const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); for (let i = 0; i < 3; i++) { @@ -249,7 +246,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { data: encodeExitRequestsDataList(exitRequests), }; - await expect(oracle.emitExitEvents(exitRequest)) + await expect(oracle.submitExitRequestsData(exitRequest)) .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") .withArgs(2, 0); }); @@ -270,7 +267,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { data: encodeExitRequestsDataList(exitRequests), }; - const emitTx = await oracle.emitExitEvents(exitRequest); + const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); for (let i = 3; i < 5; i++) { @@ -290,7 +287,7 @@ describe("ValidatorsExitBusOracle.sol:emitExitEvents", () => { { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, ]; - await expect(oracle.emitExitEvents(exitRequest)).to.be.revertedWithCustomError( + await expect(oracle.submitExitRequestsData(exitRequest)).to.be.revertedWithCustomError( oracle, "RequestsAlreadyDelivered", ); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 0c749e25b8..0bfa010aff 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -4,12 +4,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { - HashConsensus__Harness, - OracleReportSanityChecker, - ValidatorsExitBus__Harness, - WithdrawalVault__MockForVebo, -} from "typechain-types"; +import { HashConsensus__Harness, OracleReportSanityChecker, ValidatorsExitBus__Harness } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; @@ -30,7 +25,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; let oracleReportSanityChecker: OracleReportSanityChecker; - let withdrawalVault: WithdrawalVault__MockForVebo; let oracleVersion: bigint; @@ -108,13 +102,11 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { oracle = deployed.oracle; consensus = deployed.consensus; oracleReportSanityChecker = deployed.oracleReportSanityChecker; - withdrawalVault = deployed.withdrawalVault; await initVEBO({ admin: admin.address, oracle, consensus, - withdrawalVault, resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); @@ -636,7 +628,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); await oracle.grantRole(role, authorizedEntity); - const submitTx = await oracle.connect(authorizedEntity).submitReportHash(exitRequestHash); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(exitRequestHash); const exitRequest = { @@ -644,7 +636,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { data: reportData.data, }; - const emitTx = await oracle.emitExitEvents(exitRequest); + const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 042a440b4f..0a6da84256 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -1,26 +1,17 @@ import { expect } from "chai"; -import { ZeroHash } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { HashConsensus__Harness, - StakingRouter__MockForVebo, + TriggerableWithdrawalGateway__MockForVEB, ValidatorsExitBus__Harness, - WithdrawalVault__MockForVebo, } from "typechain-types"; import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; -import { - computeTimestampAtSlot, - DATA_FORMAT_LIST, - deployVEBO, - initVEBO, - SECONDS_PER_FRAME, - SLOTS_PER_FRAME, -} from "test/deploy"; +import { DATA_FORMAT_LIST, deployVEBO, initVEBO, SECONDS_PER_FRAME } from "test/deploy"; const PUBKEYS = [ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -36,8 +27,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; - let withdrawalVault: WithdrawalVault__MockForVebo; - let stakingRouter: StakingRouter__MockForVebo; + let triggerableWithdrawalGateway: TriggerableWithdrawalGateway__MockForVEB; let oracleVersion: bigint; let exitRequests: ExitRequest[]; @@ -83,18 +73,29 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { return "0x" + requests.map(encodeExitRequestHex).join(""); }; + const encodeTWGExitRequestsData = ({ moduleId, nodeOpId, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + pubkeyHex; + }; + + const encodeTWGExitDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeTWGExitRequestsData).join(""); + }; + const deploy = async () => { const deployed = await deployVEBO(admin.address); + const locator = deployed.locator; oracle = deployed.oracle; consensus = deployed.consensus; - withdrawalVault = deployed.withdrawalVault; - stakingRouter = deployed.stakingRouter; + triggerableWithdrawalGateway = deployed.triggerableWithdrawalGateway; + + console.log("twg=", await locator.triggerableWithdrawalGateway()); await initVEBO({ admin: admin.address, oracle, consensus, - withdrawalVault, resumeAfterDeploy: true, lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, }); @@ -123,38 +124,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await consensus.advanceTimeBy(24 * 60 * 60); }); - it("Should set limit for tw", async () => { - const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); - await oracle.grantRole(role, admin); - const exitLimitTx = await oracle.connect(admin).setExitRequestLimit(8, 8); - - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(8, 8); - }); - - it("initially, consensus report is empty and is not being processed", async () => { - const report = await oracle.getConsensusReport(); - expect(report.hash).to.equal(ZeroHash); - - expect(report.processingDeadlineTime).to.equal(0); - expect(report.processingStarted).to.equal(false); - - const frame = await consensus.getCurrentFrame(); - const procState = await oracle.getProcessingState(); - - expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); - expect(procState.dataHash).to.equal(ZeroHash); - expect(procState.processingDeadlineTime).to.equal(0); - expect(procState.dataSubmitted).to.equal(false); - expect(procState.dataFormat).to.equal(0); - expect(procState.requestsCount).to.equal(0); - expect(procState.requestsSubmitted).to.equal(0); - }); - - it("reference slot of the empty initial consensus report is set to the last processing slot passed to the initialize function", async () => { - const report = await oracle.getConsensusReport(); - expect(report.refSlot).to.equal(LAST_PROCESSING_REF_SLOT); - }); - it("committee reaches consensus on a report hash", async () => { const { refSlot } = await consensus.getCurrentFrame(); @@ -178,26 +147,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await triggerConsensusOnHash(reportHash); }); - it("oracle gets the report hash", async () => { - const report = await oracle.getConsensusReport(); - expect(report.hash).to.equal(reportHash); - expect(report.refSlot).to.equal(reportFields.refSlot); - expect(report.processingDeadlineTime).to.equal(computeTimestampAtSlot(report.refSlot + SLOTS_PER_FRAME)); - - expect(report.processingStarted).to.equal(false); - - const frame = await consensus.getCurrentFrame(); - const procState = await oracle.getProcessingState(); - - expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); - expect(procState.dataHash).to.equal(reportHash); - expect(procState.processingDeadlineTime).to.equal(computeTimestampAtSlot(frame.reportProcessingDeadlineSlot)); - expect(procState.dataSubmitted).to.equal(false); - expect(procState.dataFormat).to.equal(0); - expect(procState.requestsCount).to.equal(0); - expect(procState.requestsSubmitted).to.equal(0); - }); - it("some time passes", async () => { await consensus.advanceTimeBy(SECONDS_PER_FRAME / 3n); }); @@ -217,20 +166,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { } }); - it("reports are marked as processed", async () => { - const frame = await consensus.getCurrentFrame(); - const procState = await oracle.getProcessingState(); - - expect(procState.currentFrameRefSlot).to.equal(frame.refSlot); - expect(procState.dataHash).to.equal(reportHash); - expect(procState.processingDeadlineTime).to.equal(computeTimestampAtSlot(frame.reportProcessingDeadlineSlot)); - expect(procState.dataSubmitted).to.equal(true); - expect(procState.dataFormat).to.equal(DATA_FORMAT_LIST); - expect(procState.requestsCount).to.equal(exitRequests.length); - expect(procState.requestsSubmitted).to.equal(exitRequests.length); - }); - - it("someone submitted exit report data and triggered exit", async () => { + it("should triggers exits for all validators in exit request", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 2, 3], @@ -239,30 +175,14 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { { value: 4 }, ); - const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[2], PUBKEYS[3]]; - const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); - await expect(tx) - .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") - .withArgs("0x" + concatenatedPubKeys); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[0].moduleId, exitRequests[0].nodeOpId, pubkeys[0], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[1].moduleId, exitRequests[1].nodeOpId, pubkeys[1], 1, 0); + const requests = encodeTWGExitDataList(exitRequests); await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[2].moduleId, exitRequests[2].nodeOpId, pubkeys[2], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[3], 1, 0); + .to.emit(triggerableWithdrawalGateway, "Mock__triggerFullWithdrawalsTriggered") + .withArgs(requests, admin.address, 0); }); - it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { + it("should triggers exits only for validators in selected request indexes", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], @@ -273,38 +193,14 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }, ); - const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; - const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); - await expect(tx) - .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") - .withArgs("0x" + concatenatedPubKeys); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[0].moduleId, exitRequests[0].nodeOpId, pubkeys[0], 1, 0); + const requests = encodeTWGExitDataList(exitRequests.filter((req, i) => [0, 1, 3].includes(i))); await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[1].moduleId, exitRequests[1].nodeOpId, pubkeys[1], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[2], 1, 0); - - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(admin, 7); + .to.emit(triggerableWithdrawalGateway, "Mock__triggerFullWithdrawalsTriggered") + .withArgs(requests, admin.address, 0); }); - it("Not enough fee", async () => { - await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1], ZERO_ADDRESS, 0, { - value: 1, - }), - ) - .to.be.revertedWithCustomError(oracle, "InsufficientWithdrawalFee") - .withArgs(2, 1); - }); - - it("Should trigger withdrawals only for validators that were requested for voluntary exit by trusted entities earlier", async () => { + it("should revert with error if the hash of `requestsData` was not previously submitted in the VEB", async () => { await expect( oracle.triggerExits( { @@ -316,10 +212,10 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { 0, { value: 2 }, ), - ).to.be.revertedWithCustomError(oracle, "ExitHashWasNotSubmitted"); + ).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); }); - it("Requested index out of range", async () => { + it("should revert with error if requested index out of range", async () => { await expect( oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], ZERO_ADDRESS, 0, { value: 2, @@ -328,56 +224,4 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") .withArgs(5, 4); }); - - it("someone submitted exit report data and triggered exit on not sequential indexes", async () => { - await expect( - oracle.triggerExits( - { data: reportFields.data, dataFormat: reportFields.dataFormat }, - [0, 1, 3], - ZERO_ADDRESS, - 0, - { - value: 10, - }, - ), - ) - .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") - .withArgs(3, 1); - }); - - it("some time passes", async () => { - await consensus.advanceTimeBy(24 * 60 * 60); - }); - - it("Limit regenerated in a day", async () => { - const tx = await oracle.triggerExits( - { data: reportFields.data, dataFormat: reportFields.dataFormat }, - [0, 1, 3], - ZERO_ADDRESS, - 0, - { - value: 10, - }, - ); - - const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; - const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); - await expect(tx) - .to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled") - .withArgs("0x" + concatenatedPubKeys); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[0].moduleId, exitRequests[0].nodeOpId, pubkeys[0], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[1].moduleId, exitRequests[1].nodeOpId, pubkeys[1], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitRequests[3].moduleId, exitRequests[3].nodeOpId, pubkeys[2], 1, 0); - - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(admin, 7); - }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts deleted file mode 100644 index 122c4f3390..0000000000 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExitsDirectly.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - -import { - HashConsensus__Harness, - StakingRouter__MockForVebo, - ValidatorsExitBus__Harness, - WithdrawalVault__MockForVebo, -} from "typechain-types"; - -import { deployVEBO, initVEBO } from "test/deploy"; - -const PUBKEYS = [ - "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", -]; - -const ZERO_ADDRESS = ethers.ZeroAddress; - -describe("ValidatorsExitBusOracle.sol:triggerExitsDirectly", () => { - let consensus: HashConsensus__Harness; - let oracle: ValidatorsExitBus__Harness; - let admin: HardhatEthersSigner; - let withdrawalVault: WithdrawalVault__MockForVebo; - let stakingRouter: StakingRouter__MockForVebo; - - let authorizedEntity: HardhatEthersSigner; - let stranger: HardhatEthersSigner; - let exitData: DirectExitData; - - const LAST_PROCESSING_REF_SLOT = 1; - const pubkeys = [PUBKEYS[0], PUBKEYS[1], PUBKEYS[3]]; - - interface DirectExitData { - stakingModuleId: number; - nodeOperatorId: number; - validatorsPubkeys: string; - } - - const deploy = async () => { - const deployed = await deployVEBO(admin.address); - oracle = deployed.oracle; - consensus = deployed.consensus; - withdrawalVault = deployed.withdrawalVault; - stakingRouter = deployed.stakingRouter; - - await initVEBO({ - admin: admin.address, - oracle, - consensus, - withdrawalVault, - resumeAfterDeploy: true, - lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, - }); - }; - - before(async () => { - [admin, authorizedEntity, stranger] = await ethers.getSigners(); - - await deploy(); - }); - - it("some time passes", async () => { - await consensus.advanceTimeBy(24 * 60 * 60); - }); - - it("Should set limit for tw", async () => { - const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); - await oracle.grantRole(role, authorizedEntity); - const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit(4, 4); - - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(4, 4); - }); - - it("Should revert without DIRECT_EXIT_ROLE role", async () => { - const concatenatedPubKeys = pubkeys.map((pk) => pk.replace(/^0x/, "")).join(""); - - exitData = { - stakingModuleId: 1, - nodeOperatorId: 0, - validatorsPubkeys: "0x" + concatenatedPubKeys, - }; - - await expect( - oracle.connect(stranger).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { - value: 4, - }), - ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.DIRECT_EXIT_ROLE()); - }); - - it("Grant DIRECT_EXIT_ROLE role", async () => { - const role = await oracle.DIRECT_EXIT_ROLE(); - - await oracle.grantRole(role, authorizedEntity); - }); - - it("Not enough fee", async () => { - await expect( - oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { - value: 2, - }), - ) - .to.be.revertedWithCustomError(oracle, "InsufficientWithdrawalFee") - .withArgs(3, 2); - }); - - it("Emit ValidatorExit event and should trigger withdrawals", async () => { - const tx = await oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { - value: 4, - }); - const timestamp = await oracle.getTime(); - await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(authorizedEntity, 1); - - await expect(tx) - .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp, authorizedEntity); - await expect(tx) - .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp, authorizedEntity); - await expect(tx) - .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp, authorizedEntity); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], 1, 0); - }); - - it("Out of tw exit request limit", async () => { - await expect( - oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { - value: 4, - }), - ) - .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") - .withArgs(3, 1); - }); - - it("some time passes", async () => { - await consensus.advanceTimeBy(24 * 60 * 60); - }); - - it("Limit regenerated in a day", async () => { - const tx = oracle.connect(authorizedEntity).triggerExitsDirectly(exitData, ZERO_ADDRESS, 0, { - value: 4, - }); - - const timestamp = await oracle.getTime(); - await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(exitData.validatorsPubkeys); - await expect(tx).to.emit(oracle, "MadeRefund").withArgs(authorizedEntity, 1); - - await expect(tx) - .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], timestamp, authorizedEntity); - await expect(tx) - .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], timestamp, authorizedEntity); - await expect(tx) - .to.emit(oracle, "DirectExitRequest") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], timestamp, authorizedEntity); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[0], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[1], 1, 0); - - await expect(tx) - .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") - .withArgs(exitData.stakingModuleId, exitData.nodeOperatorId, pubkeys[2], 1, 0); - }); -}); diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 254173cacb..42d00fe966 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -133,7 +133,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { await expect(oracle.getExitRequestsDeliveryHistory(fakeHash)).to.be.revertedWithCustomError( oracle, - "ExitHashWasNotSubmitted", + "ExitHashNotSubmitted", ); }); diff --git a/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts b/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts index 0102254e5d..39a6331c19 100644 --- a/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts +++ b/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts @@ -92,6 +92,7 @@ describe("OracleReportSanityChecker.sol:misc", () => { withdrawalVault: withdrawalVault, postTokenRebaseReceiver: deployer.address, oracleDaemonConfig: deployer.address, + triggerableWithdrawalGateway: deployer.address, }, ]); managersRoster = { diff --git a/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts b/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts index 4265eb577e..2944924b4f 100644 --- a/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts +++ b/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts @@ -83,6 +83,7 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => { withdrawalVault: deployer.address, postTokenRebaseReceiver: deployer.address, oracleDaemonConfig: deployer.address, + triggerableWithdrawalGateway: deployer.address, }, ]); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts new file mode 100644 index 0000000000..123c449e8a --- /dev/null +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -0,0 +1,124 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { StakingRouter__MockForTWG, TriggerableWithdrawalGateway, WithdrawalVault__MockForTWG } from "typechain-types"; + +import { de0x, numberToHex } from "lib"; + +import { deployLidoLocator, updateLidoLocatorImplementation } from "../deploy/locator"; + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +]; + +const exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, +]; + +const ZERO_ADDRESS = ethers.ZeroAddress; + +describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { + let triggerableWithdrawalGateway: TriggerableWithdrawalGateway; + let withdrawalVault: WithdrawalVault__MockForTWG; + let stakingRouter: StakingRouter__MockForTWG; + let admin: HardhatEthersSigner; + let authorizedEntity: HardhatEthersSigner; + + const encodeTWGExitRequestsData = ({ moduleId, nodeOpId, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + pubkeyHex; + }; + + const encodeTWGExitDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeTWGExitRequestsData).join(""); + }; + + before(async () => { + [admin, authorizedEntity] = await ethers.getSigners(); + + const locator = await deployLidoLocator(); + const locatorAddr = await locator.getAddress(); + + withdrawalVault = await ethers.deployContract("WithdrawalVault__MockForTWG"); + stakingRouter = await ethers.deployContract("StakingRouter__MockForTWG"); + + await updateLidoLocatorImplementation(locatorAddr, { + withdrawalVault: await withdrawalVault.getAddress(), + stakingRouter: await stakingRouter.getAddress(), + }); + + triggerableWithdrawalGateway = await ethers.deployContract("TriggerableWithdrawalGateway", [locatorAddr]); + + await triggerableWithdrawalGateway.initialize(admin); + }); + + it("should revert if caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE", async () => { + const requests = encodeTWGExitDataList(exitRequests); + const role = await triggerableWithdrawalGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await expect( + triggerableWithdrawalGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 10 }), + ).to.be.revertedWithOZAccessControlError(await authorizedEntity.getAddress(), role); + }); + + it("should revert if total fee value sent is insufficient to cover all provided TW requests ", async () => { + const role = await triggerableWithdrawalGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await triggerableWithdrawalGateway.grantRole(role, authorizedEntity); + + const requests = encodeTWGExitDataList(exitRequests); + + await expect( + triggerableWithdrawalGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 1 }), + ) + .to.be.revertedWithCustomError(triggerableWithdrawalGateway, "InsufficientWithdrawalFee") + .withArgs(3, 1); + }); + + it("should add withdrawal request", async () => { + const requests = encodeTWGExitDataList(exitRequests); + + const tx = await triggerableWithdrawalGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); + + const timestamp = (await tx.getBlock())?.timestamp; + + const pubkeys = + "0x" + + exitRequests + .map((request) => { + const pubkeyHex = de0x(request.valPubkey); + return pubkeyHex; + }) + .join(""); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(triggerableWithdrawalGateway, "TriggerableExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, timestamp); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, 1, 0); + + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(pubkeys); + } + }); +}); diff --git a/test/deploy/locator.ts b/test/deploy/locator.ts index eb30806e2f..713f58dfe5 100644 --- a/test/deploy/locator.ts +++ b/test/deploy/locator.ts @@ -29,6 +29,7 @@ async function deployDummyLocator(config?: Partial, de withdrawalQueue: certainAddress("dummy-locator:withdrawalQueue"), withdrawalVault: certainAddress("dummy-locator:withdrawalVault"), validatorExitVerifier: certainAddress("dummy-locator:validatorExitVerifier"), + triggerableWithdrawalGateway: certainAddress("dummy-locator:triggerableWithdrawalGateway"), ...config, }); @@ -103,6 +104,7 @@ async function getLocatorConfig(locatorAddress: string) { "withdrawalQueue", "withdrawalVault", "oracleDaemonConfig", + "triggerableWithdrawalGateway", ] as Partial[]; const configPromises = addresses.map((name) => locator[name]()); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index db1448d60e..15f96432ef 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -1,12 +1,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { - HashConsensus__Harness, - ReportProcessor__Mock, - ValidatorsExitBusOracle, - WithdrawalVault__MockForVebo, -} from "typechain-types"; +import { HashConsensus__Harness, ReportProcessor__Mock, ValidatorsExitBusOracle } from "typechain-types"; import { CONSENSUS_VERSION, @@ -39,12 +34,8 @@ async function deployOracleReportSanityCheckerForExitBus(lidoLocator: string, ad return await ethers.deployContract("OracleReportSanityChecker", [lidoLocator, admin, limitsList]); } -async function deployWithdrawalVault() { - return await ethers.deployContract("WithdrawalVault__MockForVebo"); -} - -async function deploySR() { - return await ethers.deployContract("StakingRouter__MockForVebo"); +async function deployTWG() { + return await ethers.deployContract("TriggerableWithdrawalGateway__MockForVEB"); } export async function deployVEBO( @@ -71,14 +62,12 @@ export async function deployVEBO( const { ao, lido } = await deployMockAccountingOracle(secondsPerSlot, genesisTime); - const withdrawalVault = await deployWithdrawalVault(); - const stakingRouter = await deploySR(); + const triggerableWithdrawalGateway = await deployTWG(); await updateLidoLocatorImplementation(locatorAddr, { lido: await lido.getAddress(), accountingOracle: await ao.getAddress(), - withdrawalVault, - stakingRouter, + triggerableWithdrawalGateway, //: await lido.getAddress(), // await triggerableWithdrawalGateway.getAddress(), }); const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locatorAddr, admin); @@ -91,12 +80,12 @@ export async function deployVEBO( await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot); return { + locator, locatorAddr, oracle, consensus, oracleReportSanityChecker, - withdrawalVault, - stakingRouter, + triggerableWithdrawalGateway, }; } @@ -104,7 +93,6 @@ interface VEBOConfig { admin: string; oracle: ValidatorsExitBusOracle; consensus: HashConsensus__Harness; - withdrawalVault: WithdrawalVault__MockForVebo; dataSubmitter?: string; consensusVersion?: bigint; lastProcessingRefSlot?: number; From 00133bd9c0bb8ac39dc55f6fe1b3964b8e04a4e9 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 12 May 2025 13:11:19 +0400 Subject: [PATCH 124/405] fix: lint --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index a3baf94b80..05487a6a99 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -9,7 +9,6 @@ import {Versioned} from "../utils/Versioned.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; import {PausableUntil} from "../utils/PausableUntil.sol"; import {IValidatorsExitBus} from "../interfaces/IValidatorsExitBus.sol"; -import "hardhat/console.sol"; interface ITriggerableWithdrawalGateway { function triggerFullWithdrawals( From e23ccf4d4b961577e27cfc8fa657e1688ccd6dbf Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 12 May 2025 22:17:21 +0400 Subject: [PATCH 125/405] fix: keyIndexes sorting rule --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 22 +++++++++++++-- ...dator-exit-bus-oracle.triggerExits.test.ts | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 05487a6a99..05592a34c0 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -70,10 +70,18 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @notice Throw when in submitExitRequestsData all requests were already delivered */ error RequestsAlreadyDelivered(); - + /** + * @notice Thrown when triggerable withdrawal was requested for validator that was not delivered yet + */ error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); - + /** + * @notice Thrown when index of request in submitted data for triggerable withdrawal is out of range + */ error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + /** + * @notice Thrown when array of indexes of requests in submitted data for triggerable withdrawal is not is not strictly increasing array + */ + error InvalidKeyIndexSortOrder(); /** * @notice Thrown when a withdrawal fee refund failed @@ -99,7 +107,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa address indexed refundRecipient ); struct RequestStatus { - // Total items count in report (by default type(uint32).max, update on first report delivery) + // Total items count in report (by default type(uint256).max, update on first report delivery) uint256 totalItemsCount; // Total processed items in report (by default 0) uint256 deliveredItemsCount; @@ -249,6 +257,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @dev Reverts if: * - The hash of `requestsData` was not previously submitted in the VEB. * - Any of the provided `keyIndexes` refers to a validator that was not yet unpacked (i.e., exit requiest not emitted). + * - `keyIndexes` is not strictly increasing array */ function triggerExits( ExitRequestData calldata requestsData, @@ -274,6 +283,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes memory exits = new bytes(keyIndexes.length * PACKED_TWG_EXIT_REQUEST_LENGTH); + uint256 lastKeyIndex = type(uint256).max; + for (uint256 i = 0; i < keyIndexes.length; i++) { if (keyIndexes[i] >= requestStatus.totalItemsCount) { revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); @@ -283,6 +294,11 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyWasNotDelivered(keyIndexes[i], requestStatus.deliveredItemsCount - 1); } + if (i > 0 && keyIndexes[i] <= lastKeyIndex ) { + revert InvalidKeyIndexSortOrder(); + } + lastKeyIndex = keyIndexes[i]; + ValidatorData memory validatorData = _getValidatorData(requestsData.data, keyIndexes[i]); if (validatorData.moduleId == 0) revert InvalidRequestsData(); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 0a6da84256..333f2f2e63 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -224,4 +224,32 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") .withArgs(5, 4); }); + + it("should revert with an error if the key index array contains duplicates", async () => { + await expect( + oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [1, 2, 2], + ZERO_ADDRESS, + 0, + { + value: 2, + }, + ), + ).to.be.revertedWithCustomError(oracle, "InvalidKeyIndexSortOrder"); + }); + + it("should revert with an error if the key index array is not strictly increasing", async () => { + await expect( + oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [1, 2, 2], + ZERO_ADDRESS, + 0, + { + value: 2, + }, + ), + ).to.be.revertedWithCustomError(oracle, "InvalidKeyIndexSortOrder"); + }); }); From 8a1076808c9e0e7e808c4066aaef5ad49680f092 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 14 May 2025 17:38:49 +0200 Subject: [PATCH 126/405] refactor: rename ValidatorExitDelayVerifier contract --- ...ier.sol => ValidatorExitDelayVerifier.sol} | 8 +- contracts/0.8.9/LidoLocator.sol | 6 +- deployed-hoodi.json | 4 +- lib/deploy.ts | 2 +- lib/state-file.ts | 2 +- .../steps/0090-deploy-non-aragon-contracts.ts | 37 ++++---- scripts/triggerable-withdrawals/tw-deploy.ts | 16 ++-- scripts/triggerable-withdrawals/tw-verify.ts | 6 +- ....ts => validatorExitDelayVerifier.test.ts} | 88 +++++++++---------- ...s => validatorExitDelayVerifierHelpers.ts} | 2 +- test/0.8.9/lidoLocator.test.ts | 2 +- test/deploy/locator.ts | 2 +- 12 files changed, 90 insertions(+), 85 deletions(-) rename contracts/0.8.25/{ValidatorExitVerifier.sol => ValidatorExitDelayVerifier.sol} (98%) rename test/0.8.25/{validatorExitVerifier.test.ts => validatorExitDelayVerifier.test.ts} (82%) rename test/0.8.25/{validatorExitVerifierHelpers.ts => validatorExitDelayVerifierHelpers.ts} (97%) diff --git a/contracts/0.8.25/ValidatorExitVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol similarity index 98% rename from contracts/0.8.25/ValidatorExitVerifier.sol rename to contracts/0.8.25/ValidatorExitDelayVerifier.sol index acffb68afb..3566af1da5 100644 --- a/contracts/0.8.25/ValidatorExitVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -42,12 +42,12 @@ struct HistoricalHeaderWitness { } /** - * @title ValidatorExitVerifier + * @title ValidatorExitDelayVerifier * @notice Verifies validator proofs to ensure they are unexited after an exit request. * Allows permissionless report the status of validators which are assumed to have exited but have not. * @dev Uses EIP-4788 to confirm the correctness of a given beacon block root. */ -contract ValidatorExitVerifier { +contract ValidatorExitDelayVerifier { using SSZ for Validator; using SSZ for BeaconBlockHeader; @@ -167,7 +167,7 @@ contract ValidatorExitVerifier { * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited. */ - function verifyActiveValidatorsAfterExitRequest( + function verifyValidatorExitDelay( ProvableBeaconBlockHeader calldata beaconBlock, ValidatorWitness[] calldata validatorWitnesses, ExitRequestData calldata exitRequests @@ -216,7 +216,7 @@ contract ValidatorExitVerifier { * @param oldBlock Historical block header witness data and its proof. * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited in oldBlock.header. */ - function verifyHistoricalActiveValidatorsAfterExitRequest( + function verifyHistoricalValidatorExitDelay( ProvableBeaconBlockHeader calldata beaconBlock, HistoricalHeaderWitness calldata oldBlock, ValidatorWitness[] calldata validatorWitnesses, diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index c8c02026a5..7df3f1a20d 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -28,7 +28,7 @@ contract LidoLocator is ILidoLocator { address withdrawalQueue; address withdrawalVault; address oracleDaemonConfig; - address validatorExitVerifier; + address validatorExitDelayVerifier; } error ZeroAddress(); @@ -47,7 +47,7 @@ contract LidoLocator is ILidoLocator { address public immutable withdrawalQueue; address public immutable withdrawalVault; address public immutable oracleDaemonConfig; - address public immutable validatorExitVerifier; + address public immutable validatorExitDelayVerifier; /** * @notice declare service locations @@ -69,7 +69,7 @@ contract LidoLocator is ILidoLocator { withdrawalQueue = _assertNonZero(_config.withdrawalQueue); withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); - validatorExitVerifier = _assertNonZero(_config.validatorExitVerifier); + validatorExitDelayVerifier = _assertNonZero(_config.validatorExitDelayVerifier); } function coreComponents() external view returns( diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 71ab7a1fef..e4831a5807 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -596,9 +596,9 @@ "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] } }, - "validatorExitVerifier": { + "validatorExitDelayVerifier": { "implementation": { - "contract": "contracts/0.8.25/ValidatorExitVerifier.sol", + "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", "constructorArgs": [ "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", diff --git a/lib/deploy.ts b/lib/deploy.ts index a7f991fd02..62fed3833a 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -246,7 +246,7 @@ async function getLocatorConfig(locatorAddress: string) { "withdrawalQueue", "withdrawalVault", "oracleDaemonConfig", - "validatorExitVerifier", + "validatorExitDelayVerifier", ] as (keyof LidoLocator.ConfigStruct)[]; const configPromises = addresses.map((name) => locator[name]()); diff --git a/lib/state-file.ts b/lib/state-file.ts index e0cbc951c3..f2e41f1421 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -87,7 +87,7 @@ export enum Sk { scratchDeployGasUsed = "scratchDeployGasUsed", minFirstAllocationStrategy = "minFirstAllocationStrategy", triggerableWithdrawals = "triggerableWithdrawals", - validatorExitVerifier = "validatorExitVerifier", + validatorExitDelayVerifier = "validatorExitDelayVerifier", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 2bdfe12cf8..b8dbb50015 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -200,21 +200,26 @@ export async function main() { burnerParams.totalNonCoverSharesBurnt, ]); - // Deploy ValidatorExitVerifier - const validatorExitVerifier = await deployWithoutProxy(Sk.validatorExitVerifier, "ValidatorExitVerifier", deployer, [ - locator.address, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, - 1, // uint64 firstSupportedSlot, - 1, // uint64 pivotSlot, - chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, - chainSpec.secondsPerSlot, // uint32 secondsPerSlot, - parseInt(getEnvVariable("GENESIS_TIME")), // uint64 genesisTime, - // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 - 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds - ]); + // Deploy ValidatorExitDelayVerifier + const validatorExitDelayVerifier = await deployWithoutProxy( + Sk.validatorExitDelayVerifier, + "ValidatorExitDelayVerifier", + deployer, + [ + locator.address, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, + "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + 1, // uint64 firstSupportedSlot, + 1, // uint64 pivotSlot, + chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, + chainSpec.secondsPerSlot, // uint32 secondsPerSlot, + parseInt(getEnvVariable("GENESIS_TIME")), // uint64 genesisTime, + // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 + 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds + ], + ); // Update LidoLocator with valid implementation const locatorConfig: string[] = [ @@ -232,7 +237,7 @@ export async function main() { withdrawalQueueERC721.address, withdrawalVaultAddress, oracleDaemonConfig.address, - validatorExitVerifier.address, + validatorExitDelayVerifier.address, ]; await updateProxyImplementation(Sk.lidoLocator, "LidoLocator", locator.address, proxyContractsOwner, [locatorConfig]); } diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index eb0baf6d06..fd296df5d8 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -91,7 +91,7 @@ async function main() { log.success(`NOR implementation address: ${NOR.address}`); log.emptyLine(); - const validatorExitVerifierArgs = [ + const validatorExitDelayVerifierArgs = [ locator.address, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, @@ -105,13 +105,13 @@ async function main() { 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds ]; - const validatorExitVerifier = await deployImplementation( - Sk.validatorExitVerifier, - "ValidatorExitVerifier", + const validatorExitDelayVerifier = await deployImplementation( + Sk.validatorExitDelayVerifier, + "ValidatorExitDelayVerifier", deployer, - validatorExitVerifierArgs, + validatorExitDelayVerifierArgs, ); - log.success(`ValidatorExitVerifier implementation address: ${NOR.address}`); + log.success(`ValidatorExitDelayVerifier implementation address: ${NOR.address}`); log.emptyLine(); // fetch contract addresses that will not changed @@ -130,7 +130,7 @@ async function main() { await locator.withdrawalQueue(), await locator.withdrawalVault(), await locator.oracleDaemonConfig(), - validatorExitVerifier.address, + validatorExitDelayVerifier.address, ]; const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); @@ -142,7 +142,7 @@ VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}" WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" -VALIDATOR_EXIT_VERIFIER = "${validatorExitVerifier.address}" +VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" `); } diff --git a/scripts/triggerable-withdrawals/tw-verify.ts b/scripts/triggerable-withdrawals/tw-verify.ts index b8cde5de49..86b38e0173 100644 --- a/scripts/triggerable-withdrawals/tw-verify.ts +++ b/scripts/triggerable-withdrawals/tw-verify.ts @@ -45,7 +45,7 @@ async function main() { const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; - const validatorExitVerifierArgs = [ + const validatorExitDelayVerifierArgs = [ locator.address, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, @@ -72,8 +72,8 @@ async function main() { }); await run("verify:verify", { - address: state[Sk.validatorExitVerifier].implementation.address, - constructorArguments: validatorExitVerifierArgs, + address: state[Sk.validatorExitDelayVerifier].implementation.address, + constructorArguments: validatorExitDelayVerifierArgs, contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", }); diff --git a/test/0.8.25/validatorExitVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts similarity index 82% rename from test/0.8.25/validatorExitVerifier.test.ts rename to test/0.8.25/validatorExitDelayVerifier.test.ts index fe6bc06165..71a4ba3e29 100644 --- a/test/0.8.25/validatorExitVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { ethers } from "hardhat"; -import { StakingRouter_Mock, ValidatorExitVerifier, ValidatorsExitBusOracle_Mock } from "typechain-types"; +import { StakingRouter_Mock, ValidatorExitDelayVerifier, ValidatorsExitBusOracle_Mock } from "typechain-types"; import { ILidoLocator } from "typechain-types/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol"; import { updateBeaconBlockRoot } from "lib"; @@ -16,12 +16,12 @@ import { toHistoricalHeaderWitness, toProvableBeaconBlockHeader, toValidatorWitness, -} from "./validatorExitVerifierHelpers"; +} from "./validatorExitDelayVerifierHelpers"; import { ACTIVE_VALIDATOR_PROOF } from "./validatorState"; const EMPTY_REPORT = { data: "0x", dataFormat: 1n }; -describe("ValidatorExitVerifier.sol", () => { +describe("ValidatorExitDelayVerifier.sol", () => { let originalState: string; beforeEach(async () => { @@ -40,16 +40,16 @@ describe("ValidatorExitVerifier.sol", () => { const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; - describe("ValidatorExitVerifier Constructor", () => { + describe("ValidatorExitDelayVerifier Constructor", () => { const GI_FIRST_VALIDATOR_PREV = `0x${"1".repeat(64)}`; const GI_FIRST_VALIDATOR_CURR = `0x${"2".repeat(64)}`; const GI_HISTORICAL_SUMMARIES_PREV = `0x${"3".repeat(64)}`; const GI_HISTORICAL_SUMMARIES_CURR = `0x${"4".repeat(64)}`; - let validatorExitVerifier: ValidatorExitVerifier; + let validatorExitDelayVerifier: ValidatorExitDelayVerifier; before(async () => { - validatorExitVerifier = await ethers.deployContract("ValidatorExitVerifier", [ + validatorExitDelayVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ LIDO_LOCATOR, GI_FIRST_VALIDATOR_PREV, GI_FIRST_VALIDATOR_CURR, @@ -65,25 +65,25 @@ describe("ValidatorExitVerifier.sol", () => { }); it("sets all parameters correctly", async () => { - expect(await validatorExitVerifier.LOCATOR()).to.equal(LIDO_LOCATOR); - expect(await validatorExitVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); - expect(await validatorExitVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); - expect(await validatorExitVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); - expect(await validatorExitVerifier.GI_HISTORICAL_SUMMARIES_PREV()).to.equal(GI_HISTORICAL_SUMMARIES_PREV); - expect(await validatorExitVerifier.GI_HISTORICAL_SUMMARIES_CURR()).to.equal(GI_HISTORICAL_SUMMARIES_CURR); - expect(await validatorExitVerifier.FIRST_SUPPORTED_SLOT()).to.equal(FIRST_SUPPORTED_SLOT); - expect(await validatorExitVerifier.PIVOT_SLOT()).to.equal(PIVOT_SLOT); - expect(await validatorExitVerifier.SLOTS_PER_EPOCH()).to.equal(SLOTS_PER_EPOCH); - expect(await validatorExitVerifier.SECONDS_PER_SLOT()).to.equal(SECONDS_PER_SLOT); - expect(await validatorExitVerifier.GENESIS_TIME()).to.equal(GENESIS_TIME); - expect(await validatorExitVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()).to.equal( + expect(await validatorExitDelayVerifier.LOCATOR()).to.equal(LIDO_LOCATOR); + expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); + expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); + expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); + expect(await validatorExitDelayVerifier.GI_HISTORICAL_SUMMARIES_PREV()).to.equal(GI_HISTORICAL_SUMMARIES_PREV); + expect(await validatorExitDelayVerifier.GI_HISTORICAL_SUMMARIES_CURR()).to.equal(GI_HISTORICAL_SUMMARIES_CURR); + expect(await validatorExitDelayVerifier.FIRST_SUPPORTED_SLOT()).to.equal(FIRST_SUPPORTED_SLOT); + expect(await validatorExitDelayVerifier.PIVOT_SLOT()).to.equal(PIVOT_SLOT); + expect(await validatorExitDelayVerifier.SLOTS_PER_EPOCH()).to.equal(SLOTS_PER_EPOCH); + expect(await validatorExitDelayVerifier.SECONDS_PER_SLOT()).to.equal(SECONDS_PER_SLOT); + expect(await validatorExitDelayVerifier.GENESIS_TIME()).to.equal(GENESIS_TIME); + expect(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()).to.equal( SHARD_COMMITTEE_PERIOD_IN_SECONDS, ); }); it("reverts with 'InvalidPivotSlot' if firstSupportedSlot > pivotSlot", async () => { await expect( - ethers.deployContract("ValidatorExitVerifier", [ + ethers.deployContract("ValidatorExitDelayVerifier", [ LIDO_LOCATOR, GI_FIRST_VALIDATOR_PREV, GI_FIRST_VALIDATOR_CURR, @@ -96,15 +96,15 @@ describe("ValidatorExitVerifier.sol", () => { GENESIS_TIME, SHARD_COMMITTEE_PERIOD_IN_SECONDS, ]), - ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidPivotSlot"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidPivotSlot"); }); }); - describe("verifyActiveValidatorsAfterExitRequest method", () => { + describe("verifyValidatorExitDelay method", () => { const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000056000000000028"; const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000003b00"; - let validatorExitVerifier: ValidatorExitVerifier; + let validatorExitDelayVerifier: ValidatorExitDelayVerifier; let locator: ILidoLocator; let locatorAddr: string; @@ -125,7 +125,7 @@ describe("ValidatorExitVerifier.sol", () => { locator = await deployLidoLocator({ validatorsExitBusOracle: veboAddr, stakingRouter: stakingRouterAddr }); locatorAddr = await locator.getAddress(); - validatorExitVerifier = await ethers.deployContract("ValidatorExitVerifier", [ + validatorExitDelayVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ locatorAddr, GI_FIRST_VALIDATOR_INDEX, GI_FIRST_VALIDATOR_INDEX, @@ -172,7 +172,7 @@ describe("ValidatorExitVerifier.sol", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const tx = await validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + const tx = await validatorExitDelayVerifier.verifyValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, @@ -222,7 +222,7 @@ describe("ValidatorExitVerifier.sol", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - const tx = await validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + const tx = await validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], @@ -249,7 +249,7 @@ describe("ValidatorExitVerifier.sol", () => { }; await expect( - validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyValidatorExitDelay( { rootsTimestamp: 1n, header: invalidHeader, @@ -257,13 +257,13 @@ describe("ValidatorExitVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "UnsupportedSlot"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); }); it("reverts with 'RootNotFound' if the staticcall to the block roots contract fails/returns empty", async () => { const badTimestamp = 999_999_999; await expect( - validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyValidatorExitDelay( { rootsTimestamp: badTimestamp, header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, @@ -271,7 +271,7 @@ describe("ValidatorExitVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "RootNotFound"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "RootNotFound"); }); it("reverts with 'InvalidBlockHeader' if the block root from contract doesn't match the header root", async () => { @@ -279,12 +279,12 @@ describe("ValidatorExitVerifier.sol", () => { const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); await expect( - validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidBlockHeader"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); it("reverts if the validator proof is incorrect", async () => { @@ -327,7 +327,7 @@ describe("ValidatorExitVerifier.sol", () => { }; await expect( - validatorExitVerifier.verifyActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, timestamp), [badWitness], encodedExitRequests, @@ -342,7 +342,7 @@ describe("ValidatorExitVerifier.sol", () => { }; await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( { rootsTimestamp: 1n, header: invalidHeader, @@ -351,7 +351,7 @@ describe("ValidatorExitVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "UnsupportedSlot"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); }); it("reverts with 'UnsupportedSlot' if oldBlock slot < FIRST_SUPPORTED_SLOT", async () => { @@ -363,7 +363,7 @@ describe("ValidatorExitVerifier.sol", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), { header: invalidHeader, @@ -373,19 +373,19 @@ describe("ValidatorExitVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "UnsupportedSlot"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); }); it("reverts with 'RootNotFound' if block root contract call fails", async () => { const badTimestamp = 999_999_999; await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, badTimestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "RootNotFound"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "RootNotFound"); }); it("reverts with 'InvalidBlockHeader' if returned root doesn't match the new block header root", async () => { @@ -393,13 +393,13 @@ describe("ValidatorExitVerifier.sol", () => { const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidBlockHeader"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); it("reverts with 'InvalidGIndex' if oldBlock.rootGIndex is not under the historicalSummaries root", async () => { @@ -409,7 +409,7 @@ describe("ValidatorExitVerifier.sol", () => { const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), { header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, @@ -419,14 +419,14 @@ describe("ValidatorExitVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitVerifier, "InvalidGIndex"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); }); it("reverts if the oldBlock proof is corrupted", async () => { const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), { header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, @@ -483,7 +483,7 @@ describe("ValidatorExitVerifier.sol", () => { }; await expect( - validatorExitVerifier.verifyHistoricalActiveValidatorsAfterExitRequest( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), [badWitness], diff --git a/test/0.8.25/validatorExitVerifierHelpers.ts b/test/0.8.25/validatorExitDelayVerifierHelpers.ts similarity index 97% rename from test/0.8.25/validatorExitVerifierHelpers.ts rename to test/0.8.25/validatorExitDelayVerifierHelpers.ts index 52604d8836..eb9401b80c 100644 --- a/test/0.8.25/validatorExitVerifierHelpers.ts +++ b/test/0.8.25/validatorExitDelayVerifierHelpers.ts @@ -5,7 +5,7 @@ import { HistoricalHeaderWitnessStruct, ProvableBeaconBlockHeaderStruct, ValidatorWitnessStruct, -} from "typechain-types/contracts/0.8.25/ValidatorExitVerifier"; +} from "typechain-types/contracts/0.8.25/ValidatorExitDelayVerifier"; import { de0x, findEventsWithInterfaces, numberToHex } from "lib"; diff --git a/test/0.8.9/lidoLocator.test.ts b/test/0.8.9/lidoLocator.test.ts index 0fd20a2e4e..f0c094ba4a 100644 --- a/test/0.8.9/lidoLocator.test.ts +++ b/test/0.8.9/lidoLocator.test.ts @@ -21,7 +21,7 @@ const services = [ "withdrawalQueue", "withdrawalVault", "oracleDaemonConfig", - "validatorExitVerifier", + "validatorExitDelayVerifier", ] as const; type ArrayToUnion = A[number]; diff --git a/test/deploy/locator.ts b/test/deploy/locator.ts index eb30806e2f..7305183342 100644 --- a/test/deploy/locator.ts +++ b/test/deploy/locator.ts @@ -28,7 +28,7 @@ async function deployDummyLocator(config?: Partial, de validatorsExitBusOracle: certainAddress("dummy-locator:validatorsExitBusOracle"), withdrawalQueue: certainAddress("dummy-locator:withdrawalQueue"), withdrawalVault: certainAddress("dummy-locator:withdrawalVault"), - validatorExitVerifier: certainAddress("dummy-locator:validatorExitVerifier"), + validatorExitDelayVerifier: certainAddress("dummy-locator:validatorExitDelayVerifier"), ...config, }); From 24964a0206fd77bb90d3145d5172d7ba44ea687a Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 15 May 2025 19:01:22 +0200 Subject: [PATCH 127/405] feat: enhance NodeOperatorsRegistry with exit penalty cutoff and reporting window parameters --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 63 ++++++++++++--- test/0.4.24/nor/nor.aux.test.ts | 3 +- test/0.4.24/nor/nor.exit.manager.test.ts | 76 ++++++++++++++++++- .../0.4.24/nor/nor.initialize.upgrade.test.ts | 15 ++-- test/0.4.24/nor/nor.limits.test.ts | 3 +- test/0.4.24/nor/nor.management.flow.test.ts | 4 +- .../nor/nor.rewards.penalties.flow.test.ts | 3 +- test/0.4.24/nor/nor.signing.keys.test.ts | 3 +- test/0.4.24/nor/nor.staking.limit.test.ts | 3 +- 9 files changed, 143 insertions(+), 30 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 33dba70a32..c5469bbb0f 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -75,7 +75,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 withdrawalRequestPaidFee, uint256 exitType ); - event ExitDeadlineThresholdChanged(uint256 threshold); + event ExitDeadlineThresholdChanged(uint256 threshold, uint256 reportingWindow); // Enum to represent the state of the reward distribution process enum RewardDistributionState { @@ -185,6 +185,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Threshold in seconds after which a delayed exit is penalized // bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = keccak256("lido.NodeOperatorsRegistry.exitDelayThresholdSeconds"); bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = 0x96656d3ece9cdbe3bd729ff6d7df8d0aeb457ff7c7c42372184ae30b10b37976; + // Cutoff timestamp used to protect validators from penalization after threshold changes + // bytes32 internal constant EXIT_PENALTY_CUTOFF_TIMESTAMP = keccak256("lido.NodeOperatorsRegistry.exitPenaltyCutoffTimestamp"); + bytes32 internal constant EXIT_PENALTY_CUTOFF_TIMESTAMP = 0x93f1d4cdf7a6d0aac32b989ca335f5ae5f4322e4361b8f67a199fdda105f821b; // @@ -240,7 +243,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // // METHODS // - function initialize(address _locator, bytes32 _type, uint256 _exitDeadlineThresholdInSeconds) public onlyInit { + function initialize( + address _locator, + bytes32 _type, + uint256 _exitDeadlineThresholdInSeconds, + uint256 _exitPenaltyCutoffTimestamp + ) public onlyInit { // Initializations for v1 --> v2 _initialize_v2(_locator, _type); @@ -248,7 +256,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _initialize_v3(); // Initializations for v3 --> v4 - _initialize_v4(_exitDeadlineThresholdInSeconds); + _initialize_v4(_exitDeadlineThresholdInSeconds, _exitPenaltyCutoffTimestamp); initialized(); } @@ -272,9 +280,14 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.Distributed); } - function _initialize_v4(uint256 _exitDeadlineThresholdInSeconds) internal { + function _initialize_v4( + uint256 _exitDeadlineThresholdInSeconds, + uint256 _exitPenaltyCutoffTimestamp + ) internal { _setContractVersion(4); + EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_exitDeadlineThresholdInSeconds); + EXIT_PENALTY_CUTOFF_TIMESTAMP.setStorageUint256(_exitPenaltyCutoffTimestamp); } /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. @@ -286,10 +299,16 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L298 /// function finalizeUpgrade_v3() external - function finalizeUpgrade_v4(uint256 _exitDeadlineThresholdInSeconds) external { + /// @notice Finalizes upgrade to version 4 by initializing new exit-related parameters. + /// @param _exitDeadlineThresholdInSeconds Exit deadline threshold in seconds for validator exits. + /// @param _exitPenaltyCutoffTimestamp Cutoff timestamp before which validators cannot be penalized. + function finalizeUpgrade_v4( + uint256 _exitDeadlineThresholdInSeconds, + uint256 _exitPenaltyCutoffTimestamp + ) external { require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); _checkContractVersion(3); - _initialize_v4(_exitDeadlineThresholdInSeconds); + _initialize_v4(_exitDeadlineThresholdInSeconds, _exitPenaltyCutoffTimestamp); } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators @@ -1072,13 +1091,30 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); } - /// @notice Sets the number of seconds after which a validator is considered late for exit. - /// @param _threshold The new exit deadline threshold in seconds. - function setExitDeadlineThreshold(uint256 _threshold) external { + /// @notice Returns the cutoff timestamp before which validators cannot be penalized for delayed exit. + /// @return uint256 The cutoff timestamp used when evaluating late exits. + function exitPenaltyCutoffTimestamp() public view returns (uint256) { + return EXIT_PENALTY_CUTOFF_TIMESTAMP.getStorageUint256(); + } + + + /// @notice Sets the validator exit deadline threshold and the reporting window for late exits. + /// @dev Updates the cutoff timestamp before which validators are protected from penalization. + /// Prevents penalizing validators whose exit eligibility began before the new policy took effect. + /// @param _threshold Number of seconds a validator has to exit after becoming eligible. + /// @param _reportingWindow Additional number of seconds during which a late exit can still be reported. + function setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) external { _auth(MANAGE_NODE_OPERATOR_ROLE); require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); + require(_reportingWindow > 0, "INVALID_REPORTING_WINDOW"); + EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_threshold); - emit ExitDeadlineThresholdChanged(_threshold); + + // Set the cutoff timestamp to the current time minus the threshold and reportingWindow period + uint256 currentCutoffTimestamp = block.timestamp - _threshold - _reportingWindow; + EXIT_PENALTY_CUTOFF_TIMESTAMP.setStorageUint256(currentCutoffTimestamp); + + emit ExitDeadlineThresholdChanged(_threshold, _reportingWindow); } /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. @@ -1101,11 +1137,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @notice Determines whether a validator's exit status should be updated and will have an effect on the Node Operator. /// @param _publicKey The public key of the validator. + /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. /// @param _eligibleToExitInSec The number of seconds the validator was eligible to exit but did not. /// @return True if the validator has exceeded the exit deadline threshold and hasn't been reported yet. function isValidatorExitDelayPenaltyApplicable( uint256, // _nodeOperatorId - uint256, // _proofSlotTimestamp + uint256 _proofSlotTimestamp, bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool) { @@ -1114,7 +1151,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { if (_isValidatorExitingKeyReported(processedKeyHash)) { return false; } - return _eligibleToExitInSec >= exitDeadlineThreshold(); + return _eligibleToExitInSec >= exitDeadlineThreshold() + && _proofSlotTimestamp >= exitPenaltyCutoffTimestamp(); } /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. @@ -1135,6 +1173,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Check if exit delay exceeds the threshold require(_eligibleToExitInSec >= exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); + require(_proofSlotTimestamp >= exitPenaltyCutoffTimestamp(), "EXIT_PENALTY_CUTOFF_NOT_REACHED"); bytes32 processedKeyHash = keccak256(_publicKey); _markValidatorExitingKeyAsReported(processedKeyHash); diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index 5159daddb5..db0717bfa1 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -72,6 +72,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const contractVersionV2 = 2n; const contractVersionV3 = 3n; @@ -119,7 +120,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index b7bcb91b68..52172da363 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -57,6 +57,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const testPublicKey = "0x" + "0".repeat(48 * 2); const eligibleToExitInSec = 86400n; // 2 days @@ -103,7 +104,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "RewardDistributionStateChanged") .withArgs(RewardDistributionState.Distributed); @@ -230,4 +231,77 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { expect(shouldPenalize).to.be.false; }); }); + + context("exitPenaltyCutoffTimestamp", () => { + const threshold = 86400n; // 1 day + // eslint-disable-next-line @typescript-eslint/no-shadow + const reportingWindow = 3600n; // 1 hour + // eslint-disable-next-line @typescript-eslint/no-shadow + const eligibleToExitInSec = threshold + 100n; + + let cutoff: bigint; + + beforeEach(async () => { + // Set threshold and grace period via contract method + const tx = await nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(threshold, reportingWindow); + + // Fetch actual cutoff timestamp from the contract + cutoff = BigInt(await nor.exitPenaltyCutoffTimestamp()); + + // Get the block timestamp of the transaction + const block = await deployer.provider.getBlock(tx.blockNumber!); + const expectedCutoff = BigInt(block!.timestamp) - threshold - reportingWindow; + + // Ensure cutoff was set correctly + expect(cutoff).to.equal(expectedCutoff); + }); + + it("returns false when _proofSlotTimestamp < cutoff", async () => { + const result = await nor.isValidatorExitDelayPenaltyApplicable( + firstNodeOperatorId, + cutoff - 1n, + testPublicKey, + eligibleToExitInSec, + ); + expect(result).to.be.false; + }); + + it("returns true when _proofSlotTimestamp == cutoff", async () => { + const result = await nor.isValidatorExitDelayPenaltyApplicable( + firstNodeOperatorId, + cutoff, + testPublicKey, + eligibleToExitInSec, + ); + expect(result).to.be.true; + }); + + it("returns true when _proofSlotTimestamp > cutoff", async () => { + const result = await nor.isValidatorExitDelayPenaltyApplicable( + firstNodeOperatorId, + cutoff + 1n, + testPublicKey, + eligibleToExitInSec, + ); + expect(result).to.be.true; + }); + + it("reverts reportValidatorExitDelay when _proofSlotTimestamp < cutoff", async () => { + await expect( + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, cutoff - 1n, testPublicKey, eligibleToExitInSec), + ).to.be.revertedWith("EXIT_PENALTY_CUTOFF_NOT_REACHED"); + }); + + it("emits event when reportValidatorExitDelay is called with _proofSlotTimestamp >= cutoff", async () => { + await expect( + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, cutoff, testPublicKey, eligibleToExitInSec), + ) + .to.emit(nor, "ValidatorExitStatusUpdated") + .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff); + }); + }); }); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 7b0d97913f..846b794a91 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -88,31 +88,26 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { }); it("Reverts if Locator is zero address", async () => { - await expect(nor.initialize(ZeroAddress, moduleType, 86400n)).to.be.reverted; + await expect(nor.initialize(ZeroAddress, moduleType, 86400n, 86400n)).to.be.reverted; }); it("Reverts if was initialized with v1", async () => { - const MAX_STUCK_PENALTY_DELAY = await nor.exitDeadlineThreshold(); await nor.harness__initialize(1n); - await expect(nor.initialize(locator, moduleType, MAX_STUCK_PENALTY_DELAY)).to.be.revertedWith( - "INIT_ALREADY_INITIALIZED", - ); + await expect(nor.initialize(locator, moduleType, 86400n, 86400n)).to.be.revertedWith("INIT_ALREADY_INITIALIZED"); }); it("Reverts if already initialized", async () => { - await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), 86400n); + await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), 86400n, 86400n); - await expect(nor.initialize(locator, moduleType, 86400n)).to.be.revertedWith( - "INIT_ALREADY_INITIALIZED", - ); + await expect(nor.initialize(locator, moduleType, 86400n, 86400n)).to.be.revertedWith("INIT_ALREADY_INITIALIZED"); }); it("Makes the contract initialized to v4", async () => { const burnerAddress = await locator.burner(); const latestBlock = BigInt(await time.latestBlock()); - await expect(nor.initialize(locator, moduleType, 86400n)) + await expect(nor.initialize(locator, moduleType, 86400n, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.limits.test.ts b/test/0.4.24/nor/nor.limits.test.ts index 052b918f33..3cb383cb3c 100644 --- a/test/0.4.24/nor/nor.limits.test.ts +++ b/test/0.4.24/nor/nor.limits.test.ts @@ -78,6 +78,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const contractVersionV2 = 2n; const contractVersionV3 = 3n; @@ -122,7 +123,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.management.flow.test.ts b/test/0.4.24/nor/nor.management.flow.test.ts index ac4d51d28a..5dd848712d 100644 --- a/test/0.4.24/nor/nor.management.flow.test.ts +++ b/test/0.4.24/nor/nor.management.flow.test.ts @@ -24,7 +24,6 @@ import { import { addAragonApp, deployLidoDaoForNor } from "test/deploy"; import { Snapshot } from "test/suite"; - describe("NodeOperatorsRegistry.sol:management", () => { let deployer: HardhatEthersSigner; let user: HardhatEthersSigner; @@ -89,6 +88,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const contractVersion = 2n; before(async () => { @@ -135,7 +135,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts index f1c0868dac..3ebcab3288 100644 --- a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts +++ b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts @@ -79,6 +79,7 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const contractVersion = 2n; before(async () => { @@ -128,7 +129,7 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.signing.keys.test.ts b/test/0.4.24/nor/nor.signing.keys.test.ts index d2898c4fc6..7e971ded91 100644 --- a/test/0.4.24/nor/nor.signing.keys.test.ts +++ b/test/0.4.24/nor/nor.signing.keys.test.ts @@ -99,6 +99,7 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const contractVersion = 2n; const firstNOKeys = new FakeValidatorKeys(5, { kFill: "a", sFill: "b" }); @@ -149,7 +150,7 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), deployer); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.staking.limit.test.ts b/test/0.4.24/nor/nor.staking.limit.test.ts index 87f8fedb42..244c21ab46 100644 --- a/test/0.4.24/nor/nor.staking.limit.test.ts +++ b/test/0.4.24/nor/nor.staking.limit.test.ts @@ -82,6 +82,7 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; + const reportingWindow = 86400n; const contractVersion = 2n; before(async () => { @@ -125,7 +126,7 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") From fe923e8fedd3326c14ce35843114f37376bcc220 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 15 May 2025 19:29:21 +0200 Subject: [PATCH 128/405] feat: update reporting window parameter and adjust initialization logic --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 14 ++++++++------ scripts/scratch/deployed-testnet-defaults.json | 6 ++++-- .../steps/0120-initialize-non-aragon-contracts.ts | 4 +++- test/0.4.24/nor/nor.exit.manager.test.ts | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c5469bbb0f..9bb3a0a99d 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -247,7 +247,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { address _locator, bytes32 _type, uint256 _exitDeadlineThresholdInSeconds, - uint256 _exitPenaltyCutoffTimestamp + uint256 _reportingWindow ) public onlyInit { // Initializations for v1 --> v2 _initialize_v2(_locator, _type); @@ -256,7 +256,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _initialize_v3(); // Initializations for v3 --> v4 - _initialize_v4(_exitDeadlineThresholdInSeconds, _exitPenaltyCutoffTimestamp); + _initialize_v4(_exitDeadlineThresholdInSeconds, _reportingWindow); initialized(); } @@ -282,12 +282,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _initialize_v4( uint256 _exitDeadlineThresholdInSeconds, - uint256 _exitPenaltyCutoffTimestamp + uint256 _reportingWindow ) internal { _setContractVersion(4); - EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_exitDeadlineThresholdInSeconds); - EXIT_PENALTY_CUTOFF_TIMESTAMP.setStorageUint256(_exitPenaltyCutoffTimestamp); + _setExitDeadlineThreshold(_exitDeadlineThresholdInSeconds, _reportingWindow); } /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. @@ -1097,7 +1096,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return EXIT_PENALTY_CUTOFF_TIMESTAMP.getStorageUint256(); } - /// @notice Sets the validator exit deadline threshold and the reporting window for late exits. /// @dev Updates the cutoff timestamp before which validators are protected from penalization. /// Prevents penalizing validators whose exit eligibility began before the new policy took effect. @@ -1105,6 +1103,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @param _reportingWindow Additional number of seconds during which a late exit can still be reported. function setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) external { _auth(MANAGE_NODE_OPERATOR_ROLE); + _setExitDeadlineThreshold(_threshold, _reportingWindow); + } + + function _setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) internal { require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); require(_reportingWindow > 0, "INVALID_REPORTING_WINDOW"); diff --git a/scripts/scratch/deployed-testnet-defaults.json b/scripts/scratch/deployed-testnet-defaults.json index ead6d617e8..ff591f7519 100644 --- a/scripts/scratch/deployed-testnet-defaults.json +++ b/scripts/scratch/deployed-testnet-defaults.json @@ -133,14 +133,16 @@ "deployParameters": { "stakingModuleName": "Curated", "stakingModuleTypeId": "curated-onchain-v1", - "exitDeadlineThresholdInSeconds": 86400 + "exitDeadlineThresholdInSeconds": 86400, + "exitsReportingWindow": 86400 } }, "simpleDvt": { "deployParameters": { "stakingModuleName": "SimpleDVT", "stakingModuleTypeId": "curated-onchain-v1", - "exitDeadlineThresholdInSeconds": 86400 + "exitDeadlineThresholdInSeconds": 86400, + "exitsReportingWindow": 86400 } }, "withdrawalQueueERC721": { diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index 2f7fd1e796..68b421286a 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -50,7 +50,8 @@ export async function main() { [ lidoLocatorAddress, encodeStakingModuleTypeId(nodeOperatorsRegistryParams.stakingModuleTypeId), - simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, + nodeOperatorsRegistryParams.exitDeadlineThresholdInSeconds, + nodeOperatorsRegistryParams.exitsReportingWindow, ], { from: deployer }, ); @@ -63,6 +64,7 @@ export async function main() { lidoLocatorAddress, encodeStakingModuleTypeId(simpleDvtRegistryParams.stakingModuleTypeId), simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, + simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, ], { from: deployer }, ); diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 52172da363..60f9b41d25 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -61,7 +61,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { const testPublicKey = "0x" + "0".repeat(48 * 2); const eligibleToExitInSec = 86400n; // 2 days - const proofSlotTimestamp = 1234567890n; + const proofSlotTimestamp = (1n << 256n) - 1n; // 2^256 - 1 max value for uint256 const withdrawalRequestPaidFee = 100000n; const exitType = 1n; From e48b53fb178ea6441deea8b81444063c7e7a8790 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 16 May 2025 01:09:06 +0400 Subject: [PATCH 129/405] fix: restore limits over time frames Implement library for exit limits that restore part of the limit after each time frame. --- .../0.8.9/TriggerableWithdrawalGateway.sol | 119 ++++- .../0.8.9/interfaces/IValidatorsExitBus.sol | 13 +- contracts/0.8.9/lib/ExitLimitUtils.sol | 206 +++----- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 197 ++++--- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 18 +- .../contracts/ExitLimitUtils__Harness.sol | 57 ++- .../TriggerableWithdrawalGateway__Harness.sol | 21 + test/0.8.9/lib/exitLimitUtils.test.ts | 483 +++++++++++++----- ...-bus-oracle.submitExitRequestsData.test.ts | 13 +- ...r-exit-bus-oracle.submitReportData.test.ts | 8 +- ...awalGateway.triggerFullWithdrawals.test.ts | 112 +++- 11 files changed, 885 insertions(+), 362 deletions(-) create mode 100644 test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol diff --git a/contracts/0.8.9/TriggerableWithdrawalGateway.sol b/contracts/0.8.9/TriggerableWithdrawalGateway.sol index 9007e23eb4..cac3d6ae9f 100644 --- a/contracts/0.8.9/TriggerableWithdrawalGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalGateway.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.9; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; +import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; interface IWithdrawalVault { function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; @@ -27,6 +28,9 @@ interface IStakingRouter { * This contract is responsible for limiting TWRs, checking ADD_FULL_WITHDRAWAL_REQUEST_ROLE role before it gets to Withdrawal Vault. */ contract TriggerableWithdrawalGateway is AccessControlEnumerable { + using ExitLimitUtilsStorage for bytes32; + using ExitLimitUtils for ExitRequestLimitData; + /// @dev Errors /** * @notice Thrown when an invalid zero value is passed @@ -51,13 +55,12 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { * @notice Thrown when a withdrawal fee refund failed */ error TriggerableWithdrawalFeeRefundFailed(); - /** - * @notice Emitted when someone with ADD_FULL_WITHDRAWAL_REQUEST_ROLE role request to process TWR. - * @param stakingModuleId Module id - * @param nodeOperatorId Operator id - * @param validatorPubkey Validator public key - * @param timestamp Block timestamp + * @notice Emitted when an entity with the ADD_FULL_WITHDRAWAL_REQUEST_ROLE requests to process a TWR (triggerable withdrawal request). + * @param stakingModuleId Module id. + * @param nodeOperatorId Operator id. + * @param validatorPubkey Validator public key. + * @param timestamp Block timestamp. */ event TriggerableExitRequest( uint256 indexed stakingModuleId, @@ -65,6 +68,19 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { bytes validatorPubkey, uint256 timestamp ); + /** + * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. + * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @param exitsPerFrame The number of exits that can be restored per frame. + * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + */ + event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); + /** + * @notice Thrown when remaining exit requests limit is not enough to cover sender requests + * @param requestsCount Amount of requests that were sent for processing + * @param remainingLimit Amount of requests that still can be processed at current day + */ + error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); struct ValidatorData { uint256 stakingModuleId; @@ -73,6 +89,9 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { } bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); + bytes32 public constant TW_EXIT_REPORT_LIMIT_ROLE = keccak256("TW_EXIT_REPORT_LIMIT_ROLE"); + + bytes32 public constant TWR_LIMIT_POSITION = keccak256("lido.TriggerableWithdrawalGateway.maxExitRequestLimit"); /// Length in bytes of packed triggerable exit request uint256 internal constant PACKED_EXIT_REQUEST_LENGTH = 56; @@ -130,27 +149,92 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { _checkExitRequestData(triggerableExitData); uint256 requestsCount = triggerableExitData.length / PACKED_EXIT_REQUEST_LENGTH; - uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); + ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); + if (twrLimitData.isExitLimitSet()) { + uint256 timestamp = _getTimestamp(); + uint256 limit = twrLimitData.calculateCurrentExitLimit(timestamp); + + if (limit < requestsCount) { + revert ExitRequestsLimit(requestsCount, limit); + } + + TWR_LIMIT_POSITION.setStorageExitRequestLimit( + twrLimitData.updatePrevExitLimit(limit - requestsCount, timestamp) + ); + } + + uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); _checkFee(requestsCount, withdrawalFee); - // TODO: this method will be covered with limits bytes memory pubkeys = new bytes(requestsCount * PUBLIC_KEY_LENGTH); for (uint256 i = 0; i < requestsCount; ++i) { ValidatorData memory data = _parseExitRequestData(triggerableExitData, i); _copyPubkey(data.pubkey, pubkeys, i); - - // TODO: is it correct to send here withdrawalFee? _notifyStakingModule(data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalFee, exitType); - emit TriggerableExitRequest(data.stakingModuleId, data.nodeOperatorId, data.pubkey, block.timestamp); + emit TriggerableExitRequest(data.stakingModuleId, data.nodeOperatorId, data.pubkey, _getTimestamp()); } _addWithdrawalRequest(requestsCount, withdrawalFee, pubkeys, refundRecipient); } + /** + * @notice Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. + * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @param exitsPerFrame The number of exits that can be restored per frame. + * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + */ + function setExitRequestLimit( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration + ) external onlyRole(TW_EXIT_REPORT_LIMIT_ROLE) { + require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + + uint256 timestamp = _getTimestamp(); + + TWR_LIMIT_POSITION.setStorageExitRequestLimit( + TWR_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp + ) + ); + + emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + } + + /** + * @notice Returns information about current limits data + * @return maxExitRequestsLimit Maximum exit requests limit + * @return exitsPerFrame The number of exits that can be restored per frame. + * @return frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @return prevExitRequestsLimit Limit left after previous requests + * @return currentExitRequestsLimit Current exit requests limit + */ + function getExitRequestLimitFullInfo() + external + view + returns ( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration, + uint256 prevExitRequestsLimit, + uint256 currentExitRequestsLimit + ) + { + ExitRequestLimitData memory exitRequestLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); + maxExitRequestsLimit = exitRequestLimitData.maxExitRequestsLimit; + exitsPerFrame = exitRequestLimitData.exitsPerFrame; + frameDuration = exitRequestLimitData.frameDuration; + prevExitRequestsLimit = exitRequestLimitData.prevExitRequestsLimit; + currentExitRequestsLimit = _getCurrentExitLimit(); + } + /// Internal functions function _checkExitRequestData(bytes calldata triggerableExitData) internal pure { @@ -238,4 +322,17 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { return refund; } + + function _getTimestamp() internal view virtual returns (uint256) { + return block.timestamp; // solhint-disable-line not-rely-on-time + } + + function _getCurrentExitLimit() internal view returns (uint256) { + ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); + if (!twrLimitData.isExitLimitSet()) { + return type(uint256).max; + } + + return twrLimitData.calculateCurrentExitLimit(_getTimestamp()); + } } diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol index bc43fd90d2..e73f9f040e 100644 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -31,7 +31,18 @@ interface IValidatorsExitBus { uint8 exitType ) external payable; - function setExitRequestLimit(uint256 exitsDailyLimit, uint256 twExitsDailyLimit) external; + function setExitRequestLimit(uint256 maxExitRequests, uint256 exitsPerFrame, uint256 frameDuration) external; + + function getExitRequestLimitFullInfo() + external + view + returns ( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration, + uint256 prevExitRequestsLimit, + uint256 currentExitRequestsLimit + ); function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index bc3fcaa884..cf2e575a46 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -4,162 +4,124 @@ pragma solidity 0.8.9; import {UnstructuredStorage} from "./UnstructuredStorage.sol"; -// MSB -----------------------------------> LSB -// 256___________160_______________ 64______________ 0 -// |_______________|________________|_______________| -// | dailyLimit | dailyExitCount | currentDay | -// |<-- 96 bits -->| <-- 96 bits -->|<-- 64 bits -->| +// MSB ---------------------------------------------------------------------------------------> LSB +// 160___________________128_____________________96______________64_____________32_______________ 0 +// |______________________|_______________________|_______________|_______________|_______________| +// | maxExitRequestsLimit | prevExitRequestsLimit | prevTimestamp | frameDuration | exitsPerFrame | +// |<------ 32 bits ----->|<------ 32 bits ------>|<-- 32 bits -->|<-- 32 bits -->|<-- 32 bits -->| // struct ExitRequestLimitData { - uint96 dailyLimit; - uint96 dailyExitCount; - uint64 currentDay; + uint32 maxExitRequestsLimit; // Maximum limit + uint32 prevExitRequestsLimit; // Limit left after previous requests + uint32 prevTimestamp; // Timestamp of the last update + uint32 frameDuration; // Seconds that should pass to restore part of exits + uint32 exitsPerFrame; // Restored exits per frame } library ExitLimitUtilsStorage { using UnstructuredStorage for bytes32; - uint256 internal constant DAILY_LIMIT_OFFSET = 160; - uint256 internal constant DAILY_EXIT_COUNT_OFFSET = 64; - uint256 internal constant CURRENT_DAY_OFFSET = 0; + uint256 internal constant EXITS_PER_FRAME_OFFSET = 0; + uint256 internal constant FRAME_DURATION_OFFSET = 32; + uint256 internal constant PREV_TIMESTAMP_OFFSET = 64; + uint256 internal constant PREV_EXIT_REQUESTS_LIMIT_OFFSET = 96; + uint256 internal constant MAX_EXIT_REQUESTS_LIMIT_OFFSET = 128; function getStorageExitRequestLimit(bytes32 _position) internal view returns (ExitRequestLimitData memory data) { - uint256 slotValue = _position.getStorageUint256(); + uint256 slot = _position.getStorageUint256(); - data.currentDay = uint64(slotValue >> CURRENT_DAY_OFFSET); - data.dailyExitCount = uint96(slotValue >> DAILY_EXIT_COUNT_OFFSET); - data.dailyLimit = uint96(slotValue >> DAILY_LIMIT_OFFSET); + data.exitsPerFrame = uint32(slot >> EXITS_PER_FRAME_OFFSET); + data.frameDuration = uint32(slot >> FRAME_DURATION_OFFSET); + data.prevTimestamp = uint32(slot >> PREV_TIMESTAMP_OFFSET); + data.prevExitRequestsLimit = uint32(slot >> PREV_EXIT_REQUESTS_LIMIT_OFFSET); + data.maxExitRequestsLimit = uint32(slot >> MAX_EXIT_REQUESTS_LIMIT_OFFSET); } function setStorageExitRequestLimit(bytes32 _position, ExitRequestLimitData memory _data) internal { - _position.setStorageUint256( - (uint256(_data.currentDay) << CURRENT_DAY_OFFSET) | - (uint256(_data.dailyExitCount) << DAILY_EXIT_COUNT_OFFSET) | - (uint256(_data.dailyLimit) << DAILY_LIMIT_OFFSET) - ); + uint256 value = (uint256(_data.exitsPerFrame) << EXITS_PER_FRAME_OFFSET) | + (uint256(_data.frameDuration) << FRAME_DURATION_OFFSET) | + (uint256(_data.prevTimestamp) << PREV_TIMESTAMP_OFFSET) | + (uint256(_data.prevExitRequestsLimit) << PREV_EXIT_REQUESTS_LIMIT_OFFSET) | + (uint256(_data.maxExitRequestsLimit) << MAX_EXIT_REQUESTS_LIMIT_OFFSET); + + _position.setStorageUint256(value); } } -// TODO: description -// dailyLimit 0 - exits unlimited library ExitLimitUtils { - /** - * @notice Thrown when remaining exit requests limit is not enough to cover sender requests - * @param requestsCount Amount of requests that were sent for processing - * @param remainingLimit Amount of requests that still can be processed at current day - */ - error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); - - /** - * Method check limit and return how much can be processed - * @param requestsCount Amount of requests for processing - * @param currentTimestamp Block timestamp - * @return limit Amount of requests that can be processed - */ - function consumeLimit( - ExitRequestLimitData memory data, - uint256 requestsCount, - uint256 currentTimestamp - ) internal pure returns (uint256 limit) { - uint64 currentDay = uint64(currentTimestamp / 1 days); - - // exits unlimited - if (data.dailyLimit == 0) { - return requestsCount; + // What should happen with limits if pause is enabled + function calculateCurrentExitLimit( + ExitRequestLimitData memory _data, + uint256 timestamp + ) internal pure returns (uint256 currentLimit) { + uint256 secondsPassed = timestamp - _data.prevTimestamp; + + if (secondsPassed < _data.frameDuration || _data.exitsPerFrame == 0) { + return _data.prevExitRequestsLimit; } - if (data.currentDay != currentDay) { - return data.dailyLimit >= requestsCount ? requestsCount : data.dailyLimit; - } + uint256 framesPassed = secondsPassed / _data.frameDuration; + uint256 restoredLimit = framesPassed * _data.exitsPerFrame; - if (data.dailyExitCount >= data.dailyLimit) { - revert ExitRequestsLimit(requestsCount, 0); + uint256 newLimit = _data.prevExitRequestsLimit + restoredLimit; + if (newLimit > _data.maxExitRequestsLimit) { + newLimit = _data.maxExitRequestsLimit; } - uint256 remainingLimit = data.dailyLimit - data.dailyExitCount; - return remainingLimit >= requestsCount ? requestsCount : remainingLimit; - } - - /** - * Method check limit and revert if requests amount is more than limit - * @param requestsCount Amount of requests for processing - * @param currentTimestamp Block timestamp - */ - function checkLimit( - ExitRequestLimitData memory data, - uint256 requestsCount, - uint256 currentTimestamp - ) internal pure { - uint64 currentDay = uint64(currentTimestamp / 1 days); - - // exits unlimited - if (data.dailyLimit == 0) return; - - if (data.currentDay != currentDay) return; - - if (data.dailyExitCount >= data.dailyLimit) { - revert ExitRequestsLimit(requestsCount, 0); - } - - uint256 remainingLimit = data.dailyLimit - data.dailyExitCount; - - if (requestsCount > remainingLimit) { - revert ExitRequestsLimit(requestsCount, remainingLimit); - } + return newLimit; } - /** - * @notice Updates the current request counter and day in the exit limit data - * @param data Exit request limit struct - * @param newCount New requests amount spent during the day - * @param currentTimestamp Block timestamp - */ - function updateRequestsCounter( - ExitRequestLimitData memory data, - uint256 newCount, - uint256 currentTimestamp + function updatePrevExitLimit( + ExitRequestLimitData memory _data, + uint256 newExitRequestLimit, + uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { - require(newCount <= type(uint96).max, "TOO_LARGE_REQUESTS_COUNT_LIMIT"); - - // TODO: Should we count requests when exits are unlimited? - // If a limit is set after a period of unlimited exits, should we account for the requests that already occurred? - // if (data.dailyLimit == 0) return; + require(_data.maxExitRequestsLimit >= newExitRequestLimit, "LIMIT_EXCEEDED"); - uint64 currentDay = uint64(currentTimestamp / 1 days); - - if (data.currentDay != currentDay) { - data.currentDay = currentDay; - data.dailyExitCount = 0; - } + uint256 secondsPassed = timestamp - _data.prevTimestamp; + uint256 framesPassed = secondsPassed / _data.frameDuration; + uint32 passedTime = uint32(framesPassed) * _data.frameDuration; - uint256 updatedCount = uint256(data.dailyExitCount) + newCount; - require(updatedCount <= type(uint96).max, "DAILY_EXIT_COUNT_OVERFLOW"); - require(data.dailyLimit == 0 || updatedCount <= data.dailyLimit, "REQUESTS_COUNT_EXCEED_LIMIT"); + _data.prevExitRequestsLimit = uint32(newExitRequestLimit); + _data.prevTimestamp += passedTime; - data.dailyExitCount = uint96(updatedCount); - - return data; + return _data; } - /** - * @notice Update daily limit - * @param data Exit request limit struct - * @param limit Exit request limit per day - * @param currentTimestamp Block timestamp - */ - function setExitDailyLimit( - ExitRequestLimitData memory data, - uint256 limit, - uint256 currentTimestamp + function setExitLimits( + ExitRequestLimitData memory _data, + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration, + uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { - require(limit <= type(uint96).max, "TOO_LARGE_DAILY_LIMIT"); + // TODO: restrictions on parameters? + // require(maxExitRequests != 0, "ZERO_MAX_LIMIT"); + // require(frameDuration != 0, "ZERO_FRAME_DURATION"); + require(maxExitRequestsLimit <= type(uint32).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); + require(exitsPerFrame <= type(uint32).max, "TOO_LARGE_EXITS_PER_FRAME"); + require(frameDuration <= type(uint32).max, "TOO_LARGE_FRAME_DURATION"); + + _data.exitsPerFrame = uint32(exitsPerFrame); + _data.frameDuration = uint32(frameDuration); + + if ( + // new maxExitRequestsLimit is smaller than prev remaining limit + maxExitRequestsLimit < _data.prevExitRequestsLimit || + // previously exits were unlimited + _data.maxExitRequestsLimit == 0 + ) { + _data.prevExitRequestsLimit = uint32(maxExitRequestsLimit); + } - uint64 day = uint64(currentTimestamp / 1 days); - require(data.currentDay <= day, "INVALID_TIMESTAMP_BACKWARD"); + _data.maxExitRequestsLimit = uint32(maxExitRequestsLimit); + _data.prevTimestamp = uint32(timestamp); - data.dailyLimit = uint96(limit); + return _data; + } - return data; + function isExitLimitSet(ExitRequestLimitData memory _data) internal pure returns (bool) { + return _data.maxExitRequestsLimit != 0; } } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 05592a34c0..99318e208c 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -42,12 +42,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa error InvalidRequestsDataLength(); /** - * @notice Thrown than module id equal to zero + * @notice Thrown when module id equal to zero */ error InvalidRequestsData(); /** - * TODO: maybe this part will be deleted + * @notice Thrown when data submitted for exit requests was not sorted in ascending order or contains duplicates */ error InvalidRequestsDataSortOrder(); @@ -82,14 +82,32 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @notice Thrown when array of indexes of requests in submitted data for triggerable withdrawal is not is not strictly increasing array */ error InvalidKeyIndexSortOrder(); - /** * @notice Thrown when a withdrawal fee refund failed */ error TriggerableWithdrawalFeeRefundFailed(); + /** + * @notice Thrown when remaining exit requests limit is not enough to cover sender requests + * @param requestsCount Amount of requests that were sent for processing + * @param remainingLimit Amount of requests that still can be processed at current day + */ + error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); /// @dev Events - event StoredExitRequestHash(bytes32 exitRequestHash); + + /** + * @notice Emitted when an entity with the SUBMIT_REPORT_HASH_ROLE role submits a hash of the exit requests data. + * @param exitRequestsHash - keccak256 hash of the encoded validators list + */ + event RequestsHashSubmitted(bytes32 exitRequestsHash); + /** + * @notice Emitted when validator exit requested. + * @param stakingModuleId Id of staking module. + * @param nodeOperatorId Id of node operator. + * @param validatorIndex Validator index. + * @param validatorPubkey Public key of validator. + * @param timestamp Block timestamp + */ event ValidatorExitRequest( uint256 indexed stakingModuleId, uint256 indexed nodeOperatorId, @@ -97,15 +115,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes validatorPubkey, uint256 timestamp ); - event ExitRequestsLimitSet(uint256 exitRequestsLimit, uint256 twExitRequestsLimit); + /** + * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. + * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @param exitsPerFrame The number of exits that can be restored per frame. + * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + */ + event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); - event DirectExitRequest( - uint256 indexed stakingModuleId, - uint256 indexed nodeOperatorId, - bytes validatoPubkey, - uint256 timestamp, - address indexed refundRecipient - ); struct RequestStatus { // Total items count in report (by default type(uint256).max, update on first report delivery) uint256 totalItemsCount; @@ -160,7 +177,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); - bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.exitDailyLimit"); + bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); @@ -179,15 +196,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param exitRequestsHash - keccak256 hash of the encoded validators list */ function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { - RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; - _checkExitNotSubmitted(requestStatus); - uint256 contractVersion = getContractVersion(); _storeExitRequestHash(exitRequestsHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); + + emit RequestsHashSubmitted(exitRequestsHash); } /** - * @notice Method for submitting exit requests data + * @notice Method for submitting exit requests data. * * @dev Reverts if: * - The contract is paused. @@ -209,7 +225,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ]; _checkExitSubmitted(requestStatus); - _checkExitRequestData(request); + _checkExitRequestData(request.data, request.dataFormat); _checkContractVersion(requestStatus.contractVersion); // By default, totalItemsCount is set to type(uint256).max. @@ -224,11 +240,20 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert RequestsAlreadyDelivered(); } + uint256 requestsToDeliver = undeliveredItemsCount; ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - uint256 requestsToDeliver = exitRequestLimitData.consumeLimit(undeliveredItemsCount, _getTimestamp()); - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(requestsToDeliver, _getTimestamp()) - ); + if (exitRequestLimitData.isExitLimitSet()) { + uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); + + if (limit == 0) { + revert ExitRequestsLimit(undeliveredItemsCount, 0); + } + + requestsToDeliver = limit >= undeliveredItemsCount ? undeliveredItemsCount : limit; + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitLimit(limit - requestsToDeliver, _getTimestamp()) + ); + } require( requestStatus.totalItemsCount >= requestStatus.deliveredItemsCount + requestsToDeliver, @@ -237,8 +262,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _processExitRequestsList(request.data, requestStatus.deliveredItemsCount, requestsToDeliver); - require(requestStatus.deliveredItemsCount + requestsToDeliver - 1 >= 0, "WRONG_REQUESTS_TO_DELIVER_VALUE"); - requestStatus.deliverHistory.push( DeliveryHistory(requestStatus.deliveredItemsCount + requestsToDeliver - 1, _getTimestamp()) ); @@ -251,8 +274,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param requestsData The report data previously unpacked and emitted by the VEB. * @param keyIndexes Array of indexes pointing to validators in `requestsData.data` * to be exited via TWR. - * @param refundRecipient Address to return extra fee on TW (eip-7002) exit - * @param exitType type of request. 0 - non-refundable, 1 - require refund + * @param refundRecipient Address to return extra fee on TW (eip-7002) exit. + * @param exitType Type of request. 0 - non-refundable, 1 - require refund. * * @dev Reverts if: * - The hash of `requestsData` was not previously submitted in the VEB. @@ -272,13 +295,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa refundRecipient = msg.sender; } - // bytes calldata data = request.data; RequestStatus storage requestStatus = _storageExitRequestsHashes()[ keccak256(abi.encode(requestsData.data, requestsData.dataFormat)) ]; _checkExitSubmitted(requestStatus); - _checkExitRequestData(requestsData); + _checkExitRequestData(requestsData.data, requestsData.dataFormat); _checkContractVersion(requestStatus.contractVersion); bytes memory exits = new bytes(keyIndexes.length * PACKED_TWG_EXIT_REQUEST_LENGTH); @@ -294,7 +316,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert KeyWasNotDelivered(keyIndexes[i], requestStatus.deliveredItemsCount - 1); } - if (i > 0 && keyIndexes[i] <= lastKeyIndex ) { + if (i > 0 && keyIndexes[i] <= lastKeyIndex) { revert InvalidKeyIndexSortOrder(); } lastKeyIndex = keyIndexes[i]; @@ -312,21 +334,58 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } + /** + * @notice Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. + * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @param exitsPerFrame The number of exits that can be restored per frame. + * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + */ function setExitRequestLimit( - uint256 exitsDailyLimit, - uint256 twExitsDailyLimit + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { - require(exitsDailyLimit != 0, "ZERO_MAX_EXIT_REQUEST_LIMIT"); - require(twExitsDailyLimit != 0, "ZERO_MAX_TW_EXIT_REQUEST_LIMIT"); - require(exitsDailyLimit >= twExitsDailyLimit, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); uint256 timestamp = _getTimestamp(); EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitDailyLimit(exitsDailyLimit, timestamp) + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp + ) ); - emit ExitRequestsLimitSet(exitsDailyLimit, twExitsDailyLimit); + emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + } + + /** + * @notice Returns information about current limits data + * @return maxExitRequestsLimit Maximum exit requests limit + * @return exitsPerFrame The number of exits that can be restored per frame. + * @return frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @return prevExitRequestsLimit Limit left after previous requests + * @return currentExitRequestsLimit Current exit requests limit + */ + function getExitRequestLimitFullInfo() + external + view + returns ( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration, + uint256 prevExitRequestsLimit, + uint256 currentExitRequestsLimit + ) + { + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + maxExitRequestsLimit = exitRequestLimitData.maxExitRequestsLimit; + exitsPerFrame = exitRequestLimitData.exitsPerFrame; + frameDuration = exitRequestLimitData.frameDuration; + prevExitRequestsLimit = exitRequestLimitData.prevExitRequestsLimit; + currentExitRequestsLimit = _getCurrentExitLimit(); } /** @@ -346,18 +405,22 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); } + /** + * @notice Returns validator exit request data by index. + * @param exitRequests Encoded list of validator exit requests. + * @param dataFormat Format of the encoded exit request data. Currently, only DATA_FORMAT_LIST = 1 is supported. + * @param index Index of the exit request within the `exitRequests` list. + * @return pubkey Public key of the validator. + * @return nodeOpId ID of the node operator. + * @return moduleId ID of the staking module. + * @return valIndex Index of the validator. + */ function unpackExitRequest( bytes calldata exitRequests, uint256 dataFormat, uint256 index ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { - if (dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(dataFormat); - } - - if (exitRequests.length % PACKED_REQUEST_LENGTH != 0) { - revert InvalidRequestsDataLength(); - } + _checkExitRequestData(exitRequests, dataFormat); if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); @@ -382,35 +445,34 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _resume(); } - /// @notice Pause accepting validator exit requests util in after duration + /// @notice Pause accepting validator exit requests util in after duration. /// - /// @param _duration pause duration, seconds (use `PAUSE_INFINITELY` for unlimited) - /// @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 + /// @param _duration Pause duration, seconds (use `PAUSE_INFINITELY` for unlimited). + /// @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 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 + /// @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); } /// Internal functions - // TODO: fixed to be used in unpackExitRequest too - function _checkExitRequestData(ExitRequestData calldata request) internal pure { - if (request.dataFormat != DATA_FORMAT_LIST) { - revert UnsupportedRequestsDataFormat(request.dataFormat); + function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal pure { + if (dataFormat != DATA_FORMAT_LIST) { + revert UnsupportedRequestsDataFormat(dataFormat); } - if (request.data.length % PACKED_REQUEST_LENGTH != 0) { + if (requests.length % PACKED_REQUEST_LENGTH != 0) { revert InvalidRequestsDataLength(); } } @@ -427,6 +489,15 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } + function _getCurrentExitLimit() internal view returns (uint256) { + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + if (!exitRequestLimitData.isExitLimitSet()) { + return type(uint256).max; + } + + return exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); + } + function _getTimestamp() internal view virtual returns (uint256) { return block.timestamp; // solhint-disable-line not-rely-on-time } @@ -441,7 +512,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); RequestStatus storage request = hashes[exitRequestHash]; - require(request.contractVersion == 0, "Hash already exists"); + _checkExitNotSubmitted(request); request.totalItemsCount = totalItemsCount; request.deliveredItemsCount = deliveredItemsCount; @@ -449,8 +520,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa if (history.timestamp != 0) { request.deliverHistory.push(history); } - - emit StoredExitRequestHash(exitRequestHash); } /// Methods for reading data from tightly packed validator exit requests @@ -499,11 +568,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /** * This method read report data (DATA_FORMAT=1) within a range - * check dataWithoutPubkey <= lastDataWithoutPubkey needs to prevent duplicates - * However, it seems that duplicates are no longer an issue. - * But this logic prevent use of _getValidatorData method here - * - * check what will happen if startIndex bigger than length of data + * Check dataWithoutPubkey <= lastDataWithoutPubkey needs to prevent duplicates */ function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { uint256 offset; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 71380cee73..8e1506e775 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -244,10 +244,15 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { // Check VEB common limit ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - exitRequestLimitData.checkLimit(data.requestsCount, _getTimestamp()); - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updateRequestsCounter(data.requestsCount, _getTimestamp()) - ); + if (exitRequestLimitData.isExitLimitSet()) { + uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); + if (limit < data.requestsCount) { + revert ExitRequestsLimit(data.requestsCount, limit); + } + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitLimit(limit - data.requestsCount, _getTimestamp()) + ); + } if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); @@ -271,17 +276,18 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { ); } - function _storeOracleExitRequestHash(bytes32 exitRequestHash, uint256 requestsCount, uint256 contractVersion) internal { + function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 requestsCount, uint256 contractVersion) internal { if (requestsCount == 0) { return; } _storeExitRequestHash( - exitRequestHash, + exitRequestsHash, requestsCount, requestsCount, contractVersion, DeliveryHistory(requestsCount - 1, _getTimestamp()) ); + emit RequestsHashSubmitted(exitRequestsHash); } /// diff --git a/test/0.8.9/contracts/ExitLimitUtils__Harness.sol b/test/0.8.9/contracts/ExitLimitUtils__Harness.sol index ff05392c87..e44313e633 100644 --- a/test/0.8.9/contracts/ExitLimitUtils__Harness.sol +++ b/test/0.8.9/contracts/ExitLimitUtils__Harness.sol @@ -22,41 +22,54 @@ contract ExitLimitUtilsStorage__Harness { contract ExitLimitUtils__Harness { using ExitLimitUtils for ExitRequestLimitData; - event CheckLimitDone(); - ExitRequestLimitData public state; - function harness_setState(uint96 dailyLimit, uint96 dailyExitCount, uint64 currentDay) external { - state.dailyLimit = dailyLimit; - state.dailyExitCount = dailyExitCount; - state.currentDay = currentDay; + function harness_setState( + uint32 maxExitRequestsLimit, + uint32 prevExitRequestsLimit, + uint32 exitsPerFrame, + uint32 frameDuration, + uint32 timestamp + ) external { + state.maxExitRequestsLimit = maxExitRequestsLimit; + state.exitsPerFrame = exitsPerFrame; + state.frameDuration = frameDuration; + state.prevExitRequestsLimit = prevExitRequestsLimit; + state.prevTimestamp = timestamp; } function harness_getState() external view returns (ExitRequestLimitData memory) { - return ExitRequestLimitData(state.dailyLimit, state.dailyExitCount, state.currentDay); + return + ExitRequestLimitData( + state.maxExitRequestsLimit, + state.prevExitRequestsLimit, + state.prevTimestamp, + state.frameDuration, + state.exitsPerFrame + ); } - function consumeLimit(uint256 requestsCount, uint256 currentTimestamp) external view returns (uint256 limit) { - return state.consumeLimit(requestsCount, currentTimestamp); + function calculateCurrentExitLimit(uint256 currentTimestamp) external view returns (uint256) { + return state.calculateCurrentExitLimit(currentTimestamp); } - function checkLimit(uint256 requestsCount, uint256 currentTimestamp) external { - state.checkLimit(requestsCount, currentTimestamp); - - emit CheckLimitDone(); + function updatePrevExitLimit( + uint256 newExitRequestLimit, + uint256 timestamp + ) external view returns (ExitRequestLimitData memory) { + return state.updatePrevExitLimit(newExitRequestLimit, timestamp); } - function updateRequestsCounter( - uint256 newCount, - uint256 currentTimestamp + function setExitLimits( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration, + uint256 timestamp ) external view returns (ExitRequestLimitData memory) { - return state.updateRequestsCounter(newCount, currentTimestamp); + return state.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); } - function setExitDailyLimit( - uint256 limit, - uint256 currentTimestamp - ) external view returns (ExitRequestLimitData memory) { - return state.setExitDailyLimit(limit, currentTimestamp); + function isExitLimitSet() external view returns (bool) { + return state.isExitLimitSet(); } } diff --git a/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol new file mode 100644 index 0000000000..4ce4e0ded8 --- /dev/null +++ b/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol @@ -0,0 +1,21 @@ +pragma solidity 0.8.9; + +import {TriggerableWithdrawalGateway} from "contracts/0.8.9/TriggerableWithdrawalGateway.sol"; + +contract TriggerableWithdrawalGateway__Harness is TriggerableWithdrawalGateway { + uint256 internal _time = 2513040315; + + constructor(address lidoLocator) TriggerableWithdrawalGateway(lidoLocator) {} + + function getTimestamp() external view returns (uint256) { + return _time; + } + + function _getTimestamp() internal view override returns (uint256) { + return _time; + } + + function advanceTimeBy(uint256 timeAdvance) external { + _time += timeAdvance; + } +} diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts index 218d08eeac..fd025b0e72 100644 --- a/test/0.8.9/lib/exitLimitUtils.test.ts +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -4,13 +4,13 @@ import { ethers } from "hardhat"; import { ExitLimitUtils__Harness, ExitLimitUtilsStorage__Harness } from "typechain-types"; interface ExitRequestLimitData { - dailyLimit: bigint; - dailyExitCount: bigint; - currentDay: bigint; + maxExitRequestsLimit: bigint; + prevExitRequestsLimit: bigint; + prevTimestamp: bigint; + frameDuration: bigint; + exitsPerFrame: bigint; } -const DAY = 86400; - describe("ExitLimitUtils.sol", () => { let exitLimitStorage: ExitLimitUtilsStorage__Harness; let exitLimit: ExitLimitUtils__Harness; @@ -25,195 +25,438 @@ describe("ExitLimitUtils.sol", () => { it("Min possible values", async () => { data = { - dailyLimit: 0n, - dailyExitCount: 0n, - currentDay: 0n, + maxExitRequestsLimit: 0n, + prevExitRequestsLimit: 0n, + prevTimestamp: 0n, + frameDuration: 0n, + exitsPerFrame: 0n, }; await exitLimitStorage.setStorageExitRequestLimit(data); const result = await exitLimitStorage.getStorageExitRequestLimit(); - expect(result.dailyLimit).to.equal(0n); - expect(result.dailyExitCount).to.equal(0n); - expect(result.currentDay).to.equal(0n); + expect(result.maxExitRequestsLimit).to.equal(0n); + expect(result.prevExitRequestsLimit).to.equal(0n); + expect(result.prevTimestamp).to.equal(0n); + expect(result.frameDuration).to.equal(0n); + expect(result.exitsPerFrame).to.equal(0n); }); it("Max possible values", async () => { - const MAX_UINT96 = 2n ** 96n - 1n; - const MAX_UINT64 = 2n ** 64n - 1n; + const MAX_UINT32 = 2n ** 32n - 1n; data = { - dailyLimit: MAX_UINT96, - dailyExitCount: MAX_UINT96, - currentDay: MAX_UINT64, + maxExitRequestsLimit: MAX_UINT32, + prevExitRequestsLimit: MAX_UINT32, + prevTimestamp: MAX_UINT32, + frameDuration: MAX_UINT32, + exitsPerFrame: MAX_UINT32, }; await exitLimitStorage.setStorageExitRequestLimit(data); const result = await exitLimitStorage.getStorageExitRequestLimit(); - expect(result.dailyLimit).to.equal(MAX_UINT96); - expect(result.dailyExitCount).to.equal(MAX_UINT96); - expect(result.currentDay).to.equal(MAX_UINT64); + expect(result.maxExitRequestsLimit).to.equal(MAX_UINT32); + expect(result.prevExitRequestsLimit).to.equal(MAX_UINT32); + expect(result.prevTimestamp).to.equal(MAX_UINT32); + expect(result.frameDuration).to.equal(MAX_UINT32); + expect(result.exitsPerFrame).to.equal(MAX_UINT32); }); it("Some random values", async () => { - const dailyLimit = 100n; - const dailyExitCount = 50n; - const currentDay = 2n; + const maxExitRequestsLimit = 100n; + const prevExitRequestsLimit = 9n; + const prevTimestamp = 90n; + const frameDuration = 10n; + const exitsPerFrame = 1n; data = { - dailyLimit, - dailyExitCount, - currentDay, + maxExitRequestsLimit, + prevExitRequestsLimit, + prevTimestamp, + frameDuration, + exitsPerFrame, }; await exitLimitStorage.setStorageExitRequestLimit(data); const result = await exitLimitStorage.getStorageExitRequestLimit(); - expect(result.dailyLimit).to.equal(dailyLimit); - expect(result.dailyExitCount).to.equal(dailyExitCount); - expect(result.currentDay).to.equal(currentDay); + expect(result.maxExitRequestsLimit).to.equal(maxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(prevExitRequestsLimit); + expect(result.prevTimestamp).to.equal(prevTimestamp); + expect(result.frameDuration).to.equal(frameDuration); + expect(result.exitsPerFrame).to.equal(exitsPerFrame); }); }); context("ExitLimitUtils", () => { - context("consumeLimit", () => { - it("should allow unlimited exits when dailyLimit was not set", async () => { - await exitLimit.harness_setState(0, 0, 0); - const result = await exitLimit.consumeLimit(100, 0); - expect(result).to.equal(100); + context("calculateCurrentExitLimit", () => { + beforeEach(async () => { + await exitLimit.harness_setState(0, 0, 0, 0, 0); }); - it("should reset on new day and return requestsCount if under limit", async () => { - await exitLimit.harness_setState(10, 5, 1); - const result = await exitLimit.consumeLimit(8, 2n * BigInt(DAY)); - expect(result).to.equal(8); + it("should return prevExitRequestsLimit value (nothing restored), if no time passed", async () => { + const timestamp = 1000; + const maxExitRequestsLimit = 10; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp, + ); + + const result = await exitLimit.calculateCurrentExitLimit(timestamp); + expect(result).to.equal(prevExitRequestsLimit); }); - it("should cap requests to remaining limit", async () => { - await exitLimit.harness_setState(10, 8, 0); - const result = await exitLimit.consumeLimit(5, 0); - expect(result).to.equal(2); + it("should return prevExitRequestsLimit value (nothing restored), if less than one frame passed", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 10; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + 9); + expect(result).to.equal(prevExitRequestsLimit); }); - it("should revert if no limit left", async () => { - await exitLimit.harness_setState(5, 5, 0); - await expect(exitLimit.consumeLimit(1, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + it("Should return prevExitRequestsLimit + 1 (restored one exit), if exactly one frame passed", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 10; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + frameDuration); + expect(result).to.equal(prevExitRequestsLimit + 1); }); - it("should respect new dailyLimit after changing from unlimited", async () => { - await exitLimit.harness_setState(0, 50, 0); - const newData = await exitLimit.setExitDailyLimit(60, 0); - await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); - const result = await exitLimit.consumeLimit(11, 0); - expect(result).to.equal(10); + it("Should return prevExitRequestsLimit + restored value, if multiple full frames passed, restored value does not exceed maxExitRequestsLimit", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 20; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + 40); + expect(result).to.equal(prevExitRequestsLimit + 4); }); - it("should revert if after new dailyLimit dailyEXitCount exceed accepted amount", async () => { - await exitLimit.harness_setState(0, 50, 0); - const newData = await exitLimit.setExitDailyLimit(50, 0); - await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); - await expect(exitLimit.consumeLimit(1, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + it("Should return maxExitRequestsLimit, if restored limit exceeds max", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 100; + const prevExitRequestsLimit = 90; // remaining limit from prev usage + const exitsPerFrame = 3; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + 100); // 10 frames * 3 = 30 + expect(result).to.equal(maxExitRequestsLimit); }); - it("should process new amount of requests if new day come", async () => { - await exitLimit.harness_setState(0, 50, 1); - const newData = await exitLimit.setExitDailyLimit(50, BigInt(DAY)); - await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); - const result = await exitLimit.consumeLimit(1, 2n * BigInt(DAY)); - expect(result).to.equal(1); + it("Should return prevExitRequestsLimit, if exitsPerFrame = 0", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 100; + const prevExitRequestsLimit = 7; // remaining limit from prev usage + const exitsPerFrame = 0; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + 100); + expect(result).to.equal(7); }); - }); - context("checkLimit", () => { - it("should allow unlimited exits when dailyLimit was not set", async () => { - await exitLimit.harness_setState(0, 0, 0); - const tx = await exitLimit.checkLimit(100, 0); - await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + it("non-multiple frame passed (should truncate fractional frame)", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 20; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + 25); + expect(result).to.equal(7); // 5 + 2 }); + }); - it("should reset on new day and pass checks if under limit", async () => { - await exitLimit.harness_setState(10, 5, 1); - const tx = await exitLimit.checkLimit(8, 2n * BigInt(DAY)); - await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + context("updatePrevExitLimit", () => { + beforeEach(async () => { + await exitLimit.harness_setState(0, 0, 0, 0, 0); }); - it("should revert if limit doesnt cover required amount of requests", async () => { - await exitLimit.harness_setState(10, 8, 0); - await expect(exitLimit.checkLimit(5, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + it("should revert with LIMIT_EXCEEDED, if newExitRequestLimit exceeded maxExitRequestsLimit", async () => { + const prevTimestamp = 1000; + + const maxExitRequestsLimit = 10; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + await expect(exitLimit.updatePrevExitLimit(11, prevTimestamp + 10)).to.be.revertedWith("LIMIT_EXCEEDED"); }); - it("should revert if no limit left", async () => { - await exitLimit.harness_setState(5, 5, 0); - await expect(exitLimit.checkLimit(1, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + it("should increase prevTimestamp on frame duration if one frame passed", async () => { + const prevTimestamp = 1000; + + const maxExitRequestsLimit = 10; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const updated = await exitLimit.updatePrevExitLimit(4, prevTimestamp + 10); + expect(updated.prevExitRequestsLimit).to.equal(4); + expect(updated.prevTimestamp).to.equal(prevTimestamp + 10); }); - it("should respect new dailyLimit after changing from unlimited", async () => { - await exitLimit.harness_setState(0, 50, 0); - const newData = await exitLimit.setExitDailyLimit(60, 0); - await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); - const tx = await exitLimit.checkLimit(10, 0); - await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + it("should not change prevTimestamp, as less than frame passed", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 10; + const prevExitRequestsLimit = 5; // remaining limit from prev usage + const exitsPerFrame = 1; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const updated = await exitLimit.updatePrevExitLimit(3, prevTimestamp + 9); + expect(updated.prevExitRequestsLimit).to.equal(3); + expect(updated.prevTimestamp).to.equal(prevTimestamp); }); - it("should revert if after new dailyLimit dailyEXitCount exceed accepted amount", async () => { - await exitLimit.harness_setState(0, 50, 0); - const newData = await exitLimit.setExitDailyLimit(60, 0); - await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); - await expect(exitLimit.checkLimit(11, 0)).to.be.revertedWithCustomError(exitLimit, "ExitRequestsLimit"); + it("should increase prevTimestamp on multiple frames value, if multiple frames passed", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 100; + const prevExitRequestsLimit = 90; // remaining limit from prev usage + const exitsPerFrame = 5; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const updated = await exitLimit.updatePrevExitLimit(85, prevTimestamp + 45); + expect(updated.prevExitRequestsLimit).to.equal(85); + expect(updated.prevTimestamp).to.equal(prevTimestamp + 40); }); - it("should process new amount of requests if new day come", async () => { - await exitLimit.harness_setState(0, 50, 1); - const newData = await exitLimit.setExitDailyLimit(50, BigInt(DAY)); - await exitLimit.harness_setState(newData.dailyLimit, newData.dailyExitCount, newData.currentDay); - const tx = await exitLimit.checkLimit(1, 2n * BigInt(DAY)); - await expect(tx).to.emit(exitLimit, "CheckLimitDone"); + it("should not change prevTimestamp, if no time passed", async () => { + const prevTimestamp = 1000; + const maxExitRequestsLimit = 50; + const prevExitRequestsLimit = 25; // remaining limit from prev usage + const exitsPerFrame = 2; + const frameDuration = 10; + + await exitLimit.harness_setState( + maxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + prevTimestamp, + ); + + const updated = await exitLimit.updatePrevExitLimit(20, prevTimestamp); + expect(updated.prevExitRequestsLimit).to.equal(20); + expect(updated.prevTimestamp).to.equal(prevTimestamp); }); }); - context("updateRequestsCounter", () => { - it("should revert if newCount exceed uint96", async () => { - await exitLimit.harness_setState(0, 0, 0); + context("setExitLimits", () => { + beforeEach(async () => { + await exitLimit.harness_setState(0, 0, 0, 0, 0); + }); - await expect(exitLimit.updateRequestsCounter(2n ** 96n, 2n * BigInt(DAY))).to.be.revertedWith( - "TOO_LARGE_REQUESTS_COUNT_LIMIT", - ); + it("should initialize limits", async () => { + const timestamp = 1000; + const maxExitRequestsLimit = 100; + const exitsPerFrame = 2; + const frameDuration = 10; + + const result = await exitLimit.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + + expect(result.maxExitRequestsLimit).to.equal(maxExitRequestsLimit); + expect(result.exitsPerFrame).to.equal(exitsPerFrame); + expect(result.frameDuration).to.equal(frameDuration); + expect(result.prevExitRequestsLimit).to.equal(maxExitRequestsLimit); + expect(result.prevTimestamp).to.equal(timestamp); }); - it("should reset dailyExitLimit and currentDay on new day", async () => { - await exitLimit.harness_setState(0, 50, 1); + it("should set prevExitRequestsLimit to new maxExitRequestsLimit, if new maxExitRequestsLimit is lower than prevExitRequestsLimit", async () => { + const timestamp = 900; + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 80; + const exitsPerFrame = 2; + const frameDuration = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp, + ); - const result = await exitLimit.updateRequestsCounter(30, 2n * BigInt(DAY)); + const newMaxExitRequestsLimit = 50; + const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); - expect(result.currentDay).to.equal(2); - expect(result.dailyExitCount).to.equal(30); - expect(result.dailyLimit).to.equal(0); + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevTimestamp).to.equal(timestamp); }); - it("should revert if new dailyExitCount exceed uint96", async () => { - await exitLimit.harness_setState(0, 100, 0); + it("should not update prevExitRequestsLimit, if new maxExitRequestsLimit is higher", async () => { + const timestamp = 900; + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 80; + const exitsPerFrame = 2; + const frameDuration = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp, + ); + + const newMaxExitRequestsLimit = 150; + + const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); - await expect(exitLimit.updateRequestsCounter(2n ** 96n - 1n, 0)).to.be.revertedWith( - "DAILY_EXIT_COUNT_OVERFLOW", + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(prevExitRequestsLimit); + expect(result.prevTimestamp).to.equal(timestamp); + }); + + it("should reset prevExitRequestsLimit if old max was zero", async () => { + const timestamp = 900; + const oldMaxExitRequestsLimit = 0; + const prevExitRequestsLimit = 0; + const exitsPerFrame = 2; + const frameDuration = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp, ); + + const newMaxExitRequestsLimit = 77; + const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevTimestamp).to.equal(timestamp); }); - it("should revert if new dailyExitCount more than dailyLimit", async () => { - await exitLimit.harness_setState(100, 50, 0); + it("should revert if maxExitRequestsLimit is too large", async () => { + const MAX_UINT32 = 2 ** 32; + await expect(exitLimit.setExitLimits(MAX_UINT32, 2, 10, 1000)).to.be.revertedWith( + "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT", + ); + }); - await expect(exitLimit.updateRequestsCounter(2n ** 96n - 1n, 0)).to.be.revertedWith( - "DAILY_EXIT_COUNT_OVERFLOW", + it("should revert if exitsPerFrame is too large", async () => { + const MAX_UINT32 = 2 ** 32; + await expect(exitLimit.setExitLimits(100, MAX_UINT32, 10, 1000)).to.be.revertedWith( + "TOO_LARGE_EXITS_PER_FRAME", ); }); - it("should accumulate dailyExitCount even if requests are unlimited", async () => { - await exitLimit.harness_setState(0, 50, 1); + it("should revert if frameDuration is too large", async () => { + const MAX_UINT32 = 2 ** 32; + await expect(exitLimit.setExitLimits(100, 2, MAX_UINT32, 1000)).to.be.revertedWith("TOO_LARGE_FRAME_DURATION"); + }); + }); + + context("isExitLimitSet", () => { + it("returns false when maxExitRequestsLimit is 0", async () => { + await exitLimit.harness_setState(0, 0, 0, 0, 0); + + const result = await exitLimit.isExitLimitSet(); + expect(result).to.be.false; + }); + + it("returns true when maxExitRequestsLimit is non-zero", async () => { + await exitLimit.harness_setState(100, 50, 2, 10, 1000); - const result = await exitLimit.updateRequestsCounter(30, BigInt(DAY)); - expect(result.currentDay).to.equal(1); - expect(result.dailyExitCount).to.equal(80); - expect(result.dailyLimit).to.equal(0); + const result = await exitLimit.isExitLimitSet(); + expect(result).to.be.true; }); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 88bafbf065..34d22837c0 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -108,7 +108,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); - await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(exitRequestHash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); }); it("Emit ValidatorExit event", async () => { @@ -160,7 +160,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const request = [exitRequest.data, 2]; const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], request)); const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); - await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); exitRequest = { dataFormat: 2, data: encodeExitRequestsDataList(exitRequests) }; await expect(oracle.submitExitRequestsData(exitRequest)) .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") @@ -174,8 +174,9 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await consensus.advanceTimeBy(24 * 60 * 60); }); - it("Should deliver request fully as it is below limit (5)", async () => { - await oracle.connect(authorizedEntity).setExitRequestLimit(5, 2); + it("Should deliver request fully as it is below limit", async () => { + const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit(5, 1, 48); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(5, 1, 48); exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, @@ -232,7 +233,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { } }); - it("Should revert when limit exceeded for the day", async () => { + it("Should revert when limit exceeded for the frame", async () => { exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, @@ -252,7 +253,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should process remaining requests after a day passes", async () => { - await consensus.advanceTimeBy(24 * 60 * 60); + await consensus.advanceTimeBy(2 * 4 * 12); exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 0bfa010aff..f7193c3c34 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -320,7 +320,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const encodedData = ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [data, reportData.dataFormat]); const reportDataHash = ethers.keccak256(encodedData); - await expect(tx).to.emit(oracle, "StoredExitRequestHash").withArgs(reportDataHash); + await expect(tx).to.emit(oracle, "RequestsHashSubmitted").withArgs(reportDataHash); }); it("updates processing state", async () => { @@ -609,8 +609,8 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { it("Set exit limit", async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); await oracle.grantRole(role, admin); - const exitLimitTx = await oracle.connect(admin).setExitRequestLimit(7, 7); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(7, 7); + const exitLimitTx = await oracle.connect(admin).setExitRequestLimit(7, 1, 48); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(7, 1, 48); }); it("deliver report by actor different from oracle", async () => { @@ -629,7 +629,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { await oracle.grantRole(role, authorizedEntity); const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); - await expect(submitTx).to.emit(oracle, "StoredExitRequestHash").withArgs(exitRequestHash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); const exitRequest = { dataFormat: reportData.dataFormat, diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 123c449e8a..8292c1e294 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -3,7 +3,11 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { StakingRouter__MockForTWG, TriggerableWithdrawalGateway, WithdrawalVault__MockForTWG } from "typechain-types"; +import { + StakingRouter__MockForTWG, + TriggerableWithdrawalGateway__Harness, + WithdrawalVault__MockForTWG, +} from "typechain-types"; import { de0x, numberToHex } from "lib"; @@ -31,7 +35,7 @@ const exitRequests = [ const ZERO_ADDRESS = ethers.ZeroAddress; describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { - let triggerableWithdrawalGateway: TriggerableWithdrawalGateway; + let triggerableWithdrawalGateway: TriggerableWithdrawalGateway__Harness; let withdrawalVault: WithdrawalVault__MockForTWG; let stakingRouter: StakingRouter__MockForTWG; let admin: HardhatEthersSigner; @@ -61,7 +65,7 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { stakingRouter: await stakingRouter.getAddress(), }); - triggerableWithdrawalGateway = await ethers.deployContract("TriggerableWithdrawalGateway", [locatorAddr]); + triggerableWithdrawalGateway = await ethers.deployContract("TriggerableWithdrawalGateway__Harness", [locatorAddr]); await triggerableWithdrawalGateway.initialize(admin); }); @@ -91,6 +95,14 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { .withArgs(3, 1); }); + it("set limit", async () => { + const role = await triggerableWithdrawalGateway.TW_EXIT_REPORT_LIMIT_ROLE(); + await triggerableWithdrawalGateway.grantRole(role, authorizedEntity); + + const exitLimitTx = await triggerableWithdrawalGateway.connect(authorizedEntity).setExitRequestLimit(4, 1, 48); + await expect(exitLimitTx).to.emit(triggerableWithdrawalGateway, "ExitRequestsLimitSet").withArgs(4, 1, 48); + }); + it("should add withdrawal request", async () => { const requests = encodeTWGExitDataList(exitRequests); @@ -98,7 +110,99 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const timestamp = (await tx.getBlock())?.timestamp; + const timestamp = await triggerableWithdrawalGateway.getTimestamp(); + + const pubkeys = + "0x" + + exitRequests + .map((request) => { + const pubkeyHex = de0x(request.valPubkey); + return pubkeyHex; + }) + .join(""); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(triggerableWithdrawalGateway, "TriggerableExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, timestamp); + + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, 1, 0); + + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(pubkeys); + } + }); + + it("check current limit", async () => { + const data = await triggerableWithdrawalGateway.getExitRequestLimitFullInfo(); + + // maxExitRequestsLimit + expect(data[0]).to.equal(4); + // exitsPerFrame + expect(data[1]).to.equal(1); + // frameDuration + expect(data[2]).to.equal(48); + // prevExitRequestsLimit + // maxExitRequestsLimit (4) - exitRequests.length (3) + expect(data[3]).to.equal(1); + // currentExitRequestsLimit + // equal to prevExitRequestsLimit as timestamp is mocked in test and we didnt increase it yet + expect(data[4]).to.equal(1); + }); + + it("should revert if limit doesnt cover requests count", async () => { + const requests = encodeTWGExitDataList(exitRequests); + + await expect( + triggerableWithdrawalGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }), + ) + .to.be.revertedWithCustomError(triggerableWithdrawalGateway, "ExitRequestsLimit") + .withArgs(3, 1); + }); + + it("should revert if limit doesnt cover requests count", async () => { + const requests = encodeTWGExitDataList(exitRequests); + + await expect( + triggerableWithdrawalGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }), + ) + .to.be.revertedWithCustomError(triggerableWithdrawalGateway, "ExitRequestsLimit") + .withArgs(3, 1); + }); + + it("rewind time", async () => { + await triggerableWithdrawalGateway.advanceTimeBy(2 * 48); + }); + + it("current limit should be increased by 2", async () => { + const data = await triggerableWithdrawalGateway.getExitRequestLimitFullInfo(); + + // maxExitRequestsLimit + expect(data[0]).to.equal(4); + // exitsPerFrame + expect(data[1]).to.equal(1); + // frameDuration + expect(data[2]).to.equal(48); + // prevExitRequestsLimit + // maxExitRequestsLimit (4) - exitRequests.length (3) + expect(data[3]).to.equal(1); + // currentExitRequestsLimit + expect(data[4]).to.equal(3); + }); + + it("should add withdrawal request ias limit is enough for processing all requests", async () => { + const requests = encodeTWGExitDataList(exitRequests); + + const tx = await triggerableWithdrawalGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); + + const timestamp = await triggerableWithdrawalGateway.getTimestamp(); const pubkeys = "0x" + From 1281000ad0629e529296acd3517ad9906cb8ad9b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 16 May 2025 20:44:59 +0400 Subject: [PATCH 130/405] fix: finalize_v2 & _initialize_v2 & veb max batch size --- .../0.8.9/TriggerableWithdrawalGateway.sol | 8 +- .../0.8.9/interfaces/IValidatorsExitBus.sol | 6 -- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 76 +++++++++++++++---- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 32 +++++++- .../TriggerableWithdrawalGateway__Harness.sol | 2 +- ...ator-exit-bus-oracle.accessControl.test.ts | 2 +- .../validator-exit-bus-oracle.deploy.test.ts | 16 +++- ...awalGateway.triggerFullWithdrawals.test.ts | 7 +- test/deploy/validatorExitBusOracle.ts | 13 +++- 9 files changed, 125 insertions(+), 37 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalGateway.sol b/contracts/0.8.9/TriggerableWithdrawalGateway.sol index cac3d6ae9f..565ce04789 100644 --- a/contracts/0.8.9/TriggerableWithdrawalGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalGateway.sol @@ -97,6 +97,8 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { uint256 internal constant PACKED_EXIT_REQUEST_LENGTH = 56; uint256 internal constant PUBLIC_KEY_LENGTH = 48; + uint256 public constant VERSION = 1; + ILidoLocator internal immutable LOCATOR; /// @dev Ensures the contract’s ETH balance is unchanged. @@ -106,12 +108,10 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { assert(address(this).balance == balanceBeforeCall); } - constructor(address lidoLocator) { + constructor(address admin, address lidoLocator) { + if (admin == address(0)) revert AdminCannotBeZero(); LOCATOR = ILidoLocator(lidoLocator); - } - function initialize(address admin) external { - if (admin == address(0)) revert AdminCannotBeZero(); _setupRole(DEFAULT_ADMIN_ROLE, admin); } diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol index e73f9f040e..149aa85f6d 100644 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -8,12 +8,6 @@ interface IValidatorsExitBus { uint256 dataFormat; } - struct DirectExitData { - uint256 stakingModuleId; - uint256 nodeOperatorId; - bytes validatorsPubkeys; - } - struct DeliveryHistory { // index in array of requests uint256 lastDeliveredKeyIndex; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 99318e208c..2c8239e902 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -18,6 +18,12 @@ interface ITriggerableWithdrawalGateway { ) external payable; } +/** + * @title ValidatorsExitBus + * @notice An on-chain contract that serves as the central infrastructure for managing validator exit requests. + * It stores report hashes, emits exit events, and maintains data and tools that enables anyone to prove a validator was requested to exit. + * Unlike VEBO, it supports exit reports from a wide range of entities. + */ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ExitLimitUtilsStorage for bytes32; @@ -92,6 +98,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param remainingLimit Amount of requests that still can be processed at current day */ error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + /** + * @notice Thrown when the number of requests submitted via submitExitRequestsData exceeds the allowed maxRequestsPerBatch. + * @param requestsCount The number of requests included in the current call. + * @param maxRequestsPerBatch The maximum number of requests allowed per call to submitExitRequestsData. + */ + error MaxRequestsBatchSizeExceeded(uint256 requestsCount, uint256 maxRequestsPerBatch); /// @dev Events @@ -140,13 +152,16 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes pubkey; } + /// @notice An ACL role granting the permission to submit a hash of the exit requests data bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); - bytes32 public constant DIRECT_EXIT_ROLE = keccak256("DIRECT_EXIT_ROLE"); + /// @notice An ACL role granting the permission to set maximum exit request limit and the frame limit restoring values bytes32 public constant EXIT_REPORT_LIMIT_ROLE = keccak256("EXIT_REPORT_LIMIT_ROLE"); /// @notice An ACL role granting the permission to pause accepting validator exit requests bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); /// @notice An ACL role granting the permission to resume accepting validator exit requests bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); + /// @notice An ACL role granting the permission to set MAX_VALIDATORS_PER_BATCH value + bytes32 public constant MAX_VALIDATORS_PER_BATCH_ROLE = keccak256("MAX_VALIDATORS_PER_BATCH_ROLE"); /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; @@ -178,6 +193,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); + bytes32 internal constant MAX_VALIDATORS_PER_BATCH_POSITION = + keccak256("lido.ValidatorsExitBus.maxValidatorsPerBatch"); constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); @@ -234,6 +251,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; } + _checkRequestsBatchSize(requestStatus.totalItemsCount); + uint256 undeliveredItemsCount = requestStatus.totalItemsCount - requestStatus.deliveredItemsCount; if (undeliveredItemsCount == 0) { @@ -345,20 +364,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 exitsPerFrame, uint256 frameDuration ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { - require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); - - uint256 timestamp = _getTimestamp(); - - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( - maxExitRequestsLimit, - exitsPerFrame, - frameDuration, - timestamp - ) - ); - - emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); } /** @@ -465,6 +471,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _pauseUntil(_pauseUntilInclusive); } + function setMaxRequestsPerBatch(uint256 value) external onlyRole(MAX_VALIDATORS_PER_BATCH_ROLE) { + _setMaxRequestsPerBatch(value); + } + + function getMaxRequestsPerBatch() external view returns (uint256) { + return _getMaxRequestsPerBatch(); + } + /// Internal functions function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal pure { @@ -477,6 +491,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } + function _checkRequestsBatchSize(uint256 requestsCount) internal view { + uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); + if (requestsCount > maxRequestsPerBatch) { + revert MaxRequestsBatchSizeExceeded(requestsCount, maxRequestsPerBatch); + } + } + function _checkExitSubmitted(RequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion == 0) { revert ExitHashNotSubmitted(); @@ -502,6 +523,31 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa return block.timestamp; // solhint-disable-line not-rely-on-time } + function _setMaxRequestsPerBatch(uint256 value) internal { + MAX_VALIDATORS_PER_BATCH_POSITION.setStorageUint256(value); + } + + function _getMaxRequestsPerBatch() internal view returns (uint256) { + return MAX_VALIDATORS_PER_BATCH_POSITION.getStorageUint256(); + } + + function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { + require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + + uint256 timestamp = _getTimestamp(); + + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp + ) + ); + + emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + } + function _storeExitRequestHash( bytes32 exitRequestHash, uint256 totalItemsCount, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 8e1506e775..96beca1ba7 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -69,18 +69,44 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { address admin, address consensusContract, uint256 consensusVersion, - uint256 lastProcessingRefSlot + uint256 lastProcessingRefSlot, + uint256 maxValidatorsPerBatch, + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration ) external { if (admin == address(0)) revert AdminCannotBeZero(); _setupRole(DEFAULT_ADMIN_ROLE, admin); _pauseFor(PAUSE_INFINITELY); _initialize(consensusContract, consensusVersion, lastProcessingRefSlot); + + _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDuration); } - function finalizeUpgrade_v2() external { - _updateContractVersion(2); + /** + * @notice A function to finalize upgrade to v2 (from v1). Can be called only once + * + * For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md + */ + function finalizeUpgrade_v2( + uint256 maxValidatorsPerBatch, + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration + ) external { + _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDuration); + } + function _initialize_v2( + uint256 maxValidatorsPerBatch, + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration + ) internal { + _updateContractVersion(2); + _setMaxRequestsPerBatch(maxValidatorsPerBatch); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); } /// diff --git a/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol index 4ce4e0ded8..46380cedba 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol @@ -5,7 +5,7 @@ import {TriggerableWithdrawalGateway} from "contracts/0.8.9/TriggerableWithdrawa contract TriggerableWithdrawalGateway__Harness is TriggerableWithdrawalGateway { uint256 internal _time = 2513040315; - constructor(address lidoLocator) TriggerableWithdrawalGateway(lidoLocator) {} + constructor(address admin, address lidoLocator) TriggerableWithdrawalGateway(admin, lidoLocator) {} function getTimestamp() external view returns (uint256) { return _time; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts index 087427fae6..d7d314e2a0 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.accessControl.test.ts @@ -131,7 +131,7 @@ describe("ValidatorsExitBusOracle.sol:accessControl", () => { }); it("should revert without admin address", async () => { await expect( - oracle.initialize(ZeroAddress, await consensus.getAddress(), CONSENSUS_VERSION, 0), + oracle.initialize(ZeroAddress, await consensus.getAddress(), CONSENSUS_VERSION, 0, 600, 13000, 1, 48), ).to.be.revertedWithCustomError(oracle, "AdminCannotBeZero"); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts index 3690c032c5..7071a02e6a 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts @@ -23,8 +23,22 @@ describe("ValidatorsExitBusOracle.sol:deploy", () => { it("initialize reverts if admin address is zero", async () => { const deployed = await deployVEBO(admin.address); + const maxValidatorsPerBatch = 50; + const maxExitRequestsLimit = 100; + const exitsPerFrame = 1; + const frameDuration = 48; + await expect( - deployed.oracle.initialize(ZeroAddress, await deployed.consensus.getAddress(), CONSENSUS_VERSION, 0), + deployed.oracle.initialize( + ZeroAddress, + await deployed.consensus.getAddress(), + CONSENSUS_VERSION, + 0, + maxValidatorsPerBatch, + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, + ), ).to.be.revertedWithCustomError(defaultOracle, "AdminCannotBeZero"); }); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 8292c1e294..7e17fe4401 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -65,9 +65,10 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { stakingRouter: await stakingRouter.getAddress(), }); - triggerableWithdrawalGateway = await ethers.deployContract("TriggerableWithdrawalGateway__Harness", [locatorAddr]); - - await triggerableWithdrawalGateway.initialize(admin); + triggerableWithdrawalGateway = await ethers.deployContract("TriggerableWithdrawalGateway__Harness", [ + admin, + locatorAddr, + ]); }); it("should revert if caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE", async () => { diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 15f96432ef..8b25672c56 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -108,9 +108,16 @@ export async function initVEBO({ lastProcessingRefSlot = 0, resumeAfterDeploy = false, }: VEBOConfig) { - const initTx = await oracle.initialize(admin, await consensus.getAddress(), consensusVersion, lastProcessingRefSlot); - - await oracle.finalizeUpgrade_v2(); + const initTx = await oracle.initialize( + admin, + await consensus.getAddress(), + consensusVersion, + lastProcessingRefSlot, + 600, + 13000, + 1, + 48, + ); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_VERSION_ROLE(), admin); From c37466c7f92a6c71885ad4c5b84f3747f9e704fb Mon Sep 17 00:00:00 2001 From: F4ever Date: Sat, 17 May 2025 11:35:40 +0200 Subject: [PATCH 131/405] chore: rename twg contract + fix scratch deploy --- contracts/0.8.9/LidoLocator.sol | 6 +-- ....sol => TriggerableWithdrawalsGateway.sol} | 8 +-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 +- contracts/common/interfaces/ILidoLocator.sol | 3 +- lib/deploy.ts | 1 + lib/state-file.ts | 5 +- .../steps/0090-deploy-non-aragon-contracts.ts | 12 +++++ scripts/triggerable-withdrawals/tw-deploy.ts | 13 ++++- .../LidoLocator__MockForSanityChecker.sol | 9 ++-- ...riggerableWithdrawalsGateway__Harness.sol} | 6 +-- ...gerableWithdrawalsGateway__MockForVEB.sol} | 2 +- test/0.8.9/lidoLocator.test.ts | 2 +- ...dator-exit-bus-oracle.triggerExits.test.ts | 12 ++--- .../oracleReportSanityChecker.misc.test.ts | 3 +- ...eportSanityChecker.negative-rebase.test.ts | 3 +- ...awalGateway.triggerFullWithdrawals.test.ts | 54 +++++++++---------- test/deploy/locator.ts | 5 +- test/deploy/validatorExitBusOracle.ts | 8 +-- 18 files changed, 94 insertions(+), 62 deletions(-) rename contracts/0.8.9/{TriggerableWithdrawalGateway.sol => TriggerableWithdrawalsGateway.sol} (97%) rename test/0.8.9/contracts/{TriggerableWithdrawalGateway__Harness.sol => TriggerableWithdrawalsGateway__Harness.sol} (65%) rename test/0.8.9/contracts/{TriggerableWithdrawalGateway__MockForVEB.sol => TriggerableWithdrawalsGateway__MockForVEB.sol} (88%) diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index d383df6387..83bdd7e7b8 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -29,7 +29,7 @@ contract LidoLocator is ILidoLocator { address withdrawalVault; address oracleDaemonConfig; address validatorExitDelayVerifier; - address triggerableWithdrawalGateway; + address triggerableWithdrawalsGateway; } error ZeroAddress(); @@ -49,7 +49,7 @@ contract LidoLocator is ILidoLocator { address public immutable withdrawalVault; address public immutable oracleDaemonConfig; address public immutable validatorExitDelayVerifier; - address public immutable triggerableWithdrawalGateway; + address public immutable triggerableWithdrawalsGateway; /** * @notice declare service locations @@ -72,7 +72,7 @@ contract LidoLocator is ILidoLocator { withdrawalVault = _assertNonZero(_config.withdrawalVault); oracleDaemonConfig = _assertNonZero(_config.oracleDaemonConfig); validatorExitDelayVerifier = _assertNonZero(_config.validatorExitDelayVerifier); - triggerableWithdrawalGateway = _assertNonZero(_config.triggerableWithdrawalGateway); + triggerableWithdrawalsGateway = _assertNonZero(_config.triggerableWithdrawalsGateway); } function coreComponents() external view returns( diff --git a/contracts/0.8.9/TriggerableWithdrawalGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol similarity index 97% rename from contracts/0.8.9/TriggerableWithdrawalGateway.sol rename to contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 565ce04789..32a1056a71 100644 --- a/contracts/0.8.9/TriggerableWithdrawalGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -23,11 +23,11 @@ interface IStakingRouter { } /** - * @title TriggerableWithdrawalGateway - * @notice TriggerableWithdrawalGateway contract is one entrypoint for all triggerable withdrawal requests (TWRs) in protocol. + * @title TriggerableWithdrawalsGateway + * @notice TriggerableWithdrawalsGateway contract is one entrypoint for all triggerable withdrawal requests (TWRs) in protocol. * This contract is responsible for limiting TWRs, checking ADD_FULL_WITHDRAWAL_REQUEST_ROLE role before it gets to Withdrawal Vault. */ -contract TriggerableWithdrawalGateway is AccessControlEnumerable { +contract TriggerableWithdrawalsGateway is AccessControlEnumerable { using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; @@ -91,7 +91,7 @@ contract TriggerableWithdrawalGateway is AccessControlEnumerable { bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); bytes32 public constant TW_EXIT_REPORT_LIMIT_ROLE = keccak256("TW_EXIT_REPORT_LIMIT_ROLE"); - bytes32 public constant TWR_LIMIT_POSITION = keccak256("lido.TriggerableWithdrawalGateway.maxExitRequestLimit"); + bytes32 public constant TWR_LIMIT_POSITION = keccak256("lido.TriggerableWithdrawalsGateway.maxExitRequestLimit"); /// Length in bytes of packed triggerable exit request uint256 internal constant PACKED_EXIT_REQUEST_LENGTH = 56; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 2c8239e902..6c5ee864cb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -10,7 +10,7 @@ import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../li import {PausableUntil} from "../utils/PausableUntil.sol"; import {IValidatorsExitBus} from "../interfaces/IValidatorsExitBus.sol"; -interface ITriggerableWithdrawalGateway { +interface ITriggerableWithdrawalsGateway { function triggerFullWithdrawals( bytes calldata triggerableExitData, address refundRecipient, @@ -346,7 +346,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _copyValidatorData(validatorData, exits, i); } - ITriggerableWithdrawalGateway(LOCATOR.triggerableWithdrawalGateway()).triggerFullWithdrawals{value: msg.value}( + ITriggerableWithdrawalsGateway(LOCATOR.triggerableWithdrawalsGateway()).triggerFullWithdrawals{value: msg.value}( exits, refundRecipient, exitType diff --git a/contracts/common/interfaces/ILidoLocator.sol b/contracts/common/interfaces/ILidoLocator.sol index f381f57ac7..378a1d1332 100644 --- a/contracts/common/interfaces/ILidoLocator.sol +++ b/contracts/common/interfaces/ILidoLocator.sol @@ -20,7 +20,8 @@ interface ILidoLocator { function withdrawalVault() external view returns(address); function postTokenRebaseReceiver() external view returns(address); function oracleDaemonConfig() external view returns(address); - function triggerableWithdrawalGateway() external view returns (address); + function validatorExitDelayVerifier() external view returns (address); + function triggerableWithdrawalsGateway() external view returns (address); function coreComponents() external view returns( address elRewardsVault, address oracleReportSanityChecker, diff --git a/lib/deploy.ts b/lib/deploy.ts index 62fed3833a..1d2d5b04af 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -247,6 +247,7 @@ async function getLocatorConfig(locatorAddress: string) { "withdrawalVault", "oracleDaemonConfig", "validatorExitDelayVerifier", + "triggerableWithdrawalsGateway", ] as (keyof LidoLocator.ConfigStruct)[]; const configPromises = addresses.map((name) => locator[name]()); diff --git a/lib/state-file.ts b/lib/state-file.ts index f2e41f1421..746096976c 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -86,8 +86,8 @@ export enum Sk { chainSpec = "chainSpec", scratchDeployGasUsed = "scratchDeployGasUsed", minFirstAllocationStrategy = "minFirstAllocationStrategy", - triggerableWithdrawals = "triggerableWithdrawals", validatorExitDelayVerifier = "validatorExitDelayVerifier", + triggerableWithdrawalsGateway = "triggerableWithdrawalsGateway", } export function getAddress(contractKey: Sk, state: DeploymentState): string { @@ -110,7 +110,6 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.withdrawalQueueERC721: case Sk.withdrawalVault: return state[contractKey].proxy.address; - case Sk.triggerableWithdrawals: case Sk.apmRegistryFactory: case Sk.burner: case Sk.callsScript: @@ -133,6 +132,8 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.oracleReportSanityChecker: case Sk.wstETH: case Sk.depositContract: + case Sk.validatorExitDelayVerifier: + case Sk.triggerableWithdrawalsGateway: return state[contractKey].address; default: throw new Error(`Unsupported contract entry key ${contractKey}`); diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index b8dbb50015..16f234401f 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -221,6 +221,17 @@ export async function main() { ], ); + // Deploy Triggerable Withdrawals Gateway + const triggerableWithdrawalsGateway = await deployWithoutProxy( + Sk.triggerableWithdrawalsGateway, + "TriggerableWithdrawalsGateway", + deployer, + [ + deployer, // address admin + locator.address, // address lidoLocator + ], + ); + // Update LidoLocator with valid implementation const locatorConfig: string[] = [ accountingOracle.address, @@ -238,6 +249,7 @@ export async function main() { withdrawalVaultAddress, oracleDaemonConfig.address, validatorExitDelayVerifier.address, + triggerableWithdrawalsGateway.address, ]; await updateProxyImplementation(Sk.lidoLocator, "LidoLocator", locator.address, proxyContractsOwner, [locatorConfig]); } diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index fd296df5d8..615d03f400 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -111,7 +111,16 @@ async function main() { deployer, validatorExitDelayVerifierArgs, ); - log.success(`ValidatorExitDelayVerifier implementation address: ${NOR.address}`); + log.success(`ValidatorExitDelayVerifier implementation address: ${validatorExitDelayVerifier.address}`); + log.emptyLine(); + + const triggerableWithdrawalsGateway = await deployImplementation( + Sk.triggerableWithdrawalsGateway, + "triggerableWithdrawalsGateway", + deployer, + [deployer, locator.address], + ); + log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); log.emptyLine(); // fetch contract addresses that will not changed @@ -131,6 +140,7 @@ async function main() { await locator.withdrawalVault(), await locator.oracleDaemonConfig(), validatorExitDelayVerifier.address, + triggerableWithdrawalsGateway.address, ]; const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); @@ -143,6 +153,7 @@ WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" +TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" `); } diff --git a/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol b/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol index a5f8620343..9bee0b43bf 100644 --- a/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol +++ b/test/0.8.9/contracts/LidoLocator__MockForSanityChecker.sol @@ -22,7 +22,8 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { address withdrawalVault; address postTokenRebaseReceiver; address oracleDaemonConfig; - address triggerableWithdrawalGateway; + address validatorExitDelayVerifier; + address triggerableWithdrawalsGateway; } address public immutable lido; @@ -39,7 +40,8 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { address public immutable withdrawalVault; address public immutable postTokenRebaseReceiver; address public immutable oracleDaemonConfig; - address public immutable triggerableWithdrawalGateway; + address public immutable validatorExitDelayVerifier; + address public immutable triggerableWithdrawalsGateway; constructor(ContractAddresses memory addresses) { lido = addresses.lido; @@ -56,7 +58,8 @@ contract LidoLocator__MockForSanityChecker is ILidoLocator { withdrawalVault = addresses.withdrawalVault; postTokenRebaseReceiver = addresses.postTokenRebaseReceiver; oracleDaemonConfig = addresses.oracleDaemonConfig; - triggerableWithdrawalGateway = addresses.triggerableWithdrawalGateway; + validatorExitDelayVerifier = addresses.validatorExitDelayVerifier; + triggerableWithdrawalsGateway = addresses.triggerableWithdrawalsGateway; } function coreComponents() external view returns (address, address, address, address, address, address) { diff --git a/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol similarity index 65% rename from test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol rename to test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol index 46380cedba..264df27d61 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol @@ -1,11 +1,11 @@ pragma solidity 0.8.9; -import {TriggerableWithdrawalGateway} from "contracts/0.8.9/TriggerableWithdrawalGateway.sol"; +import {TriggerableWithdrawalsGateway} from "contracts/0.8.9/TriggerableWithdrawalsGateway.sol"; -contract TriggerableWithdrawalGateway__Harness is TriggerableWithdrawalGateway { +contract TriggerableWithdrawalsGateway__Harness is TriggerableWithdrawalsGateway { uint256 internal _time = 2513040315; - constructor(address admin, address lidoLocator) TriggerableWithdrawalGateway(admin, lidoLocator) {} + constructor(address admin, address lidoLocator) TriggerableWithdrawalsGateway(admin, lidoLocator) {} function getTimestamp() external view returns (uint256) { return _time; diff --git a/test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol similarity index 88% rename from test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol rename to test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol index d828a75337..2e5f79eca1 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalGateway__MockForVEB.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol @@ -1,6 +1,6 @@ pragma solidity 0.8.9; -contract TriggerableWithdrawalGateway__MockForVEB { +contract TriggerableWithdrawalsGateway__MockForVEB { event Mock__triggerFullWithdrawalsTriggered(bytes triggerableExitData, address refundRecipient, uint8 exitType); function triggerFullWithdrawals( diff --git a/test/0.8.9/lidoLocator.test.ts b/test/0.8.9/lidoLocator.test.ts index 8ca55064a4..a108b3dccb 100644 --- a/test/0.8.9/lidoLocator.test.ts +++ b/test/0.8.9/lidoLocator.test.ts @@ -22,7 +22,7 @@ const services = [ "withdrawalVault", "oracleDaemonConfig", "validatorExitDelayVerifier", - "triggerableWithdrawalGateway", + "triggerableWithdrawalsGateway", ] as const; type ArrayToUnion = A[number]; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 333f2f2e63..6577083df5 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -5,7 +5,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { HashConsensus__Harness, - TriggerableWithdrawalGateway__MockForVEB, + TriggerableWithdrawalsGateway__MockForVEB, ValidatorsExitBus__Harness, } from "typechain-types"; @@ -27,7 +27,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; - let triggerableWithdrawalGateway: TriggerableWithdrawalGateway__MockForVEB; + let triggerableWithdrawalsGateway: TriggerableWithdrawalsGateway__MockForVEB; let oracleVersion: bigint; let exitRequests: ExitRequest[]; @@ -88,9 +88,9 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { const locator = deployed.locator; oracle = deployed.oracle; consensus = deployed.consensus; - triggerableWithdrawalGateway = deployed.triggerableWithdrawalGateway; + triggerableWithdrawalsGateway = deployed.triggerableWithdrawalsGateway; - console.log("twg=", await locator.triggerableWithdrawalGateway()); + console.log("twg=", await locator.triggerableWithdrawalsGateway()); await initVEBO({ admin: admin.address, @@ -178,7 +178,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { const requests = encodeTWGExitDataList(exitRequests); await expect(tx) - .to.emit(triggerableWithdrawalGateway, "Mock__triggerFullWithdrawalsTriggered") + .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") .withArgs(requests, admin.address, 0); }); @@ -196,7 +196,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { const requests = encodeTWGExitDataList(exitRequests.filter((req, i) => [0, 1, 3].includes(i))); await expect(tx) - .to.emit(triggerableWithdrawalGateway, "Mock__triggerFullWithdrawalsTriggered") + .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") .withArgs(requests, admin.address, 0); }); diff --git a/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts b/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts index 39a6331c19..6eb7f7f2e3 100644 --- a/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts +++ b/test/0.8.9/sanityChecker/oracleReportSanityChecker.misc.test.ts @@ -92,7 +92,8 @@ describe("OracleReportSanityChecker.sol:misc", () => { withdrawalVault: withdrawalVault, postTokenRebaseReceiver: deployer.address, oracleDaemonConfig: deployer.address, - triggerableWithdrawalGateway: deployer.address, + validatorExitDelayVerifier: deployer.address, + triggerableWithdrawalsGateway: deployer.address, }, ]); managersRoster = { diff --git a/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts b/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts index 2944924b4f..577afeeb25 100644 --- a/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts +++ b/test/0.8.9/sanityChecker/oracleReportSanityChecker.negative-rebase.test.ts @@ -83,7 +83,8 @@ describe("OracleReportSanityChecker.sol:negative-rebase", () => { withdrawalVault: deployer.address, postTokenRebaseReceiver: deployer.address, oracleDaemonConfig: deployer.address, - triggerableWithdrawalGateway: deployer.address, + validatorExitDelayVerifier: deployer.address, + triggerableWithdrawalsGateway: deployer.address, }, ]); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 7e17fe4401..98999525b3 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -5,7 +5,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { StakingRouter__MockForTWG, - TriggerableWithdrawalGateway__Harness, + TriggerableWithdrawalsGateway__Harness, WithdrawalVault__MockForTWG, } from "typechain-types"; @@ -34,8 +34,8 @@ const exitRequests = [ const ZERO_ADDRESS = ethers.ZeroAddress; -describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { - let triggerableWithdrawalGateway: TriggerableWithdrawalGateway__Harness; +describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { + let triggerableWithdrawalsGateway: TriggerableWithdrawalsGateway__Harness; let withdrawalVault: WithdrawalVault__MockForTWG; let stakingRouter: StakingRouter__MockForTWG; let admin: HardhatEthersSigner; @@ -65,7 +65,7 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { stakingRouter: await stakingRouter.getAddress(), }); - triggerableWithdrawalGateway = await ethers.deployContract("TriggerableWithdrawalGateway__Harness", [ + triggerableWithdrawalsGateway = await ethers.deployContract("TriggerableWithdrawalsGateway__Harness", [ admin, locatorAddr, ]); @@ -73,45 +73,45 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { it("should revert if caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE", async () => { const requests = encodeTWGExitDataList(exitRequests); - const role = await triggerableWithdrawalGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); await expect( - triggerableWithdrawalGateway + triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 10 }), ).to.be.revertedWithOZAccessControlError(await authorizedEntity.getAddress(), role); }); it("should revert if total fee value sent is insufficient to cover all provided TW requests ", async () => { - const role = await triggerableWithdrawalGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); - await triggerableWithdrawalGateway.grantRole(role, authorizedEntity); + const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); const requests = encodeTWGExitDataList(exitRequests); await expect( - triggerableWithdrawalGateway + triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 1 }), ) - .to.be.revertedWithCustomError(triggerableWithdrawalGateway, "InsufficientWithdrawalFee") + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "InsufficientWithdrawalFee") .withArgs(3, 1); }); it("set limit", async () => { - const role = await triggerableWithdrawalGateway.TW_EXIT_REPORT_LIMIT_ROLE(); - await triggerableWithdrawalGateway.grantRole(role, authorizedEntity); + const role = await triggerableWithdrawalsGateway.TW_EXIT_REPORT_LIMIT_ROLE(); + await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); - const exitLimitTx = await triggerableWithdrawalGateway.connect(authorizedEntity).setExitRequestLimit(4, 1, 48); - await expect(exitLimitTx).to.emit(triggerableWithdrawalGateway, "ExitRequestsLimitSet").withArgs(4, 1, 48); + const exitLimitTx = await triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(4, 1, 48); + await expect(exitLimitTx).to.emit(triggerableWithdrawalsGateway, "ExitRequestsLimitSet").withArgs(4, 1, 48); }); it("should add withdrawal request", async () => { const requests = encodeTWGExitDataList(exitRequests); - const tx = await triggerableWithdrawalGateway + const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const timestamp = await triggerableWithdrawalGateway.getTimestamp(); + const timestamp = await triggerableWithdrawalsGateway.getTimestamp(); const pubkeys = "0x" + @@ -124,7 +124,7 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { for (const request of exitRequests) { await expect(tx) - .to.emit(triggerableWithdrawalGateway, "TriggerableExitRequest") + .to.emit(triggerableWithdrawalsGateway, "TriggerableExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, timestamp); await expect(tx) @@ -136,7 +136,7 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { }); it("check current limit", async () => { - const data = await triggerableWithdrawalGateway.getExitRequestLimitFullInfo(); + const data = await triggerableWithdrawalsGateway.getExitRequestLimitFullInfo(); // maxExitRequestsLimit expect(data[0]).to.equal(4); @@ -156,11 +156,11 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { const requests = encodeTWGExitDataList(exitRequests); await expect( - triggerableWithdrawalGateway + triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }), ) - .to.be.revertedWithCustomError(triggerableWithdrawalGateway, "ExitRequestsLimit") + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ExitRequestsLimit") .withArgs(3, 1); }); @@ -168,20 +168,20 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { const requests = encodeTWGExitDataList(exitRequests); await expect( - triggerableWithdrawalGateway + triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }), ) - .to.be.revertedWithCustomError(triggerableWithdrawalGateway, "ExitRequestsLimit") + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ExitRequestsLimit") .withArgs(3, 1); }); it("rewind time", async () => { - await triggerableWithdrawalGateway.advanceTimeBy(2 * 48); + await triggerableWithdrawalsGateway.advanceTimeBy(2 * 48); }); it("current limit should be increased by 2", async () => { - const data = await triggerableWithdrawalGateway.getExitRequestLimitFullInfo(); + const data = await triggerableWithdrawalsGateway.getExitRequestLimitFullInfo(); // maxExitRequestsLimit expect(data[0]).to.equal(4); @@ -199,11 +199,11 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { it("should add withdrawal request ias limit is enough for processing all requests", async () => { const requests = encodeTWGExitDataList(exitRequests); - const tx = await triggerableWithdrawalGateway + const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const timestamp = await triggerableWithdrawalGateway.getTimestamp(); + const timestamp = await triggerableWithdrawalsGateway.getTimestamp(); const pubkeys = "0x" + @@ -216,7 +216,7 @@ describe("TriggerableWithdrawalGateway.sol:triggerFullWithdrawals", () => { for (const request of exitRequests) { await expect(tx) - .to.emit(triggerableWithdrawalGateway, "TriggerableExitRequest") + .to.emit(triggerableWithdrawalsGateway, "TriggerableExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, timestamp); await expect(tx) diff --git a/test/deploy/locator.ts b/test/deploy/locator.ts index 6ab4608ec5..21bb1173ab 100644 --- a/test/deploy/locator.ts +++ b/test/deploy/locator.ts @@ -29,7 +29,7 @@ async function deployDummyLocator(config?: Partial, de withdrawalQueue: certainAddress("dummy-locator:withdrawalQueue"), withdrawalVault: certainAddress("dummy-locator:withdrawalVault"), validatorExitDelayVerifier: certainAddress("dummy-locator:validatorExitDelayVerifier"), - triggerableWithdrawalGateway: certainAddress("dummy-locator:triggerableWithdrawalGateway"), + triggerableWithdrawalsGateway: certainAddress("dummy-locator:triggerableWithdrawalsGateway"), ...config, }); @@ -104,7 +104,8 @@ async function getLocatorConfig(locatorAddress: string) { "withdrawalQueue", "withdrawalVault", "oracleDaemonConfig", - "triggerableWithdrawalGateway", + "validatorExitDelayVerifier", + "triggerableWithdrawalsGateway", ] as Partial[]; const configPromises = addresses.map((name) => locator[name]()); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 8b25672c56..5c82455f9d 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -35,7 +35,7 @@ async function deployOracleReportSanityCheckerForExitBus(lidoLocator: string, ad } async function deployTWG() { - return await ethers.deployContract("TriggerableWithdrawalGateway__MockForVEB"); + return await ethers.deployContract("TriggerableWithdrawalsGateway__MockForVEB"); } export async function deployVEBO( @@ -62,12 +62,12 @@ export async function deployVEBO( const { ao, lido } = await deployMockAccountingOracle(secondsPerSlot, genesisTime); - const triggerableWithdrawalGateway = await deployTWG(); + const triggerableWithdrawalsGateway = await deployTWG(); await updateLidoLocatorImplementation(locatorAddr, { lido: await lido.getAddress(), accountingOracle: await ao.getAddress(), - triggerableWithdrawalGateway, //: await lido.getAddress(), // await triggerableWithdrawalGateway.getAddress(), + triggerableWithdrawalsGateway, //: await lido.getAddress(), // await TriggerableWithdrawalsGateway.getAddress(), }); const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locatorAddr, admin); @@ -85,7 +85,7 @@ export async function deployVEBO( oracle, consensus, oracleReportSanityChecker, - triggerableWithdrawalGateway, + triggerableWithdrawalsGateway, }; } From c2c9904034ffd6159adfda413dd3dc6087ace0d4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sat, 17 May 2025 16:41:44 +0400 Subject: [PATCH 132/405] fix: scratch deploy --- lib/state-file.ts | 1 + .../steps/0090-deploy-non-aragon-contracts.ts | 8 ++++++++ .../steps/0120-initialize-non-aragon-contracts.ts | 8 ++++++++ scripts/scratch/steps/0130-grant-roles.ts | 14 ++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/lib/state-file.ts b/lib/state-file.ts index f2e41f1421..8f64ea0ef9 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -88,6 +88,7 @@ export enum Sk { minFirstAllocationStrategy = "minFirstAllocationStrategy", triggerableWithdrawals = "triggerableWithdrawals", validatorExitDelayVerifier = "validatorExitDelayVerifier", + triggerableWithdrawalGateway = "triggerableWithdrawalGateway". } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index b8dbb50015..fac5bd2350 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -221,6 +221,13 @@ export async function main() { ], ); + const triggerableWithdrawalGateway = await deployWithoutProxy( + Sk.triggerableWithdrawalGateway, + "TriggerableWithdrawalGateway", + deployer, + [admin, locator.address], + ); + // Update LidoLocator with valid implementation const locatorConfig: string[] = [ accountingOracle.address, @@ -238,6 +245,7 @@ export async function main() { withdrawalVaultAddress, oracleDaemonConfig.address, validatorExitDelayVerifier.address, + triggerableWithdrawalGateway.address, ]; await updateProxyImplementation(Sk.lidoLocator, "LidoLocator", locator.address, proxyContractsOwner, [locatorConfig]); } diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index 2f7fd1e796..c646e6fea2 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -97,6 +97,10 @@ export async function main() { // Initialize ValidatorsExitBusOracle const validatorsExitBusOracle = await loadContract("ValidatorsExitBusOracle", ValidatorsExitBusOracleAddress); + const maxValidatorsPerBatch = 600; + const maxExitRequestsLimit = 13000; + const exitsPerFrame = 1; + const frameDuration = 48; await makeTx( validatorsExitBusOracle, "initialize", @@ -105,6 +109,10 @@ export async function main() { hashConsensusForValidatorsExitBusOracleAddress, validatorsExitBusOracleParams.consensusVersion, zeroLastProcessingRefSlot, + maxValidatorsPerBatch, + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, ], { from: deployer }, ); diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index ff611c85df..ca7de2b5e8 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -3,6 +3,7 @@ import { ethers } from "hardhat"; import { Burner, StakingRouter, + TriggerableWithdrawalGateway, ValidatorsExitBusOracle, WithdrawalQueueERC721, WithdrawalVault, @@ -29,6 +30,7 @@ export async function main() { const accountingOracleAddress = state[Sk.accountingOracle].proxy.address; const validatorsExitBusOracleAddress = state[Sk.validatorsExitBusOracle].proxy.address; const depositSecurityModuleAddress = state[Sk.depositSecurityModule].address; + const triggerableWithdrawalGatewayAddress = state[Sk.triggerableWithdrawalGateway].address; // StakingRouter const stakingRouter = await loadContract("StakingRouter", stakingRouterAddress); @@ -65,6 +67,18 @@ export async function main() { log.emptyLine(); } + // TriggerableWithdrawalGateway + const triggerableWithdrawalGateway = await loadContract( + "TriggerableWithdrawalGateway", + triggerableWithdrawalGatewayAddress, + ); + await makeTx( + triggerableWithdrawalGateway, + "grantRole", + [await triggerableWithdrawalGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(), validatorsExitBusOracleAddress], + { from: deployer }, + ); + // WithdrawalQueue const withdrawalQueue = await loadContract("WithdrawalQueueERC721", withdrawalQueueAddress); if (gateSealAddress) { From 077e2f25de9c2f722e0e0326b0a69dd264f7867a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sat, 17 May 2025 16:45:45 +0400 Subject: [PATCH 133/405] fix: scratch deploy & archive sr-v2 deploy --- lib/state-file.ts | 2 +- scripts/{ => archive}/staking-router-v2/.env.sample | 0 scripts/{ => archive}/staking-router-v2/sr-v2-deploy.ts | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{ => archive}/staking-router-v2/.env.sample (100%) rename scripts/{ => archive}/staking-router-v2/sr-v2-deploy.ts (100%) diff --git a/lib/state-file.ts b/lib/state-file.ts index 8f64ea0ef9..c362a16136 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -88,7 +88,7 @@ export enum Sk { minFirstAllocationStrategy = "minFirstAllocationStrategy", triggerableWithdrawals = "triggerableWithdrawals", validatorExitDelayVerifier = "validatorExitDelayVerifier", - triggerableWithdrawalGateway = "triggerableWithdrawalGateway". + triggerableWithdrawalGateway = "triggerableWithdrawalGateway", } export function getAddress(contractKey: Sk, state: DeploymentState): string { diff --git a/scripts/staking-router-v2/.env.sample b/scripts/archive/staking-router-v2/.env.sample similarity index 100% rename from scripts/staking-router-v2/.env.sample rename to scripts/archive/staking-router-v2/.env.sample diff --git a/scripts/staking-router-v2/sr-v2-deploy.ts b/scripts/archive/staking-router-v2/sr-v2-deploy.ts similarity index 100% rename from scripts/staking-router-v2/sr-v2-deploy.ts rename to scripts/archive/staking-router-v2/sr-v2-deploy.ts From d002f585791f55270e3f1144311597a1c301583b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 18 May 2025 16:59:30 +0400 Subject: [PATCH 134/405] fix: triggerFullWithdrawals first argument type --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 47 ++++--------------- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 43 ++++++----------- ...ggerableWithdrawalsGateway__MockForVEB.sol | 12 +++-- ...dator-exit-bus-oracle.triggerExits.test.ts | 25 ++++------ ...awalGateway.triggerFullWithdrawals.test.ts | 29 ++++++------ 5 files changed, 56 insertions(+), 100 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 32a1056a71..0b13f7c8a6 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -94,7 +94,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { bytes32 public constant TWR_LIMIT_POSITION = keccak256("lido.TriggerableWithdrawalsGateway.maxExitRequestLimit"); /// Length in bytes of packed triggerable exit request - uint256 internal constant PACKED_EXIT_REQUEST_LENGTH = 56; uint256 internal constant PUBLIC_KEY_LENGTH = 48; uint256 public constant VERSION = 1; @@ -119,11 +118,11 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @dev Submits Triggerable Withdrawal Requests to the Withdrawal Vault as full withdrawal requests * for the specified validator public keys. * - * @param triggerableExitData A packed byte array containing one or more 56-byte items, each representing: - * MSB <-------------------------------------------------- LSB - * | 3 bytes | 5 bytes | 48 bytes | - * | stakingModuleId | nodeOperatorId | validatorPubkey | - * + * @param triggerableExitsData An array of `ValidatorData` structs, each representing a validator + * for which a withdrawal request will be submitted. Each entry includes: + * - `stakingModuleId`: ID of the staking module. + * - `nodeOperatorId`: ID of the node operator. + * - `pubkey`: Validator public key, 48 bytes length. * @param refundRecipient The address that will receive any excess ETH sent for fees. * @param exitType A parameter indicating the type of exit, passed to the Staking Module. * @@ -135,7 +134,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * - There is not enough limit quota left in the current frame to process all requests. */ function triggerFullWithdrawals( - bytes calldata triggerableExitData, + ValidatorData[] calldata triggerableExitsData, address refundRecipient, uint8 exitType ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { @@ -146,9 +145,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { refundRecipient = msg.sender; } - _checkExitRequestData(triggerableExitData); - - uint256 requestsCount = triggerableExitData.length / PACKED_EXIT_REQUEST_LENGTH; + uint256 requestsCount = triggerableExitsData.length; ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); if (twrLimitData.isExitLimitSet()) { @@ -170,8 +167,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { bytes memory pubkeys = new bytes(requestsCount * PUBLIC_KEY_LENGTH); for (uint256 i = 0; i < requestsCount; ++i) { - ValidatorData memory data = _parseExitRequestData(triggerableExitData, i); - + ValidatorData memory data = triggerableExitsData[i]; _copyPubkey(data.pubkey, pubkeys, i); _notifyStakingModule(data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalFee, exitType); @@ -237,39 +233,12 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { /// Internal functions - function _checkExitRequestData(bytes calldata triggerableExitData) internal pure { - if (triggerableExitData.length % PACKED_EXIT_REQUEST_LENGTH != 0) { - revert InvalidRequestsDataLength(); - } - } - function _checkFee(uint256 requestsCount, uint256 withdrawalFee) internal { if (msg.value < requestsCount * withdrawalFee) { revert InsufficientWithdrawalFee(requestsCount * withdrawalFee, msg.value); } } - function _parseExitRequestData( - bytes calldata request, - uint256 requestNumber - ) internal pure returns (ValidatorData memory data) { - uint256 dataWithoutPubkey; - uint256 offset; - bytes calldata pubkey; - - assembly { - offset := add(request.offset, mul(requestNumber, PACKED_EXIT_REQUEST_LENGTH)) - dataWithoutPubkey := shr(192, calldataload(offset)) - pubkey.length := 48 - // 8 bytes = 3 bytes (module id) + 5 bytes (operator id) - pubkey.offset := add(offset, 8) - } - - data.nodeOperatorId = uint40(dataWithoutPubkey); - data.stakingModuleId = uint24(dataWithoutPubkey >> 40); - data.pubkey = pubkey; - } - function _copyPubkey(bytes memory pubkey, bytes memory pubkeys, uint256 index) internal pure { assembly { let pubkeyMemPtr := add(pubkey, 32) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 6c5ee864cb..2caead1e5f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -11,8 +11,14 @@ import {PausableUntil} from "../utils/PausableUntil.sol"; import {IValidatorsExitBus} from "../interfaces/IValidatorsExitBus.sol"; interface ITriggerableWithdrawalsGateway { + struct ValidatorData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; + } + function triggerFullWithdrawals( - bytes calldata triggerableExitData, + ValidatorData[] calldata triggerableExitData, address refundRecipient, uint8 exitType ) external payable; @@ -322,7 +328,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkExitRequestData(requestsData.data, requestsData.dataFormat); _checkContractVersion(requestStatus.contractVersion); - bytes memory exits = new bytes(keyIndexes.length * PACKED_TWG_EXIT_REQUEST_LENGTH); + ITriggerableWithdrawalsGateway.ValidatorData[] + memory triggerableExitData = new ITriggerableWithdrawalsGateway.ValidatorData[](keyIndexes.length); uint256 lastKeyIndex = type(uint256).max; @@ -343,11 +350,15 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ValidatorData memory validatorData = _getValidatorData(requestsData.data, keyIndexes[i]); if (validatorData.moduleId == 0) revert InvalidRequestsData(); - _copyValidatorData(validatorData, exits, i); + triggerableExitData[i] = ITriggerableWithdrawalsGateway.ValidatorData( + validatorData.moduleId, + validatorData.nodeOpId, + validatorData.pubkey + ); } ITriggerableWithdrawalsGateway(LOCATOR.triggerableWithdrawalsGateway()).triggerFullWithdrawals{value: msg.value}( - exits, + triggerableExitData, refundRecipient, exitType ); @@ -663,30 +674,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - /// Methods for working with TWG exit data type - /// | MSB <------------------------------------------------ LSB - /// | 3 bytes | 5 bytes | 48 bytes | - /// | stakingModuleId | nodeOperatorId | validatorPubkey | - - function _copyValidatorData( - ValidatorData memory validatorData, - bytes memory exitData, - uint256 index - ) internal pure { - uint256 nodeOpId = validatorData.nodeOpId; - uint256 moduleId = validatorData.moduleId; - bytes memory pubkey = validatorData.pubkey; - - assembly { - let exitDataOffset := add(exitData, add(32, mul(56, index))) - let id := or(shl(40, moduleId), nodeOpId) - mstore(exitDataOffset, shl(192, id)) - let pubkeyOffset := add(pubkey, 32) - mstore(add(exitDataOffset, 8), mload(pubkeyOffset)) - mstore(add(exitDataOffset, 40), mload(add(pubkeyOffset, 32))) - } - } - /// Storage helpers function _storageExitRequestsHashes() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { bytes32 position = EXIT_REQUESTS_HASHES_POSITION; diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol index 2e5f79eca1..e88edd162d 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol @@ -1,13 +1,19 @@ pragma solidity 0.8.9; contract TriggerableWithdrawalsGateway__MockForVEB { - event Mock__triggerFullWithdrawalsTriggered(bytes triggerableExitData, address refundRecipient, uint8 exitType); + event Mock__triggerFullWithdrawalsTriggered(uint256 exitsCount, address refundRecipient, uint8 exitType); + + struct ValidatorData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; + } function triggerFullWithdrawals( - bytes calldata triggerableExitData, + ValidatorData[] calldata triggerableExitData, address refundRecipient, uint8 exitType ) external payable { - emit Mock__triggerFullWithdrawalsTriggered(triggerableExitData, refundRecipient, exitType); + emit Mock__triggerFullWithdrawalsTriggered(triggerableExitData.length, refundRecipient, exitType); } } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 6577083df5..72179df875 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -73,25 +73,20 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { return "0x" + requests.map(encodeExitRequestHex).join(""); }; - const encodeTWGExitRequestsData = ({ moduleId, nodeOpId, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + pubkeyHex; - }; - - const encodeTWGExitDataList = (requests: ExitRequest[]) => { - return "0x" + requests.map(encodeTWGExitRequestsData).join(""); + const createValidatorDataList = (requests: ExitRequest[]) => { + return requests.map((request) => ({ + stakingModuleId: request.moduleId, + nodeOperatorId: request.nodeOpId, + pubkey: request.valPubkey, + })); }; const deploy = async () => { const deployed = await deployVEBO(admin.address); - const locator = deployed.locator; oracle = deployed.oracle; consensus = deployed.consensus; triggerableWithdrawalsGateway = deployed.triggerableWithdrawalsGateway; - console.log("twg=", await locator.triggerableWithdrawalsGateway()); - await initVEBO({ admin: admin.address, oracle, @@ -175,11 +170,11 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { { value: 4 }, ); - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); await expect(tx) .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests, admin.address, 0); + .withArgs(requests.length, admin.address, 0); }); it("should triggers exits only for validators in selected request indexes", async () => { @@ -193,11 +188,11 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }, ); - const requests = encodeTWGExitDataList(exitRequests.filter((req, i) => [0, 1, 3].includes(i))); + const requests = createValidatorDataList(exitRequests.filter((req, i) => [0, 1, 3].includes(i))); await expect(tx) .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests, admin.address, 0); + .withArgs(requests.length, admin.address, 0); }); it("should revert with error if the hash of `requestsData` was not previously submitted in the VEB", async () => { diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 98999525b3..26db0ab5c7 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -9,7 +9,7 @@ import { WithdrawalVault__MockForTWG, } from "typechain-types"; -import { de0x, numberToHex } from "lib"; +import { de0x } from "lib"; import { deployLidoLocator, updateLidoLocatorImplementation } from "../deploy/locator"; @@ -41,14 +41,12 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { let admin: HardhatEthersSigner; let authorizedEntity: HardhatEthersSigner; - const encodeTWGExitRequestsData = ({ moduleId, nodeOpId, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + pubkeyHex; - }; - - const encodeTWGExitDataList = (requests: ExitRequest[]) => { - return "0x" + requests.map(encodeTWGExitRequestsData).join(""); + const createValidatorDataList = (requests: ExitRequest[]) => { + return requests.map((request) => ({ + stakingModuleId: request.moduleId, + nodeOperatorId: request.nodeOpId, + pubkey: request.valPubkey, + })); }; before(async () => { @@ -72,8 +70,9 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }); it("should revert if caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE", async () => { - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await expect( triggerableWithdrawalsGateway .connect(authorizedEntity) @@ -85,7 +84,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); await expect( triggerableWithdrawalsGateway @@ -105,7 +104,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }); it("should add withdrawal request", async () => { - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) @@ -153,7 +152,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }); it("should revert if limit doesnt cover requests count", async () => { - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); await expect( triggerableWithdrawalsGateway @@ -165,7 +164,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }); it("should revert if limit doesnt cover requests count", async () => { - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); await expect( triggerableWithdrawalsGateway @@ -197,7 +196,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }); it("should add withdrawal request ias limit is enough for processing all requests", async () => { - const requests = encodeTWGExitDataList(exitRequests); + const requests = createValidatorDataList(exitRequests); const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) From 7f42ec1292234425ac4dc994a65649f86b001eb8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 18 May 2025 17:32:14 +0400 Subject: [PATCH 135/405] fix: set limits in TWG via constructor --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 41 ++++++++++++------- .../steps/0090-deploy-non-aragon-contracts.ts | 9 ++-- ...TriggerableWithdrawalsGateway__Harness.sol | 8 +++- ...-bus-oracle.submitExitRequestsData.test.ts | 21 ++++++++++ ...awalGateway.triggerFullWithdrawals.test.ts | 3 ++ test/deploy/validatorExitBusOracle.ts | 16 ++++++-- 6 files changed, 74 insertions(+), 24 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 0b13f7c8a6..20c4555ad1 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -107,11 +107,18 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { assert(address(this).balance == balanceBeforeCall); } - constructor(address admin, address lidoLocator) { + constructor( + address admin, + address lidoLocator, + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration + ) { if (admin == address(0)) revert AdminCannotBeZero(); LOCATOR = ILidoLocator(lidoLocator); _setupRole(DEFAULT_ADMIN_ROLE, admin); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); } /** @@ -188,20 +195,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { uint256 exitsPerFrame, uint256 frameDuration ) external onlyRole(TW_EXIT_REPORT_LIMIT_ROLE) { - require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); - - uint256 timestamp = _getTimestamp(); - - TWR_LIMIT_POSITION.setStorageExitRequestLimit( - TWR_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( - maxExitRequestsLimit, - exitsPerFrame, - frameDuration, - timestamp - ) - ); - - emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); } /** @@ -304,4 +298,21 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { return twrLimitData.calculateCurrentExitLimit(_getTimestamp()); } + + function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { + require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + + uint256 timestamp = _getTimestamp(); + + TWR_LIMIT_POSITION.setStorageExitRequestLimit( + TWR_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, + timestamp + ) + ); + + emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + } } diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 16f234401f..c9814189fb 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -222,14 +222,15 @@ export async function main() { ); // Deploy Triggerable Withdrawals Gateway + const maxExitRequestsLimit = 13000; + const exitsPerFrame = 1; + const frameDuration = 48; + const triggerableWithdrawalsGateway = await deployWithoutProxy( Sk.triggerableWithdrawalsGateway, "TriggerableWithdrawalsGateway", deployer, - [ - deployer, // address admin - locator.address, // address lidoLocator - ], + [admin, locator.address, maxExitRequestsLimit, exitsPerFrame, frameDuration], ); // Update LidoLocator with valid implementation diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol index 264df27d61..f0abea1363 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol @@ -5,7 +5,13 @@ import {TriggerableWithdrawalsGateway} from "contracts/0.8.9/TriggerableWithdraw contract TriggerableWithdrawalsGateway__Harness is TriggerableWithdrawalsGateway { uint256 internal _time = 2513040315; - constructor(address admin, address lidoLocator) TriggerableWithdrawalsGateway(admin, lidoLocator) {} + constructor( + address admin, + address lidoLocator, + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDuration + ) TriggerableWithdrawalsGateway(admin, lidoLocator, maxExitRequestsLimit, exitsPerFrame, frameDuration) {} function getTimestamp() external view returns (uint256) { return _time; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 34d22837c0..45a35b7f21 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -293,5 +293,26 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { "RequestsAlreadyDelivered", ); }); + + it("Should revert if maxBatchSize exceeded", async () => { + const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); + await oracle.grantRole(role, authorizedEntity); + + const maxRequestsPerBatch = 4; + + await oracle.connect(authorizedEntity).setMaxRequestsPerBatch(maxRequestsPerBatch); + + exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + await expect(oracle.submitExitRequestsData(exitRequest)) + .to.be.revertedWithCustomError(oracle, "MaxRequestsBatchSizeExceeded") + .withArgs(exitRequests.length, maxRequestsPerBatch); + }); }); }); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 26db0ab5c7..b98d8874cb 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -66,6 +66,9 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { triggerableWithdrawalsGateway = await ethers.deployContract("TriggerableWithdrawalsGateway__Harness", [ admin, locatorAddr, + 100, + 1, + 48, ]); }); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 5c82455f9d..14c73bfc7a 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -97,6 +97,10 @@ interface VEBOConfig { consensusVersion?: bigint; lastProcessingRefSlot?: number; resumeAfterDeploy?: boolean; + maxRequestsPerBatch?: number; + maxExitRequestsLimit?: number; + exitsPerFrame?: number; + frameDuration?: number; } export async function initVEBO({ @@ -107,16 +111,20 @@ export async function initVEBO({ consensusVersion = CONSENSUS_VERSION, lastProcessingRefSlot = 0, resumeAfterDeploy = false, + maxRequestsPerBatch = 600, + maxExitRequestsLimit = 13000, + exitsPerFrame = 1, + frameDuration = 48, }: VEBOConfig) { const initTx = await oracle.initialize( admin, await consensus.getAddress(), consensusVersion, lastProcessingRefSlot, - 600, - 13000, - 1, - 48, + maxRequestsPerBatch, + maxExitRequestsLimit, + exitsPerFrame, + frameDuration, ); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin); From 66bbdb61be7b58a272b13071e0ee5f5cacdf7a17 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 16 May 2025 10:21:12 +0200 Subject: [PATCH 136/405] feat: add validator exit delay verifier integration tests --- .env.example | 1 + .../0.8.25/ValidatorExitDelayVerifier.sol | 28 ++-- .../0.8.25/interfaces/IValidatorsExitBus.sol | 5 +- deployed-hoodi.json | 4 +- globals.d.ts | 1 + lib/eips/eip4788.ts | 10 ++ lib/eips/eip7251.ts | 34 ++++ lib/eips/index.ts | 1 + lib/protocol/discover.ts | 5 + lib/protocol/networks.ts | 1 + lib/protocol/provision.ts | 3 +- lib/protocol/types.ts | 4 + .../steps/0090-deploy-non-aragon-contracts.ts | 4 +- scripts/triggerable-withdrawals/tw-deploy.ts | 4 +- scripts/triggerable-withdrawals/tw-verify.ts | 4 +- .../0.8.25/validatorExitDelayVerifier.test.ts | 4 +- test/0.8.25/validatorState.ts | 91 +++++------ .../report-validator-exit-delay.ts | 145 ++++++++++++++++++ 18 files changed, 278 insertions(+), 71 deletions(-) create mode 100644 lib/eips/eip7251.ts create mode 100644 test/integration/report-validator-exit-delay.ts diff --git a/.env.example b/.env.example index f61f0a1aea..753e80a619 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,7 @@ LOCAL_ORACLE_DAEMON_CONFIG_ADDRESS= LOCAL_ORACLE_REPORT_SANITY_CHECKER_ADDRESS= LOCAL_SDVT_ADDRESS= LOCAL_STAKING_ROUTER_ADDRESS= +LOCAL_VALIDATOR_EXIT_DELAY_VERIFIER_ADDRESS= LOCAL_VALIDATORS_EXIT_BUS_ORACLE_ADDRESS= LOCAL_WITHDRAWAL_QUEUE_ADDRESS= LOCAL_WITHDRAWAL_VAULT_ADDRESS= diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 3566af1da5..31bd64f2f6 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -106,8 +106,8 @@ contract ValidatorExitDelayVerifier { error InvalidPivotSlot(); error ZeroLidoLocatorAddress(); error ExitRequestNotEligibleOnProvableBeaconBlock( - uint64 provableBeaconBlockTimestamp, - uint64 eligibleExitRequestTimestamp + uint256 provableBeaconBlockTimestamp, + uint256 eligibleExitRequestTimestamp ); error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); @@ -178,7 +178,7 @@ contract ValidatorExitDelayVerifier { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); - uint64 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); + uint256 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { ValidatorWitness calldata witness = validatorWitnesses[i]; @@ -189,7 +189,7 @@ contract ValidatorExitDelayVerifier { witness.exitRequestIndex ); - uint64 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( + uint256 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( requestsDeliveryHistory, witness, proofSlotTimestamp @@ -229,7 +229,7 @@ contract ValidatorExitDelayVerifier { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); - uint64 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); + uint256 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { ValidatorWitness calldata witness = validatorWitnesses[i]; @@ -240,7 +240,7 @@ contract ValidatorExitDelayVerifier { witness.exitRequestIndex ); - uint64 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( + uint256 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( requestsDeliveryHistory, witness, proofSlotTimestamp @@ -329,24 +329,24 @@ contract ValidatorExitDelayVerifier { /** * @dev Determines how many seconds have passed since a validator was first eligible * to exit after ValidatorsExitBusOracle exit request. - * @return uint64 The elapsed seconds since the earliest eligible exit request time. + * @return uint256 The elapsed seconds since the earliest eligible exit request time. */ function _getSecondsSinceExitRequestEligible( ExitRequestsDeliveryHistory memory history, ValidatorWitness calldata witness, - uint64 referenceSlotTimestamp - ) internal view returns (uint64) { - uint64 validatorExitRequestTimestamp = _getExitRequestTimestamp(history, witness.exitRequestIndex); + uint256 referenceSlotTimestamp + ) internal view returns (uint256) { + uint256 validatorExitRequestTimestamp = _getExitRequestTimestamp(history, witness.exitRequestIndex); // The earliest a validator can voluntarily exit is after the Shard Committee Period // subsequent to its activation epoch. - uint64 earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + + uint256 earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + (witness.activationEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + SHARD_COMMITTEE_PERIOD_IN_SECONDS; // The actual eligible timestamp is the max between the exit request submission time // and the earliest possible voluntary exit time. - uint64 eligibleExitRequestTimestamp = validatorExitRequestTimestamp > earliestPossibleVoluntaryExitTimestamp + uint256 eligibleExitRequestTimestamp = validatorExitRequestTimestamp > earliestPossibleVoluntaryExitTimestamp ? validatorExitRequestTimestamp : earliestPossibleVoluntaryExitTimestamp; @@ -380,7 +380,7 @@ contract ValidatorExitDelayVerifier { function _getExitRequestTimestamp( ExitRequestsDeliveryHistory memory history, uint256 index - ) internal pure returns (uint64 validatorExitRequestTimestamp) { + ) internal pure returns (uint256 validatorExitRequestTimestamp) { if (index >= history.totalItemsCount) { revert KeyIndexOutOfRange(index, history.totalItemsCount); } @@ -400,7 +400,7 @@ contract ValidatorExitDelayVerifier { assert(false); } - function _slotToTimestamp(uint64 slot) internal view returns (uint64) { + function _slotToTimestamp(uint64 slot) internal view returns (uint256) { return GENESIS_TIME + slot * SECONDS_PER_SLOT; } } diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol index 907deea590..11809b394d 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -5,11 +5,10 @@ pragma solidity 0.8.25; struct DeliveryHistory { - uint64 timestamp; - // Key index in exit request array + // index in array of requests uint256 lastDeliveredKeyIndex; + uint256 timestamp; } - interface IValidatorsExitBus { function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash diff --git a/deployed-hoodi.json b/deployed-hoodi.json index e4831a5807..3bdc4e66e2 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -604,8 +604,8 @@ "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", "0x0000000000000000000000000000000000000000000000000096000000000028", "0x0000000000000000000000000000000000000000000000000096000000000028", - "0x000000000000000000000000000000000000000000000000000000000161c004", - "0x000000000000000000000000000000000000000000000000000000000161c004", + "0x0000000000000000000000000000000000000000000000000000000000005b00", + "0x0000000000000000000000000000000000000000000000000000000000005b00", 1, 1, 32, diff --git a/globals.d.ts b/globals.d.ts index 4f4052302f..7d4b1f67d5 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -44,6 +44,7 @@ declare namespace NodeJS { LOCAL_ORACLE_REPORT_SANITY_CHECKER_ADDRESS?: string; LOCAL_SDVT_ADDRESS?: string; LOCAL_STAKING_ROUTER_ADDRESS?: string; + LOCAL_VALIDATOR_EXIT_DELAY_VERIFIER_ADDRESS?: string; LOCAL_VALIDATORS_EXIT_BUS_ORACLE_ADDRESS?: string; LOCAL_WITHDRAWAL_QUEUE_ADDRESS?: string; LOCAL_WITHDRAWAL_VAULT_ADDRESS?: string; diff --git a/lib/eips/eip4788.ts b/lib/eips/eip4788.ts index 5ce929262c..449c1f04bd 100644 --- a/lib/eips/eip4788.ts +++ b/lib/eips/eip4788.ts @@ -1,3 +1,5 @@ +import { ethers } from "hardhat"; + import { impersonate } from "lib"; // Address of the Beacon Block Storage contract, which exposes beacon chain roots. @@ -21,3 +23,11 @@ export const updateBeaconBlockRoot = async (root: string): Promise => { return blockDetails.timestamp; }; + +export const ensureEIP4788BeaconBlockRootContractPresent = async (): Promise => { + const code = await ethers.provider.getCode(BEACON_ROOTS_ADDRESS); + + if (code === "0x") { + throw new Error(`EIP7788 Beacon Block Root contract not found at ${BEACON_ROOTS_ADDRESS}`); + } +}; diff --git a/lib/eips/eip7251.ts b/lib/eips/eip7251.ts new file mode 100644 index 0000000000..8a98437c49 --- /dev/null +++ b/lib/eips/eip7251.ts @@ -0,0 +1,34 @@ +import { ethers } from "hardhat"; + +import { EIP7251ConsolidationRequest__Mock } from "typechain-types"; + +import { log } from "lib"; + +// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7251.md#configuration +export const EIP7251_ADDRESS = "0x0000BBdDc7CE488642fb579F8B00f3a590007251"; +export const EIP7251_MIN_WITHDRAWAL_REQUEST_FEE = 1n; + +export const deployEIP7251ConsolidationRequestContract = async ( + fee: bigint, +): Promise => { + const eip7251Mock = await ethers.deployContract("EIP7251ConsolidationRequest__Mock"); + const eip7251MockAddress = await eip7251Mock.getAddress(); + + await ethers.provider.send("hardhat_setCode", [EIP7251_ADDRESS, await ethers.provider.getCode(eip7251MockAddress)]); + + const contract = await ethers.getContractAt("EIP7251ConsolidationRequest__Mock", EIP7251_ADDRESS); + await contract.mock__setFee(fee); + + return contract; +}; + +export const ensureEIP7251ConsolidationRequestContractPresent = async (): Promise => { + const code = await ethers.provider.getCode(EIP7251_ADDRESS); + + if (code === "0x") { + log.warning(`EIP7251 consolidation request contract not found at ${EIP7251_ADDRESS}`); + + await deployEIP7251ConsolidationRequestContract(EIP7251_MIN_WITHDRAWAL_REQUEST_FEE); + log.success("EIP7251 consolidation request contract is present"); + } +}; diff --git a/lib/eips/index.ts b/lib/eips/index.ts index 93662f8400..b5e8780295 100644 --- a/lib/eips/index.ts +++ b/lib/eips/index.ts @@ -1,3 +1,4 @@ export * from "./eip712"; export * from "./eip4788"; export * from "./eip7002"; +export * from "./eip7251"; diff --git a/lib/protocol/discover.ts b/lib/protocol/discover.ts index 71eaa55384..3c37390121 100644 --- a/lib/protocol/discover.ts +++ b/lib/protocol/discover.ts @@ -84,6 +84,10 @@ const getCoreContracts = async (locator: LoadedContract, config: Pr ), burner: loadContract("Burner", config.get("burner") || (await locator.burner())), stakingRouter: loadContract("StakingRouter", config.get("stakingRouter") || (await locator.stakingRouter())), + validatorExitDelayVerifier: loadContract( + "ValidatorExitDelayVerifier", + config.get("validatorExitDelayVerifier") || (await locator.validatorExitDelayVerifier()), + ), validatorsExitBusOracle: loadContract( "ValidatorsExitBusOracle", config.get("validatorsExitBusOracle") || (await locator.validatorsExitBusOracle()), @@ -175,6 +179,7 @@ export async function discover() { "Execution Layer Rewards Vault": foundationContracts.elRewardsVault.address, "Withdrawal Queue": foundationContracts.withdrawalQueue.address, "Withdrawal Vault": foundationContracts.withdrawalVault.address, + "Validator Exit Delay Verifier": foundationContracts.validatorExitDelayVerifier.address, "Validators Exit Bus Oracle": foundationContracts.validatorsExitBusOracle.address, "Oracle Daemon Config": foundationContracts.oracleDaemonConfig.address, "Oracle Report Sanity Checker": foundationContracts.oracleReportSanityChecker.address, diff --git a/lib/protocol/networks.ts b/lib/protocol/networks.ts index c82cc91c50..0d94f2e614 100644 --- a/lib/protocol/networks.ts +++ b/lib/protocol/networks.ts @@ -53,6 +53,7 @@ const defaultEnv = { oracleReportSanityChecker: "ORACLE_REPORT_SANITY_CHECKER_ADDRESS", burner: "BURNER_ADDRESS", stakingRouter: "STAKING_ROUTER_ADDRESS", + validatorExitDelayVerifier: "VALIDATOR_EXIT_DELAY_VERIFIER_ADDRESS", validatorsExitBusOracle: "VALIDATORS_EXIT_BUS_ORACLE_ADDRESS", withdrawalQueue: "WITHDRAWAL_QUEUE_ADDRESS", withdrawalVault: "WITHDRAWAL_VAULT_ADDRESS", diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 8d2f656fa0..9493554bd5 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -1,5 +1,5 @@ import { log } from "lib"; -import { ensureEIP7002WithdrawalRequestContractPresent } from "lib/eips"; +import { ensureEIP4788BeaconBlockRootContractPresent, ensureEIP7002WithdrawalRequestContractPresent } from "lib/eips"; import { ensureHashConsensusInitialEpoch, @@ -26,6 +26,7 @@ export const provision = async (ctx: ProtocolContext) => { // Ensure necessary precompiled contracts are present await ensureEIP7002WithdrawalRequestContractPresent(); + await ensureEIP4788BeaconBlockRootContractPresent(); // Ensure protocol is fully operational await ensureHashConsensusInitialEpoch(ctx); diff --git a/lib/protocol/types.ts b/lib/protocol/types.ts index ce2223804b..44cc3841a2 100644 --- a/lib/protocol/types.ts +++ b/lib/protocol/types.ts @@ -17,6 +17,7 @@ import { OracleDaemonConfig, OracleReportSanityChecker, StakingRouter, + ValidatorExitDelayVerifier, ValidatorsExitBusOracle, WithdrawalQueueERC721, WithdrawalVault, @@ -38,6 +39,7 @@ export type ProtocolNetworkItems = { oracleReportSanityChecker: string; burner: string; stakingRouter: string; + validatorExitDelayVerifier: string; validatorsExitBusOracle: string; withdrawalQueue: string; withdrawalVault: string; @@ -63,6 +65,7 @@ export interface ContractTypes { OracleReportSanityChecker: OracleReportSanityChecker; Burner: Burner; StakingRouter: StakingRouter; + ValidatorExitDelayVerifier: ValidatorExitDelayVerifier; ValidatorsExitBusOracle: ValidatorsExitBusOracle; WithdrawalQueueERC721: WithdrawalQueueERC721; WithdrawalVault: WithdrawalVault; @@ -92,6 +95,7 @@ export type CoreContracts = { oracleReportSanityChecker: LoadedContract; burner: LoadedContract; stakingRouter: LoadedContract; + validatorExitDelayVerifier: LoadedContract; validatorsExitBusOracle: LoadedContract; withdrawalQueue: LoadedContract; withdrawalVault: LoadedContract; diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index b8dbb50015..3c3d82e050 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -209,8 +209,8 @@ export async function main() { locator.address, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, 1, // uint64 firstSupportedSlot, 1, // uint64 pivotSlot, chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index fd296df5d8..4f11c933f9 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -95,8 +95,8 @@ async function main() { locator.address, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, 1, // uint64 firstSupportedSlot, 1, // uint64 pivotSlot, 32, // uint32 slotsPerEpoch, diff --git a/scripts/triggerable-withdrawals/tw-verify.ts b/scripts/triggerable-withdrawals/tw-verify.ts index 86b38e0173..fae50f443b 100644 --- a/scripts/triggerable-withdrawals/tw-verify.ts +++ b/scripts/triggerable-withdrawals/tw-verify.ts @@ -49,8 +49,8 @@ async function main() { locator.address, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesPrev, - "0x000000000000000000000000000000000000000000000000000000000161c004", // GIndex gIHistoricalSummariesCurr, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, 1, // uint64 firstSupportedSlot, 1, // uint64 pivotSlot, 32, // uint32 slotsPerEpoch, diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 71a4ba3e29..f4114e1c52 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -101,8 +101,8 @@ describe("ValidatorExitDelayVerifier.sol", () => { }); describe("verifyValidatorExitDelay method", () => { - const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000056000000000028"; - const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000003b00"; + const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; let validatorExitDelayVerifier: ValidatorExitDelayVerifier; diff --git a/test/0.8.25/validatorState.ts b/test/0.8.25/validatorState.ts index 99519dcb51..af8a5034df 100644 --- a/test/0.8.25/validatorState.ts +++ b/test/0.8.25/validatorState.ts @@ -19,6 +19,7 @@ export type ValidatorState = { }; export type ValidatorStateProof = { + firstValidatorGI: string; beaconBlockHeaderRoot: string; beaconBlockHeader: BlockHeader; futureBeaconBlockHeaderRoot: string; @@ -30,55 +31,57 @@ export type ValidatorStateProof = { }; export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { - beaconBlockHeaderRoot: "0xa7f100995b35584c670fe25aa97ae23a8305f5eba8eee3532dedfcc8cf934dca", + beaconBlockHeaderRoot: "0x3959b7073981bd6b71b8dfb37cba8505d69291de2c7c55167be7ed6d361903b7", beaconBlockHeader: { - slot: 10080800, + slot: 22140000, proposerIndex: "1337", - parentRoot: "0x03aa03b69bedd0e423ba545d38e216c4bf2f423e6f5a308477501b9a31ff8d8f", - stateRoot: "0x508ee9ba052583d9cae510e7333d9776514d42cd10b853395dc24c275a95bc1d", - bodyRoot: "0x8db50db3356352a01197abd32a52f97c2bb9b48bdbfb045ea4a7f67c9b84be0b", + parentRoot: "0x9fff93777a5d7464d400991242767c87401bea8444a19a4e9d69c6fcbf9e8870", + stateRoot: "0x8ae908388464dc5e368cf76126a6e29eb9ac12a1690ea4131eadbf8fd78ae355", + bodyRoot: "0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1", }, - futureBeaconBlockHeaderRoot: "0xca237c523d507a91b2b91389d517c0d4b03e66732984b5d56c74a47a06eb7ef4", + futureBeaconBlockHeaderRoot: "0x0e4ac8359cd39eb19803d5b7a299f337b93bf6f683fd4989f5c4ba1804354655", futureBeaconBlockHeader: { - slot: 14411095, + slot: 46908000, proposerIndex: "31415", - parentRoot: "0x391127160b857e9cdec243ea70f42082d28135c75880c2b5c505b98dec726c79", - stateRoot: "0x972b36a298aa6bc1d205d115f0384fe1e3a301625907c07f5344b26337d5f494", - bodyRoot: "0x8db50db3356352a01197abd32a52f97c2bb9b48bdbfb045ea4a7f67c9b84be0b", + parentRoot: "0x3ad291882aef24918d223a3f89a6ccc45e0a8f1071977233e3c9f83e5dd5db26", + stateRoot: "0x40231cae24f6beb7c58eac9e7680b0856eeae5604b697edd2eeae0525b185d96", + bodyRoot: "0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1", }, + + firstValidatorGI: "0x0000000000000000000000000000000000000000000000000096000000000028", validator: { - pubkey: "0x800000c8a5364c1d1e3c4cdb65a28fd21daff4e1fb426c0fb09808105467e4a490d8b3507e7efffbd71024129f1a6b8d", - withdrawalCredentials: "0x0100000000000000000000007cd73ab82e3a8e74a3fdfd6a41fed60536b8e501", + pubkey: "0xaeb399bf5648b0e9980c1731824c269631a41320c3d7f730c40587e1a37a5e1c8b5755fd90080a7b3fb90d3fd419c0a7", + withdrawalCredentials: "0x010000000000000000000000b3e29c46ee1745724417c0c51eb2351a1c01cf36", effectiveBalance: 32000000000n, - activationEligibilityEpoch: 207905n, - activationEpoch: 217838n, + activationEligibilityEpoch: 10n, + activationEpoch: 16n, exitEpoch: 18446744073709551615n, withdrawableEpoch: 18446744073709551615n, slashed: false, - index: 773833, + index: 129, }, validatorProof: [ - "0xcb6bfee06d1227e0f2d9cca5bd508b7fc1069379141f44b0d683eb5aec483005", - "0x1c8852d46a4244090d9b25822086fb3616072c2ae7b8a89d04b4db9953ed922d", - "0x671048760e5cadb005cf8ed6a11fd398b882cb2610c8ab25c0cd8f1bb2a663dc", - "0x5fa5cf691165e3159b86e357c2a4e82c867014e7ec2570e38d3cc3bb694b35e2", - "0xe5ef1dd73ffa166b176139a24d4d8b53361df9dc26f5ac51c0bf642d9b5dbf25", - "0xdb356970833ed8b780d20530aa5e0a8bd5ebd2c751c4e9ddc25e0097c629e750", - "0xceb46d7f9478540174155825a82db4b38201d4d4c047dbefb7546eaea942a6de", - "0x89c916b9678fbcde3d7d07c26de94fd62c2ae51800b392a83b6f346126c40c6d", - "0x1da07003bdc86171360808803bbeb41919e25118c7e8aefb9a21f46d5f19e72b", - "0xad57317afc56b03b6e198ed270b64db4a8f25f132dbf6b56d287c97c6b525db9", - "0x40f9f5e8fe27eadfcf3c3af2ff0e02ccdce8b536cd4faf5b8ed0a36d40247663", - "0x05b761f89ed65cf91ac63aad3c8c50bb2aa0c277639d0fd784b6e0b2ccf05395", - "0x3fd79435deff850fae1bdef0d77a3ffe93b092172e225837cf4ef141fa5689cb", - "0x044709022ba087a75f6ea66b7a3a1e23fe3712fd351c401f03b578ba8aa0a603", - "0xe45e266fed3b13b3c8a81fa3064b5af5e25f9b274da2da4032358766d23a9eac", - "0x046d692534483df5307eb2d69c5a1f8b27068ad1dda96423f854fc88e19571a8", - "0x7f9ef0a29605f457a735757148c16f88bda95ee0eaaf7e5351fa6ea3aa3cf305", - "0x1a1965b540ad413b822af6f49160553bd0fd6f9adefcdf5ef862262af43ddd54", - "0x56206a2520034ea75dab955bc85a305b4681191255111c2c8d27ac23173e5647", - "0x5ee416708837b80e3f2b625cbd130839d8efdbe88bcbb0076ffdd8cd2229c103", - "0xb0019865e6408ce0d5a36a6188d7c1e3272976c6a1ccbc58e6c35cca19a8fb6c", + "0x13d8db07469f8bc21ec0a89cea8dd2d91131d245658a52c496eba69b0d53cbbb", + "0xe5ff0954daf817e5cd32f00b983945cba3d130d60fa5cabed35bf889fd28249a", + "0x1fec0d4d8cf525de88e940206b5d927b9183e83babf8cf9bf0354d66ba272d68", + "0xca49e8bcc3f7e484104fd399fb0d4899026311d531c76375774091b45596554b", + "0xd4e91bc9a1de135af4734e6890b2c55eeac7861f7dc015d54f7f8facdbaed8c1", + "0xf3568470a660b87488bd6d6ad46084e7fda30f36cc12a7e6fe6487bb9f833eca", + "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0xb893aa56b221bb6e19bcca6277d6460edb235bfb1a491c28744be5753e7556ff", + "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", @@ -98,14 +101,15 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", - "0x455d180000000000000000000000000000000000000000000000000000000000", - "0x87ed190000000000000000000000000000000000000000000000000000000000", - "0xb95e35337be0ebfa1ae00f659346dfce7bb59865d4bde0299df3e548c24e00aa", - "0x001b9a4b331100497e69174269986fcd37e62145bf51123cb67fb3108c2422fd", - "0x339028e1baffbe94bcf2d5e671de99ff958e0c8afd8c1844370dc1af2fa00315", - "0xa48b01f6407ef8dc6b77f5df0fa4fef5b1b9795c7e99c13fa8aad0eac6036676", + "0xa600000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x653f8df7fc1818ec14d4e4ffaffa4b5fef87482ec3e691cd2d1d0f97b479e44f", + "0xbf684169745bffbc1837466cf8d60daf6b6aa0f1b237cc67d032035b9b8f054e", + "0x63ce6bd2bcbc959710fd8ca4c2e521d294380851fd4595786c2377d95d735d45", + "0x122b6933c3d4037c4cde8edf021c974e100977181a7905101b4fad8401158ec7", + "0xec18ccb0df14bb427a0f393e28e84e84ce166f26e14a528edc9a309485b845a8", ], - historicalSummariesGI: "0x000000000000000000000000000000000000000000000000000000ec00000000", + historicalSummariesGI: "0x0000000000000000000000000000000000000000000000000000016c00000000", historicalRootProof: [ "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", @@ -138,5 +142,6 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", "0xe537052d30df4f0436cd5a3c5debd331c770d9df46da47e0e3db74906186fa09", "0x4616e1d9312a92eb228e8cd5483fa1fca64d99781d62129bc53718d194b98c45", + "0xa1381fdc64967103fe79c0705727851ce61e7f91bee7e3e7759f9283c91ff7ff", ], }; diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts new file mode 100644 index 0000000000..f90609d3c5 --- /dev/null +++ b/test/integration/report-validator-exit-delay.ts @@ -0,0 +1,145 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { advanceChainTime, ether, getCurrentBlockTimestamp, updateBeaconBlockRoot } from "lib"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { + encodeExitRequestsDataListWithFormat, + toHistoricalHeaderWitness, + toProvableBeaconBlockHeader, + toValidatorWitness, +} from "test/0.8.25/validatorExitDelayVerifierHelpers"; +import { ACTIVE_VALIDATOR_PROOF } from "test/0.8.25/validatorState"; +import { Snapshot } from "test/suite"; + +describe("Report Validator Exit Delay", () => { + let ctx: ProtocolContext; + let beforeEachSnapshot: string; + + let vebReportSubmitter: HardhatEthersSigner; + + const moduleId = 1; // NOR module ID + + before(async () => { + ctx = await getProtocolContext(); + + [vebReportSubmitter] = await ethers.getSigners(); + + const { nor, stakingRouter, validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + const agentSigner = await ctx.getSigner("agent", ether("1")); + await validatorsExitBusOracle + .connect(agentSigner) + .grantRole(await validatorsExitBusOracle.SUBMIT_REPORT_HASH_ROLE(), vebReportSubmitter.address); + + if (await validatorsExitBusOracle.isPaused()) { + await validatorsExitBusOracle + .connect(agentSigner) + .grantRole(await validatorsExitBusOracle.RESUME_ROLE(), vebReportSubmitter.address); + + await validatorsExitBusOracle.connect(vebReportSubmitter).resume(); + } + + await stakingRouter + .connect(agentSigner) + .grantRole(await stakingRouter.REPORT_EXITED_VALIDATORS_STATUS_ROLE(), validatorExitDelayVerifier.address); + + // Ensure that the validatorExitDelayVerifier contract and provided proof use same GI + expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal( + ACTIVE_VALIDATOR_PROOF.firstValidatorGI, + ); + + // Ensure that nor is a first module in staking router + expect((await stakingRouter.getStakingModule(moduleId)).stakingModuleAddress).to.equal(nor.address); + }); + + beforeEach(async () => (beforeEachSnapshot = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(beforeEachSnapshot)); + + it("Should report validator exit delay", async () => { + const { nor, validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + const nodeOpId = 2; + const exitRequests = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const currentBlockTimestamp = await getCurrentBlockTimestamp(); + const proofSlotTimestamp = + (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); + + // Set the block timestamp to 7 days before the proof time + await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); + + await validatorsExitBusOracle.connect(vebReportSubmitter).submitReportHash(encodedExitRequestsHash); + await validatorsExitBusOracle.emitExitEvents(encodedExitRequests); + + const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory.history[0].timestamp; + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ) + .and.to.emit(nor, "ValidatorExitStatusUpdated") + .withArgs(nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, eligibleToExitInSec, proofSlotTimestamp); + }); + + it("Should report validator exit delay historically", async () => { + const { nor, validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + const nodeOpId = 2; + const exitRequests = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const currentBlockTimestamp = await getCurrentBlockTimestamp(); + const proofSlotTimestamp = + (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); + + // Set the block timestamp to 7 days before the proof time + await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); + + await validatorsExitBusOracle.connect(vebReportSubmitter).submitReportHash(encodedExitRequestsHash); + await validatorsExitBusOracle.emitExitEvents(encodedExitRequests); + + const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory.history[0].timestamp; + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ) + .and.to.emit(nor, "ValidatorExitStatusUpdated") + .withArgs(nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, eligibleToExitInSec, proofSlotTimestamp); + }); +}); From f0ba2f0a3a29132c175c0dd427ff2507a2e3e08b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 19 May 2025 11:42:53 +0200 Subject: [PATCH 137/405] refactor: remove consolidation --- lib/eips/eip7251.ts | 34 ---------------------------------- lib/eips/index.ts | 1 - 2 files changed, 35 deletions(-) delete mode 100644 lib/eips/eip7251.ts diff --git a/lib/eips/eip7251.ts b/lib/eips/eip7251.ts deleted file mode 100644 index 8a98437c49..0000000000 --- a/lib/eips/eip7251.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ethers } from "hardhat"; - -import { EIP7251ConsolidationRequest__Mock } from "typechain-types"; - -import { log } from "lib"; - -// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7251.md#configuration -export const EIP7251_ADDRESS = "0x0000BBdDc7CE488642fb579F8B00f3a590007251"; -export const EIP7251_MIN_WITHDRAWAL_REQUEST_FEE = 1n; - -export const deployEIP7251ConsolidationRequestContract = async ( - fee: bigint, -): Promise => { - const eip7251Mock = await ethers.deployContract("EIP7251ConsolidationRequest__Mock"); - const eip7251MockAddress = await eip7251Mock.getAddress(); - - await ethers.provider.send("hardhat_setCode", [EIP7251_ADDRESS, await ethers.provider.getCode(eip7251MockAddress)]); - - const contract = await ethers.getContractAt("EIP7251ConsolidationRequest__Mock", EIP7251_ADDRESS); - await contract.mock__setFee(fee); - - return contract; -}; - -export const ensureEIP7251ConsolidationRequestContractPresent = async (): Promise => { - const code = await ethers.provider.getCode(EIP7251_ADDRESS); - - if (code === "0x") { - log.warning(`EIP7251 consolidation request contract not found at ${EIP7251_ADDRESS}`); - - await deployEIP7251ConsolidationRequestContract(EIP7251_MIN_WITHDRAWAL_REQUEST_FEE); - log.success("EIP7251 consolidation request contract is present"); - } -}; diff --git a/lib/eips/index.ts b/lib/eips/index.ts index b5e8780295..93662f8400 100644 --- a/lib/eips/index.ts +++ b/lib/eips/index.ts @@ -1,4 +1,3 @@ export * from "./eip712"; export * from "./eip4788"; export * from "./eip7002"; -export * from "./eip7251"; From b02d3690887a2ff96d934d6974e012892724ecb0 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 19 May 2025 12:42:07 +0200 Subject: [PATCH 138/405] refactor: update contract methods names in tests --- test/integration/report-validator-exit-delay.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index f90609d3c5..8ef9f56290 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -82,8 +82,8 @@ describe("Report Validator Exit Delay", () => { // Set the block timestamp to 7 days before the proof time await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); - await validatorsExitBusOracle.connect(vebReportSubmitter).submitReportHash(encodedExitRequestsHash); - await validatorsExitBusOracle.emitExitEvents(encodedExitRequests); + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory.history[0].timestamp; @@ -123,8 +123,8 @@ describe("Report Validator Exit Delay", () => { // Set the block timestamp to 7 days before the proof time await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); - await validatorsExitBusOracle.connect(vebReportSubmitter).submitReportHash(encodedExitRequestsHash); - await validatorsExitBusOracle.emitExitEvents(encodedExitRequests); + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory.history[0].timestamp; From 87f2d5fdeab625172c6b9cc784acd08fb7049b14 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 19 May 2025 15:46:50 +0400 Subject: [PATCH 139/405] fix: exclude exitType & renaming --- .../0.8.9/interfaces/IValidatorsExitBus.sol | 9 +-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 74 +++++++++---------- .../contracts/ValidatorsExitBus__Harness.sol | 4 +- ...dator-exit-bus-oracle.triggerExits.test.ts | 39 +++------- .../oracle/validator-exit-bus.helpers.test.ts | 10 +-- 5 files changed, 58 insertions(+), 78 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol index 149aa85f6d..9413aa7950 100644 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -10,7 +10,7 @@ interface IValidatorsExitBus { struct DeliveryHistory { // index in array of requests - uint256 lastDeliveredKeyIndex; + uint256 lastDeliveredExitDataIndex; uint256 timestamp; } @@ -19,10 +19,9 @@ interface IValidatorsExitBus { function submitExitRequestsData(ExitRequestData calldata request) external; function triggerExits( - ExitRequestData calldata request, - uint256[] calldata keyIndexes, - address refundRecipient, - uint8 exitType + ExitRequestData calldata exitsData, + uint256[] calldata exitDataIndexes, + address refundRecipient ) external payable; function setExitRequestLimit(uint256 maxExitRequests, uint256 exitsPerFrame, uint256 frameDuration) external; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 2caead1e5f..7f9997bbc9 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -82,28 +82,29 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @notice Throw when in submitExitRequestsData all requests were already delivered */ error RequestsAlreadyDelivered(); + /** - * @notice Thrown when triggerable withdrawal was requested for validator that was not delivered yet + * @notice Thrown when any of the provided `exitDataIndexes` refers to a validator that was not yet delivered (i.e., exit request not emitted) */ - error KeyWasNotDelivered(uint256 keyIndex, uint256 lastDeliveredKeyIndex); + error ExitDataWasNotDelivered(uint256 exitDataIndex, uint256 lastDeliveredExitDataIndex); + /** * @notice Thrown when index of request in submitted data for triggerable withdrawal is out of range */ - error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + error ExitDataIndexOutOfRange(uint256 exitDataIndex, uint256 totalItemsCount); + /** * @notice Thrown when array of indexes of requests in submitted data for triggerable withdrawal is not is not strictly increasing array */ - error InvalidKeyIndexSortOrder(); - /** - * @notice Thrown when a withdrawal fee refund failed - */ - error TriggerableWithdrawalFeeRefundFailed(); + error InvalidExitDataIndexSortOrder(); + /** * @notice Thrown when remaining exit requests limit is not enough to cover sender requests * @param requestsCount Amount of requests that were sent for processing * @param remainingLimit Amount of requests that still can be processed at current day */ error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + /** * @notice Thrown when the number of requests submitted via submitExitRequestsData exceeds the allowed maxRequestsPerBatch. * @param requestsCount The number of requests included in the current call. @@ -150,7 +151,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 contractVersion; DeliveryHistory[] deliverHistory; } - struct ValidatorData { uint256 nodeOpId; uint256 moduleId; @@ -296,22 +296,20 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /** * @notice Submits Triggerable Withdrawal Requests to the Triggerable Withdrawals Gateway. * - * @param requestsData The report data previously unpacked and emitted by the VEB. - * @param keyIndexes Array of indexes pointing to validators in `requestsData.data` - * to be exited via TWR. + * @param exitsData The report data previously unpacked and emitted by the VEB. + * @param exitDataIndexes Array of of sorted indexes pointing to validators in `exitsData.data` + * to be exited via TWR. * @param refundRecipient Address to return extra fee on TW (eip-7002) exit. - * @param exitType Type of request. 0 - non-refundable, 1 - require refund. * * @dev Reverts if: - * - The hash of `requestsData` was not previously submitted in the VEB. - * - Any of the provided `keyIndexes` refers to a validator that was not yet unpacked (i.e., exit requiest not emitted). - * - `keyIndexes` is not strictly increasing array + * - The hash of `exitsData` was not previously submitted in the VEB. + * - Any of the provided `exitDataIndexes` refers to a validator that was not yet delivered (i.e., exit request not emitted). + * - `exitDataIndexes` is not strictly increasing array */ function triggerExits( - ExitRequestData calldata requestsData, - uint256[] calldata keyIndexes, - address refundRecipient, - uint8 exitType + ExitRequestData calldata exitsData, + uint256[] calldata exitDataIndexes, + address refundRecipient ) external payable { if (msg.value == 0) revert ZeroArgument("msg.value"); @@ -321,33 +319,33 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } RequestStatus storage requestStatus = _storageExitRequestsHashes()[ - keccak256(abi.encode(requestsData.data, requestsData.dataFormat)) + keccak256(abi.encode(exitsData.data, exitsData.dataFormat)) ]; _checkExitSubmitted(requestStatus); - _checkExitRequestData(requestsData.data, requestsData.dataFormat); + _checkExitRequestData(exitsData.data, exitsData.dataFormat); _checkContractVersion(requestStatus.contractVersion); ITriggerableWithdrawalsGateway.ValidatorData[] - memory triggerableExitData = new ITriggerableWithdrawalsGateway.ValidatorData[](keyIndexes.length); + memory triggerableExitData = new ITriggerableWithdrawalsGateway.ValidatorData[](exitDataIndexes.length); - uint256 lastKeyIndex = type(uint256).max; + uint256 lastExitDataIndex = type(uint256).max; - for (uint256 i = 0; i < keyIndexes.length; i++) { - if (keyIndexes[i] >= requestStatus.totalItemsCount) { - revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount); + for (uint256 i = 0; i < exitDataIndexes.length; i++) { + if (exitDataIndexes[i] >= requestStatus.totalItemsCount) { + revert ExitDataIndexOutOfRange(exitDataIndexes[i], requestStatus.totalItemsCount); } - if (keyIndexes[i] > (requestStatus.deliveredItemsCount - 1)) { - revert KeyWasNotDelivered(keyIndexes[i], requestStatus.deliveredItemsCount - 1); + if (exitDataIndexes[i] > (requestStatus.deliveredItemsCount - 1)) { + revert ExitDataWasNotDelivered(exitDataIndexes[i], requestStatus.deliveredItemsCount - 1); } - if (i > 0 && keyIndexes[i] <= lastKeyIndex) { - revert InvalidKeyIndexSortOrder(); + if (i > 0 && exitDataIndexes[i] <= lastExitDataIndex) { + revert InvalidExitDataIndexSortOrder(); } - lastKeyIndex = keyIndexes[i]; + lastExitDataIndex = exitDataIndexes[i]; - ValidatorData memory validatorData = _getValidatorData(requestsData.data, keyIndexes[i]); + ValidatorData memory validatorData = _getValidatorData(exitsData.data, exitDataIndexes[i]); if (validatorData.moduleId == 0) revert InvalidRequestsData(); triggerableExitData[i] = ITriggerableWithdrawalsGateway.ValidatorData( @@ -357,11 +355,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } - ITriggerableWithdrawalsGateway(LOCATOR.triggerableWithdrawalsGateway()).triggerFullWithdrawals{value: msg.value}( - triggerableExitData, - refundRecipient, - exitType - ); + ITriggerableWithdrawalsGateway(LOCATOR.triggerableWithdrawalsGateway()).triggerFullWithdrawals{ + value: msg.value + }(triggerableExitData, refundRecipient, 1); } /** @@ -440,7 +436,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkExitRequestData(exitRequests, dataFormat); if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { - revert KeyIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); + revert ExitDataIndexOutOfRange(index, exitRequests.length / PACKED_REQUEST_LENGTH); } ValidatorData memory validatorData = _getValidatorData(exitRequests, index); diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index e435b02e8f..e10fb89fb5 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -47,14 +47,14 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { uint256 totalItemsCount, uint256 deliveredItemsCount, uint256 contractVersion, - uint256 lastDeliveredKeyIndex + uint256 lastDeliveredExitDataIndex ) external { _storeExitRequestHash( exitRequestHash, totalItemsCount, deliveredItemsCount, contractVersion, - DeliveryHistory(lastDeliveredKeyIndex, block.timestamp) + DeliveryHistory(lastDeliveredExitDataIndex, block.timestamp) ); } } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 72179df875..0bad8f117c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -166,7 +166,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { { data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 2, 3], ZERO_ADDRESS, - 0, { value: 4 }, ); @@ -174,7 +173,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await expect(tx) .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests.length, admin.address, 0); + .withArgs(requests.length, admin.address, 1); }); it("should triggers exits only for validators in selected request indexes", async () => { @@ -182,7 +181,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { { data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 3], ZERO_ADDRESS, - 0, { value: 10, }, @@ -192,7 +190,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await expect(tx) .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests.length, admin.address, 0); + .withArgs(requests.length, admin.address, 1); }); it("should revert with error if the hash of `requestsData` was not previously submitted in the VEB", async () => { @@ -204,7 +202,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }, [0], ZERO_ADDRESS, - 0, { value: 2 }, ), ).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); @@ -212,39 +209,27 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { it("should revert with error if requested index out of range", async () => { await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], ZERO_ADDRESS, 0, { + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], ZERO_ADDRESS, { value: 2, }), ) - .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") + .to.be.revertedWithCustomError(oracle, "ExitDataIndexOutOfRange") .withArgs(5, 4); }); it("should revert with an error if the key index array contains duplicates", async () => { await expect( - oracle.triggerExits( - { data: reportFields.data, dataFormat: reportFields.dataFormat }, - [1, 2, 2], - ZERO_ADDRESS, - 0, - { - value: 2, - }, - ), - ).to.be.revertedWithCustomError(oracle, "InvalidKeyIndexSortOrder"); + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { + value: 2, + }), + ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); }); it("should revert with an error if the key index array is not strictly increasing", async () => { await expect( - oracle.triggerExits( - { data: reportFields.data, dataFormat: reportFields.dataFormat }, - [1, 2, 2], - ZERO_ADDRESS, - 0, - { - value: 2, - }, - ), - ).to.be.revertedWithCustomError(oracle, "InvalidKeyIndexSortOrder"); + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { + value: 2, + }), + ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 42d00fe966..9ca0bd945e 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -107,14 +107,14 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { ); }); - it("reverts if the index is out of range (KeyIndexOutOfRange)", async () => { + it("reverts if the index is out of range (ExitDataIndexOutOfRange)", async () => { // We have only 1 request => 64 bytes const exitRequests = [{ moduleId: 1, nodeOpId: 1, valIndex: 1, valPubkey: PUBKEYS[0] }]; const data = encodeExitRequestsDataList(exitRequests); // There is exactly 1 request, so index=1 is out of range (should be 0) await expect(oracle.unpackExitRequest(data, DATA_FORMAT_LIST, 1)) - .to.be.revertedWithCustomError(oracle, "KeyIndexOutOfRange") + .to.be.revertedWithCustomError(oracle, "ExitDataIndexOutOfRange") .withArgs(1, 1); // index=1, total=1 }); }); @@ -142,7 +142,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { const totalItemsCount = 5; const deliveredItemsCount = 2; const contractVersion = 42; - const lastDeliveredKeyIndex = 1; + const lastDeliveredExitDataIndex = 1; // Call the helper to store the hash await oracle.storeExitRequestHash( @@ -150,7 +150,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { totalItemsCount, deliveredItemsCount, contractVersion, - lastDeliveredKeyIndex, + lastDeliveredExitDataIndex, ); const [returnedTotalItemsCount, returnedDeliveredItemsCount, returnedHistory] = @@ -160,7 +160,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { expect(returnedDeliveredItemsCount).to.equal(deliveredItemsCount); expect(returnedHistory.length).to.equal(1); const [firstDelivery] = returnedHistory; - expect(firstDelivery.lastDeliveredKeyIndex).to.equal(lastDeliveredKeyIndex); + expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); }); }); }); From 02245b4862a7575101c5cb13b1668dcdf75cc3fe Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 19 May 2025 17:08:05 +0200 Subject: [PATCH 140/405] feat: remove consolidation support from withdrawal vault --- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 77 ++-------------------- 1 file changed, 5 insertions(+), 72 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index 02225b900f..10f886d604 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -9,22 +9,18 @@ import {PausableUntil} from "./utils/PausableUntil.sol"; /** * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests - * @dev This contract enables validators to submit EIP-7002 withdrawal requests - * and manages the associated fees. + * @dev This contract enables validators to submit EIP-7002 withdrawal requests. */ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUntil { - address constant CONSOLIDATION_REQUEST = 0x0000BBdDc7CE488642fb579F8B00f3a590007251; address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); bytes32 public constant ADD_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_WITHDRAWAL_REQUEST_ROLE"); - bytes32 public constant ADD_CONSOLIDATION_REQUEST_ROLE = keccak256("ADD_CONSOLIDATION_REQUEST_ROLE"); uint256 internal constant PUBLIC_KEY_LENGTH = 48; event WithdrawalRequestAdded(bytes request); - event ConsolidationRequestAdded(bytes request); error ZeroArgument(string name); error MalformedPubkeysArray(); @@ -103,13 +99,15 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt if (pubkeys.length == 0) revert ZeroArgument("pubkeys"); if (pubkeys.length % PUBLIC_KEY_LENGTH != 0) revert MalformedPubkeysArray(); - uint256 requestsCount = _countPubkeys(pubkeys); + uint256 requestsCount = pubkeys.length / PUBLIC_KEY_LENGTH; if (requestsCount != amounts.length) revert ArraysLengthMismatch(requestsCount, amounts.length); uint256 feePerRequest = _getRequestFee(WITHDRAWAL_REQUEST); uint256 totalFee = requestsCount * feePerRequest; - _requireExactFee(totalFee); + if (totalFee != msg.value) { + revert IncorrectFee(msg.value, totalFee); + } bytes memory request = new bytes(56); for (uint256 i = 0; i < requestsCount; i++) { @@ -137,61 +135,6 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt return _getRequestFee(WITHDRAWAL_REQUEST); } - /** - * @dev Submits EIP-7251 consolidation requests for the specified public keys. - * - * @param sourcePubkeys A tightly packed array of 48-byte source public keys corresponding to validators requesting consolidation. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @param targetPubkeys A tightly packed array of 48-byte target public keys corresponding to validators requesting consolidation. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @notice Reverts if: - * - The caller does not have the `ADD_CONSOLIDATION_REQUEST_ROLE`. - * - The provided public key array is empty. - * - The provided public key array malformed. - * - The provided source public key and target public key arrays are not of equal length. - * - The provided total withdrawal fee value is invalid. - */ - function addConsolidationRequests( - bytes calldata sourcePubkeys, - bytes calldata targetPubkeys - ) external payable onlyRole(ADD_CONSOLIDATION_REQUEST_ROLE) whenResumed preservesEthBalance { - if (sourcePubkeys.length == 0) revert ZeroArgument("sourcePubkeys"); - if (sourcePubkeys.length % PUBLIC_KEY_LENGTH != 0) revert MalformedPubkeysArray(); - if (sourcePubkeys.length != targetPubkeys.length) - revert ArraysLengthMismatch(sourcePubkeys.length, sourcePubkeys.length); - - uint256 requestsCount = _countPubkeys(sourcePubkeys); - uint256 feePerRequest = _getRequestFee(CONSOLIDATION_REQUEST); - - _requireExactFee(requestsCount * feePerRequest); - - bytes memory request = new bytes(96); - for (uint256 i = 0; i < requestsCount; i++) { - assembly { - calldatacopy(add(request, 32), add(sourcePubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) - calldatacopy(add(request, 80), add(targetPubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) - } - - (bool success, ) = CONSOLIDATION_REQUEST.call{value: feePerRequest}(request); - - if (!success) { - revert RequestAdditionFailed(request); - } - - emit ConsolidationRequestAdded(request); - } - } - - /** - * @dev Retrieves the current EIP-7251 consolidation fee. - * @return The minimum fee required per consolidation request. - */ - function getConsolidationRequestFee() external view returns (uint256) { - return _getRequestFee(CONSOLIDATION_REQUEST); - } - function _getRequestFee(address requestedContract) internal view returns (uint256) { (bool success, bytes memory feeData) = requestedContract.staticcall(""); @@ -205,14 +148,4 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt return abi.decode(feeData, (uint256)); } - - function _countPubkeys(bytes calldata pubkeys) internal pure returns (uint256) { - return (pubkeys.length / PUBLIC_KEY_LENGTH); - } - - function _requireExactFee(uint256 requiredFee) internal view { - if (requiredFee != msg.value) { - revert IncorrectFee(msg.value, requiredFee); - } - } } From c212f5edce7ca8abef4f7974c13e8cc76a438972 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 20 May 2025 03:41:45 +0400 Subject: [PATCH 141/405] fix: new way to store delivery history --- .../0.8.9/interfaces/IValidatorsExitBus.sol | 6 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 237 +++++++++++------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 9 +- .../contracts/ValidatorsExitBus__Harness.sol | 15 +- ...dator-exit-bus-oracle.triggerExits.test.ts | 4 +- .../oracle/validator-exit-bus.helpers.test.ts | 24 +- 6 files changed, 180 insertions(+), 115 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol index 9413aa7950..d6b7668e70 100644 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -10,8 +10,8 @@ interface IValidatorsExitBus { struct DeliveryHistory { // index in array of requests - uint256 lastDeliveredExitDataIndex; - uint256 timestamp; + uint32 lastDeliveredExitDataIndex; + uint32 timestamp; } function submitExitRequestsHash(bytes32 exitReportHash) external; @@ -39,7 +39,7 @@ interface IValidatorsExitBus { function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash - ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history); + ) external view returns (DeliveryHistory[] memory history); function unpackExitRequest( bytes calldata exitRequests, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 7f9997bbc9..a3673b76b6 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -112,6 +112,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ error MaxRequestsBatchSizeExceeded(uint256 requestsCount, uint256 maxRequestsPerBatch); + error DeliveredIndexOutOfBounds(); + error DeliveryWasNotStarted(); + /// @dev Events /** @@ -142,21 +145,18 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); - struct RequestStatus { - // Total items count in report (by default type(uint256).max, update on first report delivery) - uint256 totalItemsCount; - // Total processed items in report (by default 0) - uint256 deliveredItemsCount; - // Vebo contract version at the time of hash submission - uint256 contractVersion; - DeliveryHistory[] deliverHistory; - } struct ValidatorData { uint256 nodeOpId; uint256 moduleId; uint256 valIndex; bytes pubkey; } + struct PackedRequestStatus { + uint8 contractVersion; + uint32 deliveryHistoryLength; + uint32 lastDeliveredExitDataIndex; // index of validator, maximum 600 for example + uint32 lastDeliveredExitDataTimestamp; + } /// @notice An ACL role granting the permission to submit a hash of the exit requests data bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); @@ -176,8 +176,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 internal constant PACKED_TWG_EXIT_REQUEST_LENGTH = 56; - ILidoLocator internal immutable LOCATOR; - /// @notice The list format of the validator exit requests data. Used when all /// requests fit into a single transaction. /// @@ -196,11 +194,15 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// uint256 public constant DATA_FORMAT_LIST = 1; + ILidoLocator internal immutable LOCATOR; + /// Hash constant for mapping exit requests storage bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); bytes32 internal constant MAX_VALIDATORS_PER_BATCH_POSITION = keccak256("lido.ValidatorsExitBus.maxValidatorsPerBatch"); + bytes32 internal constant PACKED_REQUEST_STATUS_POSITION = keccak256("lido.ValidatorsExitBus.packedRequestStatus"); + bytes32 internal constant DELIVERY_HISTORY_POSITION = keccak256("lido.ValidatorsBus.deliveryHistory"); constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); @@ -220,9 +222,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); - _storeExitRequestHash(exitRequestsHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); + // _storeExitRequestHash(exitRequestsHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); - emit RequestsHashSubmitted(exitRequestsHash); + _storePackedRequestStatus( + exitRequestsHash, + PackedRequestStatus(uint8(contractVersion), 0, type(uint32).max, type(uint32).max) + ); } /** @@ -241,56 +246,50 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param request - The exit requests structure. */ function submitExitRequestsData(ExitRequestData calldata request) external whenResumed { - bytes calldata data = request.data; - - RequestStatus storage requestStatus = _storageExitRequestsHashes()[ - keccak256(abi.encode(data, request.dataFormat)) - ]; + // bytes calldata data = request.data; + bytes32 exitRequestsHash = keccak256(abi.encode(request.data, request.dataFormat)); + PackedRequestStatus storage requestStatus = _storagePackedRequestStatus()[exitRequestsHash]; _checkExitSubmitted(requestStatus); _checkExitRequestData(request.data, request.dataFormat); _checkContractVersion(requestStatus.contractVersion); - // By default, totalItemsCount is set to type(uint256).max. - // If an exit is emitted for the request for the first time, the default value is used for totalItemsCount. - if (requestStatus.totalItemsCount == type(uint256).max) { - requestStatus.totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; - } + uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; + uint32 lastDeliveredIndex = requestStatus.lastDeliveredExitDataIndex; - _checkRequestsBatchSize(requestStatus.totalItemsCount); + // maybe this check is extra + if (requestStatus.deliveryHistoryLength != 0 && lastDeliveredIndex >= totalItemsCount) { + revert DeliveredIndexOutOfBounds(); + } - uint256 undeliveredItemsCount = requestStatus.totalItemsCount - requestStatus.deliveredItemsCount; + uint256 startIndex = requestStatus.deliveryHistoryLength == 0 ? 0 : lastDeliveredIndex + 1; + uint256 undeliveredItemsCount = totalItemsCount - startIndex; if (undeliveredItemsCount == 0) { revert RequestsAlreadyDelivered(); } - uint256 requestsToDeliver = undeliveredItemsCount; - ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - if (exitRequestLimitData.isExitLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); + uint256 requestsToDeliver = _consumeLimit(undeliveredItemsCount); - if (limit == 0) { - revert ExitRequestsLimit(undeliveredItemsCount, 0); - } + _processExitRequestsList(request.data, startIndex, requestsToDeliver); + + uint256 newLastDeliveredIndex = startIndex + requestsToDeliver - 1; - requestsToDeliver = limit >= undeliveredItemsCount ? undeliveredItemsCount : limit; - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitLimit(limit - requestsToDeliver, _getTimestamp()) + if (requestStatus.deliveryHistoryLength == 1) { + _storeDeliveryEntry( + exitRequestsHash, + requestStatus.lastDeliveredExitDataIndex, + requestStatus.lastDeliveredExitDataTimestamp ); - } - require( - requestStatus.totalItemsCount >= requestStatus.deliveredItemsCount + requestsToDeliver, - "INDEX_OUT_OF_RANGE" - ); + _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); + } - _processExitRequestsList(request.data, requestStatus.deliveredItemsCount, requestsToDeliver); + if (requestStatus.deliveryHistoryLength > 1) { + _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); + } - requestStatus.deliverHistory.push( - DeliveryHistory(requestStatus.deliveredItemsCount + requestsToDeliver - 1, _getTimestamp()) - ); - requestStatus.deliveredItemsCount += requestsToDeliver; + _updatePackedRequestStatus(requestStatus, requestStatus.deliveryHistoryLength + 1, newLastDeliveredIndex); } /** @@ -318,11 +317,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa refundRecipient = msg.sender; } - RequestStatus storage requestStatus = _storageExitRequestsHashes()[ + PackedRequestStatus storage requestStatus = _storagePackedRequestStatus()[ keccak256(abi.encode(exitsData.data, exitsData.dataFormat)) ]; _checkExitSubmitted(requestStatus); + _checkDeliveryStarted(requestStatus); _checkExitRequestData(exitsData.data, exitsData.dataFormat); _checkContractVersion(requestStatus.contractVersion); @@ -332,17 +332,14 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 lastExitDataIndex = type(uint256).max; for (uint256 i = 0; i < exitDataIndexes.length; i++) { - if (exitDataIndexes[i] >= requestStatus.totalItemsCount) { - revert ExitDataIndexOutOfRange(exitDataIndexes[i], requestStatus.totalItemsCount); - } - - if (exitDataIndexes[i] > (requestStatus.deliveredItemsCount - 1)) { - revert ExitDataWasNotDelivered(exitDataIndexes[i], requestStatus.deliveredItemsCount - 1); + if (exitDataIndexes[i] > requestStatus.lastDeliveredExitDataIndex) { + revert ExitDataWasNotDelivered(exitDataIndexes[i], requestStatus.lastDeliveredExitDataIndex); } if (i > 0 && exitDataIndexes[i] <= lastExitDataIndex) { revert InvalidExitDataIndexSortOrder(); } + lastExitDataIndex = exitDataIndexes[i]; ValidatorData memory validatorData = _getValidatorData(exitsData.data, exitDataIndexes[i]); @@ -404,18 +401,35 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /** * @notice Returns unpacking history and current status for specific exitRequestsData * - * @dev Reverts if such exitRequestsHash was not submited. - * * @param exitRequestsHash - The exit requests hash. + * + * @dev Reverts if: + * - exitRequestsHash was not submited + * - No delivery history */ - function getExitRequestsDeliveryHistory( - bytes32 exitRequestsHash - ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { - RequestStatus storage requestStatus = _storageExitRequestsHashes()[exitRequestsHash]; + function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (DeliveryHistory[] memory) { + mapping(bytes32 => PackedRequestStatus) storage packedRequestStatusMap = _storagePackedRequestStatus(); + PackedRequestStatus storage storedRequest = packedRequestStatusMap[exitRequestsHash]; - _checkExitSubmitted(requestStatus); + if (storedRequest.contractVersion == 0) { + revert ExitHashNotSubmitted(); + } + + if (storedRequest.deliveryHistoryLength == 0) { + revert DeliveryWasNotStarted(); + } + + if (storedRequest.deliveryHistoryLength == 1) { + DeliveryHistory[] memory deliveryHistory = new DeliveryHistory[](1); + deliveryHistory[0] = DeliveryHistory( + storedRequest.lastDeliveredExitDataIndex, + storedRequest.lastDeliveredExitDataTimestamp + ); + return deliveryHistory; + } - return (requestStatus.totalItemsCount, requestStatus.deliveredItemsCount, requestStatus.deliverHistory); + mapping(bytes32 => DeliveryHistory[]) storage deliveryHistoryMap = _storageDeliveryHistory(); + return deliveryHistoryMap[exitRequestsHash]; } /** @@ -432,7 +446,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes calldata exitRequests, uint256 dataFormat, uint256 index - ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { + ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { _checkExitRequestData(exitRequests, dataFormat); if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { @@ -488,35 +502,41 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Internal functions - function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal pure { + function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal view { if (dataFormat != DATA_FORMAT_LIST) { revert UnsupportedRequestsDataFormat(dataFormat); } - if (requests.length % PACKED_REQUEST_LENGTH != 0) { + if (requests.length == 0 || requests.length % PACKED_REQUEST_LENGTH != 0) { revert InvalidRequestsDataLength(); } - } - function _checkRequestsBatchSize(uint256 requestsCount) internal view { uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); + uint256 requestsCount = requests.length / PACKED_REQUEST_LENGTH; + if (requestsCount > maxRequestsPerBatch) { revert MaxRequestsBatchSizeExceeded(requestsCount, maxRequestsPerBatch); } } - function _checkExitSubmitted(RequestStatus storage requestStatus) internal view { + function _checkExitSubmitted(PackedRequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion == 0) { revert ExitHashNotSubmitted(); } } - function _checkExitNotSubmitted(RequestStatus storage requestStatus) internal view { + function _checkExitNotSubmitted(PackedRequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion != 0) { revert ExitHashAlreadySubmitted(); } } + function _checkDeliveryStarted(PackedRequestStatus storage status) internal view { + if (status.deliveryHistoryLength == 0) { + revert DeliveryWasNotStarted(); + } + } + function _getCurrentExitLimit() internal view returns (uint256) { ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (!exitRequestLimitData.isExitLimitSet()) { @@ -555,24 +575,65 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); } - function _storeExitRequestHash( - bytes32 exitRequestHash, - uint256 totalItemsCount, - uint256 deliveredItemsCount, - uint256 contractVersion, - DeliveryHistory memory history - ) internal { - mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes(); - RequestStatus storage request = hashes[exitRequestHash]; + function _consumeLimit(uint256 requestsCount) internal returns (uint256 requestsToDeliver) { + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); + if (!exitRequestLimitData.isExitLimitSet()) { + return requestsCount; + } - _checkExitNotSubmitted(request); + uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); - request.totalItemsCount = totalItemsCount; - request.deliveredItemsCount = deliveredItemsCount; - request.contractVersion = contractVersion; - if (history.timestamp != 0) { - request.deliverHistory.push(history); + if (limit == 0) { + revert ExitRequestsLimit(requestsCount, 0); + } + + requestsToDeliver = limit >= requestsCount ? requestsCount : limit; + EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( + exitRequestLimitData.updatePrevExitLimit(limit - requestsToDeliver, _getTimestamp()) + ); + } + + function _storePackedRequestStatus(bytes32 exitRequestsHash, PackedRequestStatus memory requestStatus) internal { + mapping(bytes32 => PackedRequestStatus) storage packedRequestStatusMap = _storagePackedRequestStatus(); + PackedRequestStatus storage storedRequest = packedRequestStatusMap[exitRequestsHash]; + + if (storedRequest.contractVersion != 0) { + revert ExitHashAlreadySubmitted(); } + + storedRequest.contractVersion = requestStatus.contractVersion; + storedRequest.deliveryHistoryLength = requestStatus.deliveryHistoryLength; + storedRequest.lastDeliveredExitDataIndex = requestStatus.lastDeliveredExitDataIndex; + storedRequest.lastDeliveredExitDataTimestamp = requestStatus.lastDeliveredExitDataTimestamp; + + emit RequestsHashSubmitted(exitRequestsHash); + } + + function _updatePackedRequestStatus( + PackedRequestStatus storage requestStatus, + uint256 deliveryHistoryLength, + uint256 lastDeliveredExitDataIndex + ) internal { + require(deliveryHistoryLength <= type(uint32).max, "DELIVERY_HISTORY_LENGTH_OVERFLOW"); + require(lastDeliveredExitDataIndex <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW"); + requestStatus.deliveryHistoryLength = uint32(deliveryHistoryLength); + requestStatus.lastDeliveredExitDataIndex = uint32(lastDeliveredExitDataIndex); + requestStatus.lastDeliveredExitDataTimestamp = uint32(_getTimestamp()); + } + + function _storeDeliveryEntry( + bytes32 exitRequestsHash, + uint256 lastDeliveredExitDataIndex, + uint256 lastDeliveredExitDataTimestamp + ) internal { + require(lastDeliveredExitDataIndex <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW"); + require(lastDeliveredExitDataTimestamp <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); + + mapping(bytes32 => DeliveryHistory[]) storage deliveryHistoryMap = _storageDeliveryHistory(); + DeliveryHistory[] storage deliveryHistory = deliveryHistoryMap[exitRequestsHash]; + deliveryHistory.push( + DeliveryHistory(uint32(lastDeliveredExitDataIndex), uint32(lastDeliveredExitDataTimestamp)) + ); } /// Methods for reading data from tightly packed validator exit requests @@ -671,8 +732,16 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } /// Storage helpers - function _storageExitRequestsHashes() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { - bytes32 position = EXIT_REQUESTS_HASHES_POSITION; + + function _storagePackedRequestStatus() internal pure returns (mapping(bytes32 => PackedRequestStatus) storage r) { + bytes32 position = PACKED_REQUEST_STATUS_POSITION; + assembly { + r.slot := position + } + } + + function _storageDeliveryHistory() internal pure returns (mapping(bytes32 => DeliveryHistory[]) storage r) { + bytes32 position = DELIVERY_HISTORY_POSITION; assembly { r.slot := position } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 96beca1ba7..f6088d8083 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -306,14 +306,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storeExitRequestHash( - exitRequestsHash, - requestsCount, - requestsCount, - contractVersion, - DeliveryHistory(requestsCount - 1, _getTimestamp()) - ); - emit RequestsHashSubmitted(exitRequestsHash); + _storePackedRequestStatus(exitRequestsHash, PackedRequestStatus(uint8(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); } /// diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index e10fb89fb5..8b3dc78bae 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -44,17 +44,14 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { function storeExitRequestHash( bytes32 exitRequestHash, - uint256 totalItemsCount, - uint256 deliveredItemsCount, - uint256 contractVersion, - uint256 lastDeliveredExitDataIndex + uint8 contractVersion, + uint32 deliveryHistoryLength, + uint32 lastDeliveredExitDataIndex, + uint32 timestamp ) external { - _storeExitRequestHash( + _storePackedRequestStatus( exitRequestHash, - totalItemsCount, - deliveredItemsCount, - contractVersion, - DeliveryHistory(lastDeliveredExitDataIndex, block.timestamp) + PackedRequestStatus(contractVersion, deliveryHistoryLength, lastDeliveredExitDataIndex, timestamp) ); } } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 0bad8f117c..615810db8e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -213,8 +213,8 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { value: 2, }), ) - .to.be.revertedWithCustomError(oracle, "ExitDataIndexOutOfRange") - .withArgs(5, 4); + .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") // TODO: fix in code return "ExitDataIndexOutOfRange") + .withArgs(5, 3); // 4 }); it("should revert with an error if the key index array contains duplicates", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 9ca0bd945e..908b521c65 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -8,7 +8,7 @@ import { ValidatorsExitBus__Harness } from "typechain-types"; import { de0x, numberToHex } from "lib"; -import { deployVEBO } from "test/deploy"; +import { deployVEBO, initVEBO } from "test/deploy"; import { Snapshot } from "test/suite"; const PUBKEYS = [ @@ -45,6 +45,15 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { const deploy = async () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; + const consensus = deployed.consensus; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + resumeAfterDeploy: true, + lastProcessingRefSlot: 1, //LAST_PROCESSING_REF_SLOT, + }); }; before(async () => { @@ -139,25 +148,22 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { it("returns correct data for a previously stored exitRequestsHash", async () => { const exitRequestsHash = keccak256("0x1111"); - const totalItemsCount = 5; - const deliveredItemsCount = 2; + const deliveryHistoryLength = 1; + const timestamp = await oracle.getTime(); const contractVersion = 42; const lastDeliveredExitDataIndex = 1; // Call the helper to store the hash await oracle.storeExitRequestHash( exitRequestsHash, - totalItemsCount, - deliveredItemsCount, contractVersion, + deliveryHistoryLength, lastDeliveredExitDataIndex, + timestamp, ); - const [returnedTotalItemsCount, returnedDeliveredItemsCount, returnedHistory] = - await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + const returnedHistory = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(returnedTotalItemsCount).to.equal(totalItemsCount); - expect(returnedDeliveredItemsCount).to.equal(deliveredItemsCount); expect(returnedHistory.length).to.equal(1); const [firstDelivery] = returnedHistory; expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); From 684130b6305b210497bcab5b66c7c7d22dd548b3 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 20 May 2025 12:56:43 +0400 Subject: [PATCH 142/405] fix: PackedRequestStatus -> RequestStatus --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 32 +++++++++------ contracts/0.8.9/oracle/ValidatorsExitBus.sol | 40 +++++++++---------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- .../contracts/ValidatorsExitBus__Harness.sol | 4 +- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 20c4555ad1..58ac8dc0f1 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -154,19 +154,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { uint256 requestsCount = triggerableExitsData.length; - ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); - if (twrLimitData.isExitLimitSet()) { - uint256 timestamp = _getTimestamp(); - uint256 limit = twrLimitData.calculateCurrentExitLimit(timestamp); - - if (limit < requestsCount) { - revert ExitRequestsLimit(requestsCount, limit); - } - - TWR_LIMIT_POSITION.setStorageExitRequestLimit( - twrLimitData.updatePrevExitLimit(limit - requestsCount, timestamp) - ); - } + _checkExitRequestLimit(requestsCount); uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); _checkFee(requestsCount, withdrawalFee); @@ -315,4 +303,22 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); } + + function _checkExitRequestLimit(uint256 requestsCount) internal { + ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); + if (!twrLimitData.isExitLimitSet()) { + return; + } + + uint256 timestamp = _getTimestamp(); + uint256 limit = twrLimitData.calculateCurrentExitLimit(timestamp); + + if (limit < requestsCount) { + revert ExitRequestsLimit(requestsCount, limit); + } + + TWR_LIMIT_POSITION.setStorageExitRequestLimit( + twrLimitData.updatePrevExitLimit(limit - requestsCount, timestamp) + ); + } } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index a3673b76b6..6bbd752e7e 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -151,7 +151,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 valIndex; bytes pubkey; } - struct PackedRequestStatus { + struct RequestStatus { uint8 contractVersion; uint32 deliveryHistoryLength; uint32 lastDeliveredExitDataIndex; // index of validator, maximum 600 for example @@ -201,7 +201,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); bytes32 internal constant MAX_VALIDATORS_PER_BATCH_POSITION = keccak256("lido.ValidatorsExitBus.maxValidatorsPerBatch"); - bytes32 internal constant PACKED_REQUEST_STATUS_POSITION = keccak256("lido.ValidatorsExitBus.packedRequestStatus"); + bytes32 internal constant REQUEST_STATUS_POSITION = keccak256("lido.ValidatorsExitBus.requestStatus"); bytes32 internal constant DELIVERY_HISTORY_POSITION = keccak256("lido.ValidatorsBus.deliveryHistory"); constructor(address lidoLocator) { @@ -222,11 +222,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); - // _storeExitRequestHash(exitRequestsHash, type(uint256).max, 0, contractVersion, DeliveryHistory(0, 0)); - - _storePackedRequestStatus( + _storeRequestStatus( exitRequestsHash, - PackedRequestStatus(uint8(contractVersion), 0, type(uint32).max, type(uint32).max) + RequestStatus(uint8(contractVersion), 0, type(uint32).max, type(uint32).max) ); } @@ -248,7 +246,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa function submitExitRequestsData(ExitRequestData calldata request) external whenResumed { // bytes calldata data = request.data; bytes32 exitRequestsHash = keccak256(abi.encode(request.data, request.dataFormat)); - PackedRequestStatus storage requestStatus = _storagePackedRequestStatus()[exitRequestsHash]; + RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestsHash]; _checkExitSubmitted(requestStatus); _checkExitRequestData(request.data, request.dataFormat); @@ -289,7 +287,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); } - _updatePackedRequestStatus(requestStatus, requestStatus.deliveryHistoryLength + 1, newLastDeliveredIndex); + _updateRequestStatus(requestStatus, requestStatus.deliveryHistoryLength + 1, newLastDeliveredIndex); } /** @@ -317,7 +315,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa refundRecipient = msg.sender; } - PackedRequestStatus storage requestStatus = _storagePackedRequestStatus()[ + RequestStatus storage requestStatus = _storageRequestStatus()[ keccak256(abi.encode(exitsData.data, exitsData.dataFormat)) ]; @@ -408,8 +406,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * - No delivery history */ function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (DeliveryHistory[] memory) { - mapping(bytes32 => PackedRequestStatus) storage packedRequestStatusMap = _storagePackedRequestStatus(); - PackedRequestStatus storage storedRequest = packedRequestStatusMap[exitRequestsHash]; + mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); + RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; if (storedRequest.contractVersion == 0) { revert ExitHashNotSubmitted(); @@ -519,19 +517,19 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _checkExitSubmitted(PackedRequestStatus storage requestStatus) internal view { + function _checkExitSubmitted(RequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion == 0) { revert ExitHashNotSubmitted(); } } - function _checkExitNotSubmitted(PackedRequestStatus storage requestStatus) internal view { + function _checkExitNotSubmitted(RequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion != 0) { revert ExitHashAlreadySubmitted(); } } - function _checkDeliveryStarted(PackedRequestStatus storage status) internal view { + function _checkDeliveryStarted(RequestStatus storage status) internal view { if (status.deliveryHistoryLength == 0) { revert DeliveryWasNotStarted(); } @@ -593,9 +591,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } - function _storePackedRequestStatus(bytes32 exitRequestsHash, PackedRequestStatus memory requestStatus) internal { - mapping(bytes32 => PackedRequestStatus) storage packedRequestStatusMap = _storagePackedRequestStatus(); - PackedRequestStatus storage storedRequest = packedRequestStatusMap[exitRequestsHash]; + function _storeRequestStatus(bytes32 exitRequestsHash, RequestStatus memory requestStatus) internal { + mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); + RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; if (storedRequest.contractVersion != 0) { revert ExitHashAlreadySubmitted(); @@ -609,8 +607,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa emit RequestsHashSubmitted(exitRequestsHash); } - function _updatePackedRequestStatus( - PackedRequestStatus storage requestStatus, + function _updateRequestStatus( + RequestStatus storage requestStatus, uint256 deliveryHistoryLength, uint256 lastDeliveredExitDataIndex ) internal { @@ -733,8 +731,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Storage helpers - function _storagePackedRequestStatus() internal pure returns (mapping(bytes32 => PackedRequestStatus) storage r) { - bytes32 position = PACKED_REQUEST_STATUS_POSITION; + function _storageRequestStatus() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { + bytes32 position = REQUEST_STATUS_POSITION; assembly { r.slot := position } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index f6088d8083..3af86187fb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -306,7 +306,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storePackedRequestStatus(exitRequestsHash, PackedRequestStatus(uint8(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); + _storeRequestStatus(exitRequestsHash, RequestStatus(uint8(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); } /// diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 8b3dc78bae..cf4a2d6af6 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -49,9 +49,9 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { uint32 lastDeliveredExitDataIndex, uint32 timestamp ) external { - _storePackedRequestStatus( + _storeRequestStatus( exitRequestHash, - PackedRequestStatus(contractVersion, deliveryHistoryLength, lastDeliveredExitDataIndex, timestamp) + RequestStatus(contractVersion, deliveryHistoryLength, lastDeliveredExitDataIndex, timestamp) ); } } From 206392a69c2caf0b537455b36f2f6ddeecc16698 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 20 May 2025 15:39:59 +0200 Subject: [PATCH 143/405] feat: update exit delay verifier after VEB getExitRequestDeliveryHistory method changed --- .../0.8.25/ValidatorExitDelayVerifier.sol | 42 ++++++------------- .../0.8.25/interfaces/IValidatorsExitBus.sol | 2 +- .../ValidatorsExitBusOracle_Mock.sol | 24 ++++------- .../0.8.25/validatorExitDelayVerifier.test.ts | 24 ++--------- .../report-validator-exit-delay.ts | 4 +- 5 files changed, 28 insertions(+), 68 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 31bd64f2f6..6489296b21 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -51,12 +51,6 @@ contract ValidatorExitDelayVerifier { using SSZ for Validator; using SSZ for BeaconBlockHeader; - struct ExitRequestsDeliveryHistory { - uint256 totalItemsCount; - uint256 deliveredItemsCount; - DeliveryHistory[] deliveryHistory; - } - /// @notice EIP-4788 contract address that provides a mapping of timestamp -> known beacon block root. address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; @@ -109,8 +103,7 @@ contract ValidatorExitDelayVerifier { uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); - error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex); - error KeyIndexOutOfRange(uint256 keyIndex, uint256 totalItemsCount); + error KeyWasNotUnpacked(uint256 keyIndex); /** * @dev The previous and current forks can be essentially the same. @@ -177,7 +170,7 @@ contract ValidatorExitDelayVerifier { IValidatorsExitBus vebo = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { @@ -228,7 +221,7 @@ contract ValidatorExitDelayVerifier { IValidatorsExitBus vebo = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - ExitRequestsDeliveryHistory memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { @@ -332,7 +325,7 @@ contract ValidatorExitDelayVerifier { * @return uint256 The elapsed seconds since the earliest eligible exit request time. */ function _getSecondsSinceExitRequestEligible( - ExitRequestsDeliveryHistory memory history, + DeliveryHistory[] memory history, ValidatorWitness calldata witness, uint256 referenceSlotTimestamp ) internal view returns (uint256) { @@ -369,35 +362,24 @@ contract ValidatorExitDelayVerifier { function _getExitRequestDeliveryHistory( IValidatorsExitBus vebo, ExitRequestData calldata exitRequests - ) internal view returns (ExitRequestsDeliveryHistory memory) { + ) internal view returns (DeliveryHistory[] memory) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); - (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) = vebo - .getExitRequestsDeliveryHistory(exitRequestsHash); + DeliveryHistory[] memory history = vebo.getExitRequestsDeliveryHistory(exitRequestsHash); - return ExitRequestsDeliveryHistory(totalItemsCount, deliveredItemsCount, history); + return history; } function _getExitRequestTimestamp( - ExitRequestsDeliveryHistory memory history, + DeliveryHistory[] memory deliveryHistory, uint256 index ) internal pure returns (uint256 validatorExitRequestTimestamp) { - if (index >= history.totalItemsCount) { - revert KeyIndexOutOfRange(index, history.totalItemsCount); - } - - if (index > history.deliveredItemsCount - 1) { - revert KeyWasNotUnpacked(index, history.deliveredItemsCount - 1); - } - - for (uint256 i = 0; i < history.deliveryHistory.length; i++) { - if (history.deliveryHistory[i].lastDeliveredKeyIndex >= index) { - return history.deliveryHistory[i].timestamp; + for (uint256 i = 0; i < deliveryHistory.length; i++) { + if (deliveryHistory[i].lastDeliveredKeyIndex >= index) { + return deliveryHistory[i].timestamp; } } - // As the loop should always end prematurely with the `return` statement, - // this code should be unreachable. We assert `false` just to be safe. - assert(false); + revert KeyWasNotUnpacked(index); } function _slotToTimestamp(uint64 slot) internal view returns (uint256) { diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol index 11809b394d..b9df6196d3 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -12,7 +12,7 @@ struct DeliveryHistory { interface IValidatorsExitBus { function getExitRequestsDeliveryHistory( bytes32 exitRequestsHash - ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history); + ) external view returns (DeliveryHistory[] memory history); function unpackExitRequest( bytes calldata exitRequests, diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index d335d4a830..39c0e67e2d 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -3,43 +3,37 @@ pragma solidity ^0.8.0; import {IValidatorsExitBus, DeliveryHistory} from "contracts/0.8.25/interfaces/IValidatorsExitBus.sol"; -struct MockExitRequestDeliveryHistory { - uint256 totalItemsCount; - uint256 deliveredItemsCount; - DeliveryHistory[] history; -} - struct MockExitRequestData { bytes pubkey; uint256 nodeOpId; uint256 moduleId; uint256 valIndex; } + contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { bytes32 _hash; - MockExitRequestDeliveryHistory private _history; + DeliveryHistory[] private _deliveryHistory; MockExitRequestData[] private _data; function setExitRequests( bytes32 exitRequestsHash, - MockExitRequestDeliveryHistory calldata history, + DeliveryHistory[] calldata deliveryHistory, MockExitRequestData[] calldata data ) external { _hash = exitRequestsHash; - _history = history; + + for (uint256 i = 0; i < deliveryHistory.length; i++) { + _deliveryHistory.push(deliveryHistory[i]); + } for (uint256 i = 0; i < data.length; i++) { _data.push(data[i]); } } - function getExitRequestsDeliveryHistory( - bytes32 exitRequestsHash - ) external view returns (uint256 totalItemsCount, uint256 deliveredItemsCount, DeliveryHistory[] memory history) { + function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (DeliveryHistory[] memory) { require(exitRequestsHash == _hash, "Mock error, Invalid exitRequestsHash"); - totalItemsCount = _history.totalItemsCount; - deliveredItemsCount = _history.deliveredItemsCount; - history = _history.history; + return _deliveryHistory; } function unpackExitRequest( diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index f4114e1c52..201facf18c 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -162,11 +162,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { await vebo.setExitRequests( encodedExitRequestsHash, - { - totalItemsCount: 1n, - deliveredItemsCount: 1n, - history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - }, + [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], exitRequests, ); @@ -212,11 +208,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { await vebo.setExitRequests( encodedExitRequestsHash, - { - totalItemsCount: 1n, - deliveredItemsCount: 1n, - history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - }, + [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], exitRequests, ); @@ -306,11 +298,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { await vebo.setExitRequests( encodedExitRequestsHash, - { - totalItemsCount: 1n, - deliveredItemsCount: 1n, - history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - }, + [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], exitRequests, ); @@ -462,11 +450,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { await vebo.setExitRequests( encodedExitRequestsHash, - { - totalItemsCount: 1n, - deliveredItemsCount: 1n, - history: [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - }, + [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], exitRequests, ); diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 8ef9f56290..6b1c3595d8 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -86,7 +86,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); - const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory.history[0].timestamp; + const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory[0].timestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -127,7 +127,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); - const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory.history[0].timestamp; + const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory[0].timestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); From c9b7ebdcfd02bbab3bf31d49173c1aca56b7af2b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 20 May 2025 17:44:57 +0400 Subject: [PATCH 144/405] fix: uint32 for contractVersion & simplify history storage --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 49 ++++++++--------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- .../contracts/ValidatorsExitBus__Harness.sol | 12 +++- .../oracle/validator-exit-bus.helpers.test.ts | 55 ++++++++++++++++++- 4 files changed, 86 insertions(+), 32 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 6bbd752e7e..d6a301bcaf 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -152,7 +152,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes pubkey; } struct RequestStatus { - uint8 contractVersion; + uint32 contractVersion; uint32 deliveryHistoryLength; uint32 lastDeliveredExitDataIndex; // index of validator, maximum 600 for example uint32 lastDeliveredExitDataTimestamp; @@ -222,9 +222,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); - _storeRequestStatus( + _storeNewHashRequestStatus( exitRequestsHash, - RequestStatus(uint8(contractVersion), 0, type(uint32).max, type(uint32).max) + RequestStatus(uint32(contractVersion), 0, type(uint32).max, type(uint32).max) ); } @@ -273,21 +273,19 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 newLastDeliveredIndex = startIndex + requestsToDeliver - 1; - if (requestStatus.deliveryHistoryLength == 1) { - _storeDeliveryEntry( - exitRequestsHash, - requestStatus.lastDeliveredExitDataIndex, - requestStatus.lastDeliveredExitDataTimestamp - ); - - _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); - } - - if (requestStatus.deliveryHistoryLength > 1) { + // if totalItemsCount == requestsToDeliver, we deliver it via one attempt and don't need to store deliveryHistory array + // can store in RequestStatus via lastDeliveredExitDataIndex and lastDeliveredExitDataTimestamp fields + bool deliverFullyAtOnce = totalItemsCount == requestsToDeliver && requestStatus.deliveryHistoryLength == 0; + if (!deliverFullyAtOnce) { _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); } - _updateRequestStatus(requestStatus, requestStatus.deliveryHistoryLength + 1, newLastDeliveredIndex); + _updateRequestStatus( + requestStatus, + requestStatus.deliveryHistoryLength + 1, + newLastDeliveredIndex, + _getTimestamp() + ); } /** @@ -397,13 +395,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } /** - * @notice Returns unpacking history and current status for specific exitRequestsData + * @notice Returns delivery history and current status for specific exitRequestsData * * @param exitRequestsHash - The exit requests hash. * * @dev Reverts if: * - exitRequestsHash was not submited - * - No delivery history */ function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (DeliveryHistory[] memory) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); @@ -414,7 +411,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } if (storedRequest.deliveryHistoryLength == 0) { - revert DeliveryWasNotStarted(); + DeliveryHistory[] memory deliveryHistory; + return deliveryHistory; } if (storedRequest.deliveryHistoryLength == 1) { @@ -523,12 +521,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _checkExitNotSubmitted(RequestStatus storage requestStatus) internal view { - if (requestStatus.contractVersion != 0) { - revert ExitHashAlreadySubmitted(); - } - } - function _checkDeliveryStarted(RequestStatus storage status) internal view { if (status.deliveryHistoryLength == 0) { revert DeliveryWasNotStarted(); @@ -591,7 +583,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } - function _storeRequestStatus(bytes32 exitRequestsHash, RequestStatus memory requestStatus) internal { + function _storeNewHashRequestStatus(bytes32 exitRequestsHash, RequestStatus memory requestStatus) internal { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; @@ -610,13 +602,16 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa function _updateRequestStatus( RequestStatus storage requestStatus, uint256 deliveryHistoryLength, - uint256 lastDeliveredExitDataIndex + uint256 lastDeliveredExitDataIndex, + uint256 lastDeliveredExitDataTimestamp ) internal { require(deliveryHistoryLength <= type(uint32).max, "DELIVERY_HISTORY_LENGTH_OVERFLOW"); require(lastDeliveredExitDataIndex <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW"); + require(lastDeliveredExitDataTimestamp <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); + requestStatus.deliveryHistoryLength = uint32(deliveryHistoryLength); requestStatus.lastDeliveredExitDataIndex = uint32(lastDeliveredExitDataIndex); - requestStatus.lastDeliveredExitDataTimestamp = uint32(_getTimestamp()); + requestStatus.lastDeliveredExitDataTimestamp = uint32(lastDeliveredExitDataTimestamp); } function _storeDeliveryEntry( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 3af86187fb..88bb37f317 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -306,7 +306,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storeRequestStatus(exitRequestsHash, RequestStatus(uint8(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); + _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); } /// diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index cf4a2d6af6..c2ceb1145f 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -42,16 +42,24 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { return _storageDataProcessingState().value; } - function storeExitRequestHash( + function storeNewHashRequestStatus( bytes32 exitRequestHash, uint8 contractVersion, uint32 deliveryHistoryLength, uint32 lastDeliveredExitDataIndex, uint32 timestamp ) external { - _storeRequestStatus( + _storeNewHashRequestStatus( exitRequestHash, RequestStatus(contractVersion, deliveryHistoryLength, lastDeliveredExitDataIndex, timestamp) ); } + + function storeDeliveryEntry( + bytes32 exitRequestsHash, + uint256 lastDeliveredExitDataIndex, + uint256 lastDeliveredExitDataTimestamp + ) external { + _storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); + } } diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 908b521c65..3ebba6ee2d 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -146,15 +146,37 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { ); }); - it("returns correct data for a previously stored exitRequestsHash", async () => { + it("Returns empty history if deliveryHistoryLength is equal to 0", async () => { + const MAX_UINT32 = 2 ** 32 - 1; const exitRequestsHash = keccak256("0x1111"); + const deliveryHistoryLength = 0; + const contractVersion = 42; + const lastDeliveredExitDataIndex = MAX_UINT32; + const lastDeliveredExitDataTimestamp = MAX_UINT32; + + // Call the helper to store the hash + await oracle.storeNewHashRequestStatus( + exitRequestsHash, + contractVersion, + deliveryHistoryLength, + lastDeliveredExitDataIndex, + lastDeliveredExitDataTimestamp, + ); + + const returnedHistory = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + + expect(returnedHistory.length).to.equal(0); + }); + + it("Returns array with single record if deliveryHistoryLength is equal to 1", async () => { + const exitRequestsHash = keccak256("0x2222"); const deliveryHistoryLength = 1; const timestamp = await oracle.getTime(); const contractVersion = 42; const lastDeliveredExitDataIndex = 1; // Call the helper to store the hash - await oracle.storeExitRequestHash( + await oracle.storeNewHashRequestStatus( exitRequestsHash, contractVersion, deliveryHistoryLength, @@ -168,5 +190,34 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { const [firstDelivery] = returnedHistory; expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); }); + + it("Returns array with multiple reconrds if deliveryHistoryLength is equal to ", async () => { + const exitRequestsHash = keccak256("0x3333"); + const deliveryHistoryLength = 2; + const timestamp = await oracle.getTime(); + const contractVersion = 42; + // Call the helper to store the hash + await oracle.storeNewHashRequestStatus( + exitRequestsHash, + contractVersion, + deliveryHistoryLength, + 1, + timestamp + 1n, + ); + + await oracle.storeDeliveryEntry(exitRequestsHash, 0, timestamp); + + await oracle.storeDeliveryEntry(exitRequestsHash, 1, timestamp + 1n); + + const returnedHistory = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + + expect(returnedHistory.length).to.equal(2); + const [firstDelivery, secondDelivery] = returnedHistory; + expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(0); + expect(firstDelivery.timestamp).to.equal(timestamp); + + expect(secondDelivery.lastDeliveredExitDataIndex).to.equal(1); + expect(secondDelivery.timestamp).to.equal(timestamp + 1n); + }); }); }); From 0d787e7db836f2f1666a9254debb2a3aa07bd518 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 20 May 2025 16:19:18 +0200 Subject: [PATCH 145/405] feat: improve naming in ValidatorExitDelayVerifier contract --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 6489296b21..7fb117afed 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -188,7 +188,7 @@ contract ValidatorExitDelayVerifier { proofSlotTimestamp ); - _verifyValidatorIsNotExited(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); + _verifyValidatorExitUnset(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); stakingRouter.reportValidatorExitDelay( moduleId, @@ -239,7 +239,7 @@ contract ValidatorExitDelayVerifier { proofSlotTimestamp ); - _verifyValidatorIsNotExited(oldBlock.header, witness, pubkey, valIndex); + _verifyValidatorExitUnset(oldBlock.header, witness, pubkey, valIndex); stakingRouter.reportValidatorExitDelay( moduleId, @@ -292,9 +292,16 @@ contract ValidatorExitDelayVerifier { } /** - * @dev Verifies that a validator is still active (exitEpoch == FAR_FUTURE_EPOCH) and proves it against the state root. + * @notice Proves—via an SSZ Merkle proof—that the validator + * has not scheduled nor completed an exit. + * + * @dev It reconstructs the `Validator` object with `exitEpoch` hard-coded + * to `FAR_FUTURE_EPOCH` and checks that this leaf is present under + * the supplied `stateRoot`. + * + * Reverts if proof verification fail. */ - function _verifyValidatorIsNotExited( + function _verifyValidatorExitUnset( BeaconBlockHeader calldata header, ValidatorWitness calldata witness, bytes memory pubkey, From 95216674e2c1cad2127068b2c6a033f5c75734db Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 20 May 2025 19:48:28 +0400 Subject: [PATCH 146/405] fix: exclude max batch size check from unpack/triggerExits methods --- contracts/0.8.9/interfaces/IValidatorsExitBus.sol | 2 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol index d6b7668e70..ef5fee2e06 100644 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -45,7 +45,7 @@ interface IValidatorsExitBus { bytes calldata exitRequests, uint256 dataFormat, uint256 index - ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); + ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); function resume() external; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d6a301bcaf..08621c55a8 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -250,6 +250,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkExitSubmitted(requestStatus); _checkExitRequestData(request.data, request.dataFormat); + _checkMaxBatchSize(request.data); _checkContractVersion(requestStatus.contractVersion); uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; @@ -442,7 +443,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa bytes calldata exitRequests, uint256 dataFormat, uint256 index - ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { + ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { _checkExitRequestData(exitRequests, dataFormat); if (index >= exitRequests.length / PACKED_REQUEST_LENGTH) { @@ -498,7 +499,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa /// Internal functions - function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal view { + function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal pure { if (dataFormat != DATA_FORMAT_LIST) { revert UnsupportedRequestsDataFormat(dataFormat); } @@ -506,7 +507,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa if (requests.length == 0 || requests.length % PACKED_REQUEST_LENGTH != 0) { revert InvalidRequestsDataLength(); } + } + function _checkMaxBatchSize(bytes calldata requests) internal view { uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); uint256 requestsCount = requests.length / PACKED_REQUEST_LENGTH; From 3899a6fba66669313af39ff2b5041c6f86c12732 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 21 May 2025 13:34:12 +0400 Subject: [PATCH 147/405] fix: ExitRequestData -> ExitRequestsData & added desciptions --- .../0.8.9/interfaces/IValidatorsExitBus.sol | 6 +-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 49 +++++++++++++------ 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol index ef5fee2e06..7d8c038da2 100644 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.9; interface IValidatorsExitBus { - struct ExitRequestData { + struct ExitRequestsData { bytes data; uint256 dataFormat; } @@ -16,10 +16,10 @@ interface IValidatorsExitBus { function submitExitRequestsHash(bytes32 exitReportHash) external; - function submitExitRequestsData(ExitRequestData calldata request) external; + function submitExitRequestsData(ExitRequestsData calldata request) external; function triggerExits( - ExitRequestData calldata exitsData, + ExitRequestsData calldata exitsData, uint256[] calldata exitDataIndexes, address refundRecipient ) external payable; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 08621c55a8..d5b43b2fbc 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -174,8 +174,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 internal constant PUBLIC_KEY_LENGTH = 48; - uint256 internal constant PACKED_TWG_EXIT_REQUEST_LENGTH = 56; - /// @notice The list format of the validator exit requests data. Used when all /// requests fit into a single transaction. /// @@ -196,13 +194,24 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ILidoLocator internal immutable LOCATOR; - /// Hash constant for mapping exit requests storage - bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION = keccak256("lido.ValidatorsExitBus.reportHashes"); - bytes32 public constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); + // Storage slot for exit request limit configuration and current quota tracking + bytes32 internal constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); + // Storage slot for the maximum number of validator exit requests allowed per processing batch bytes32 internal constant MAX_VALIDATORS_PER_BATCH_POSITION = keccak256("lido.ValidatorsExitBus.maxValidatorsPerBatch"); + + // Storage slot for mapping(bytes32 => RequestStatus), keyed by exitRequestsHash bytes32 internal constant REQUEST_STATUS_POSITION = keccak256("lido.ValidatorsExitBus.requestStatus"); - bytes32 internal constant DELIVERY_HISTORY_POSITION = keccak256("lido.ValidatorsBus.deliveryHistory"); + // Storage slot for mapping(bytes32 => DeliveryHistory[]), keyed by exitRequestsHash + bytes32 internal constant DELIVERY_HISTORY_POSITION = keccak256("lido.ValidatorsExitBus.deliveryHistory"); + + // RequestStatus stores the last delivered index of the request, timestamp of delivery, and deliveryHistory length (number of deliveries). + // If a request is fully delivered in one step (as with oracle requests, which can't be delivered partially), + // only RequestStatus is used for efficiency. + // If a request is delivered in parts (e.g., due to limit constraints), + // DeliveryHistory[] stores full delivery records in addition to RequestStatus. + // If deliveryHistoryLength == 1, delivery info is read from RequestStatus; otherwise, from DeliveryHistory[]. + // Both mappings use the same key (exitRequestsHash). constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); @@ -243,7 +252,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * * @param request - The exit requests structure. */ - function submitExitRequestsData(ExitRequestData calldata request) external whenResumed { + function submitExitRequestsData(ExitRequestsData calldata request) external whenResumed { // bytes calldata data = request.data; bytes32 exitRequestsHash = keccak256(abi.encode(request.data, request.dataFormat)); RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestsHash]; @@ -303,7 +312,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * - `exitDataIndexes` is not strictly increasing array */ function triggerExits( - ExitRequestData calldata exitsData, + ExitRequestsData calldata exitsData, uint256[] calldata exitDataIndexes, address refundRecipient ) external payable { @@ -395,6 +404,22 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa currentExitRequestsLimit = _getCurrentExitLimit(); } + /** + * @notice Sets the maximum allowed number of validator exit requests to process in a single batch. + * @param maxRequests The new maximum number of exit requests allowed per batch. + */ + function setMaxRequestsPerBatch(uint256 maxRequests) external onlyRole(MAX_VALIDATORS_PER_BATCH_ROLE) { + _setMaxRequestsPerBatch(maxRequests); + } + + /** + * @notice Returns information about allowed number of validator exit requests to process in a single batch. + * @return The new maximum number of exit requests allowed per batch + */ + function getMaxRequestsPerBatch() external view returns (uint256) { + return _getMaxRequestsPerBatch(); + } + /** * @notice Returns delivery history and current status for specific exitRequestsData * @@ -489,14 +514,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _pauseUntil(_pauseUntilInclusive); } - function setMaxRequestsPerBatch(uint256 value) external onlyRole(MAX_VALIDATORS_PER_BATCH_ROLE) { - _setMaxRequestsPerBatch(value); - } - - function getMaxRequestsPerBatch() external view returns (uint256) { - return _getMaxRequestsPerBatch(); - } - /// Internal functions function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal pure { From 23e51645c8db58243250e5584f0480b92ed4576f Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 21 May 2025 17:03:28 +0200 Subject: [PATCH 148/405] feat: add integration tests for validator exit delay verifier --- .../report-validator-exit-delay.ts | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 6b1c3595d8..ad48a6b9a5 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -90,6 +90,15 @@ describe("Report Validator Exit Delay", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + expect( + await nor.isValidatorExitDelayPenaltyApplicable( + nodeOpId, + proofSlotTimestamp, + ACTIVE_VALIDATOR_PROOF.validator.pubkey, + eligibleToExitInSec, + ), + ).to.be.true; + await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), @@ -99,6 +108,23 @@ describe("Report Validator Exit Delay", () => { ) .and.to.emit(nor, "ValidatorExitStatusUpdated") .withArgs(nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, eligibleToExitInSec, proofSlotTimestamp); + + expect( + await nor.isValidatorExitDelayPenaltyApplicable( + nodeOpId, + proofSlotTimestamp, + ACTIVE_VALIDATOR_PROOF.validator.pubkey, + eligibleToExitInSec, + ), + ).to.be.false; + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); }); it("Should report validator exit delay historically", async () => { @@ -131,6 +157,15 @@ describe("Report Validator Exit Delay", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + expect( + await nor.isValidatorExitDelayPenaltyApplicable( + nodeOpId, + proofSlotTimestamp, + ACTIVE_VALIDATOR_PROOF.validator.pubkey, + eligibleToExitInSec, + ), + ).to.be.true; + await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), @@ -141,5 +176,163 @@ describe("Report Validator Exit Delay", () => { ) .and.to.emit(nor, "ValidatorExitStatusUpdated") .withArgs(nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, eligibleToExitInSec, proofSlotTimestamp); + + expect( + await nor.isValidatorExitDelayPenaltyApplicable( + nodeOpId, + proofSlotTimestamp, + ACTIVE_VALIDATOR_PROOF.validator.pubkey, + eligibleToExitInSec, + ), + ).to.be.false; + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + }); + + it("Should revert when validator reported multiple times in a single transaction", async () => { + const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + // Setup multiple exit requests with the same pubkey + const nodeOpIds = [1, 2]; + const exitRequests = nodeOpIds.map((nodeOpId) => ({ + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + })); + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const currentBlockTimestamp = await getCurrentBlockTimestamp(); + const proofSlotTimestamp = + (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); + + // Set the block timestamp to 7 days before the proof time + await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); + + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + const witnesses = nodeOpIds.map((_, index) => toValidatorWitness(ACTIVE_VALIDATOR_PROOF, index)); + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + witnesses, + encodedExitRequests, + ), + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + }); + + it("Should revert when exit request hash is not submitted", async () => { + const { validatorExitDelayVerifier, validatorsExitBusOracle } = ctx.contracts; + + const exitRequests = [ + { + moduleId, + nodeOpId: 2, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + + const { encodedExitRequests } = encodeExitRequestsDataListWithFormat(exitRequests); + // Note that we don't submit the hash to ValidatorsExitBusOracle + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(await validatorsExitBusOracle, "ExitHashNotSubmitted"); + }); + + it("Should revert when submitting validator exit delay with invalid beacon block root", async () => { + const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + const nodeOpId = 2; + const exitRequests = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); + + // Use a different block root that won't match the header + const fakeRoot = "0xbadbadbad0000000000000000000000000000000000000000000000000000000"; + const mismatchTimestamp = await updateBeaconBlockRoot(fakeRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); + }); + + it("Should revert when reporting validator exit delay before exit deadline threshold", async () => { + const { nor, validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + const nodeOpId = 2; + const exitRequests = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const currentBlockTimestamp = await getCurrentBlockTimestamp(); + const proofSlotTimestamp = + (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); + + const exitDeadlineThreshold = await nor.exitDeadlineThreshold(); + await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - exitDeadlineThreshold); + + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); + + const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory[0].timestamp; + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + expect( + await nor.isValidatorExitDelayPenaltyApplicable( + nodeOpId, + proofSlotTimestamp, + ACTIVE_VALIDATOR_PROOF.validator.pubkey, + eligibleToExitInSec, + ), + ).to.be.false; + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWith("EXIT_DELAY_BELOW_THRESHOLD"); }); }); From 544822a40134e8a9bd69dc7c93188704c8cbb5f5 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 21 May 2025 20:59:19 +0400 Subject: [PATCH 149/405] fix: improved veb coverage --- contracts/0.8.9/lib/ExitLimitUtils.sol | 8 +- test/0.8.9/lib/exitLimitUtils.test.ts | 9 +- ...-bus-oracle.submitExitRequestsData.test.ts | 316 +++++++++++------- ...dator-exit-bus-oracle.triggerExits.test.ts | 11 + 4 files changed, 214 insertions(+), 130 deletions(-) diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index cf2e575a46..29b93c7323 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -96,12 +96,12 @@ library ExitLimitUtils { uint256 frameDuration, uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { - // TODO: restrictions on parameters? - // require(maxExitRequests != 0, "ZERO_MAX_LIMIT"); - // require(frameDuration != 0, "ZERO_FRAME_DURATION"); + // TODO: do we allow maxExitRequests be equal to zero? + // require(maxExitRequests != 0, "ZERO_MAX_LIMIT");; require(maxExitRequestsLimit <= type(uint32).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); - require(exitsPerFrame <= type(uint32).max, "TOO_LARGE_EXITS_PER_FRAME"); require(frameDuration <= type(uint32).max, "TOO_LARGE_FRAME_DURATION"); + require(exitsPerFrame <= maxExitRequestsLimit, "TOO_LARGE_EXITS_PER_FRAME"); + require(frameDuration != 0, "ZERO_FRAME_DURATION"); _data.exitsPerFrame = uint32(exitsPerFrame); _data.frameDuration = uint32(frameDuration); diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts index fd025b0e72..11ca1f9ba4 100644 --- a/test/0.8.9/lib/exitLimitUtils.test.ts +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -426,16 +426,13 @@ describe("ExitLimitUtils.sol", () => { it("should revert if maxExitRequestsLimit is too large", async () => { const MAX_UINT32 = 2 ** 32; - await expect(exitLimit.setExitLimits(MAX_UINT32, 2, 10, 1000)).to.be.revertedWith( + await expect(exitLimit.setExitLimits(MAX_UINT32, 1, 10, 1000)).to.be.revertedWith( "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT", ); }); - it("should revert if exitsPerFrame is too large", async () => { - const MAX_UINT32 = 2 ** 32; - await expect(exitLimit.setExitLimits(100, MAX_UINT32, 10, 1000)).to.be.revertedWith( - "TOO_LARGE_EXITS_PER_FRAME", - ); + it("should revert if exitsPerFrame bigger than maxExitRequestsLimit", async () => { + await expect(exitLimit.setExitLimits(100, 101, 10, 1000)).to.be.revertedWith("TOO_LARGE_EXITS_PER_FRAME"); }); it("should revert if frameDuration is too large", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 45a35b7f21..31ae8945ea 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -9,6 +9,10 @@ import { de0x, numberToHex } from "lib"; import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; +// ----------------------------------------------------------------------------- +// Constants & helpers +// ----------------------------------------------------------------------------- + const PUBKEYS = [ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", @@ -17,41 +21,59 @@ const PUBKEYS = [ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", ]; +// ----------------------------------------------------------------------------- +// Encoding +// ----------------------------------------------------------------------------- + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +interface ExitRequestData { + dataFormat: number; + data: string; +} + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; let admin: HardhatEthersSigner; - let exitRequests: ExitRequest[]; - let exitRequestHash: string; - let exitRequest: ExitRequestData; + let exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + ]; + + let exitRequest: ExitRequestData = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; + + let exitRequestHash: string = hashExitRequest(exitRequest); + let authorizedEntity: HardhatEthersSigner; let stranger: HardhatEthersSigner; const LAST_PROCESSING_REF_SLOT = 1; - interface ExitRequest { - moduleId: number; - nodeOpId: number; - valIndex: number; - valPubkey: string; - } - - interface ExitRequestData { - dataFormat: number; - data: string; - } - - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; - }; - - const encodeExitRequestsDataList = (requests: ExitRequest[]) => { - return "0x" + requests.map(encodeExitRequestHex).join(""); - }; - const deploy = async () => { const deployed = await deployVEBO(admin.address); oracle = deployed.oracle; @@ -73,28 +95,15 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Initially, report was not submitted", async () => { - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - ]; - - exitRequest = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(exitRequests) }; - await expect(oracle.submitExitRequestsData(exitRequest)) .to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted") .withArgs(); }); it("Should revert without SUBMIT_REPORT_HASH_ROLE role", async () => { - const request = [exitRequest.data, exitRequest.dataFormat]; - const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["(bytes, uint256)"], [request])); - - await expect(oracle.connect(stranger).submitExitRequestsHash(hash)).to.be.revertedWithOZAccessControlError( - await stranger.getAddress(), - await oracle.SUBMIT_REPORT_HASH_ROLE(), - ); + await expect( + oracle.connect(stranger).submitExitRequestsHash(exitRequestHash), + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.SUBMIT_REPORT_HASH_ROLE()); }); it("Should store exit hash for authorized entity", async () => { @@ -102,15 +111,17 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await oracle.grantRole(role, authorizedEntity); - exitRequestHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), - ); - const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); }); + it("Should revert if hash was already submitted", async () => { + await expect( + oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash), + ).to.be.revertedWithCustomError(oracle, "ExitHashAlreadySubmitted"); + }); + it("Emit ValidatorExit event", async () => { const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); @@ -157,12 +168,16 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should revert if wrong DATA_FORMAT", async () => { - const request = [exitRequest.data, 2]; - const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], request)); + const exitRequestWrongDataFormat: ExitRequestData = { + dataFormat: 2, + data: encodeExitRequestsDataList(exitRequests), + }; + const hash = hashExitRequest(exitRequestWrongDataFormat); const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); - exitRequest = { dataFormat: 2, data: encodeExitRequestsDataList(exitRequests) }; - await expect(oracle.submitExitRequestsData(exitRequest)) + + await expect(oracle.submitExitRequestsData(exitRequestWrongDataFormat)) .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") .withArgs(2); }); @@ -174,9 +189,36 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await consensus.advanceTimeBy(24 * 60 * 60); }); + // ----------------------------------------------------------------------------- + // Shared test data + // ----------------------------------------------------------------------------- + const MAX_EXIT_REQUESTS_LIMIT = 5; + const EXITS_PER_FRAME = 1; + const FRAME_DURATION = 48; + + // Data for case when limit is not enough to process entire request + const VALIDATORS_DELIVERED_BY_PARTS: ExitRequest[] = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, + { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + ]; + + const REQUEST_DELIVERED_BY_PARTS = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(VALIDATORS_DELIVERED_BY_PARTS), + }; + + const HASH_REQUEST_DELIVERED_BY_PARTS = hashExitRequest(REQUEST_DELIVERED_BY_PARTS); + it("Should deliver request fully as it is below limit", async () => { - const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit(5, 1, 48); - await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(5, 1, 48); + const exitLimitTx = await oracle + .connect(authorizedEntity) + .setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION); + await expect(exitLimitTx) + .to.emit(oracle, "ExitRequestsLimitSet") + .withArgs(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION); exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, @@ -188,9 +230,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { data: encodeExitRequestsDataList(exitRequests), }; - exitRequestHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), - ); + exitRequestHash = hashExitRequest(exitRequest); await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); const emitTx = await oracle.submitExitRequestsData(exitRequest); @@ -204,29 +244,12 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should deliver part of request equal to remaining limit", async () => { - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, - ]; - - exitRequest = { - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }; - - exitRequestHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequest.data, exitRequest.dataFormat]), - ); - - await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); - const emitTx = await oracle.submitExitRequestsData(exitRequest); + await oracle.connect(authorizedEntity).submitExitRequestsHash(HASH_REQUEST_DELIVERED_BY_PARTS); + const emitTx = await oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS); const timestamp = await oracle.getTime(); for (let i = 0; i < 3; i++) { - const request = exitRequests[i]; + const request = VALIDATORS_DELIVERED_BY_PARTS[i]; await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); @@ -234,45 +257,38 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should revert when limit exceeded for the frame", async () => { - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, - ]; - - exitRequest = { - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }; - - await expect(oracle.submitExitRequestsData(exitRequest)) + await expect(oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS)) .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") .withArgs(2, 0); }); - it("Should process remaining requests after a day passes", async () => { - await consensus.advanceTimeBy(2 * 4 * 12); + it("Current limit should be equal to 0", async () => { + const data = await oracle.getExitRequestLimitFullInfo(); - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, - ]; + expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); + expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); + expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.prevExitRequestsLimit).to.equal(0); + expect(data.currentExitRequestsLimit).to.equal(0); + }); - exitRequest = { - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }; + it("Should current limit should be increased on 2 if 2*48 seconds passed", async () => { + await consensus.advanceTimeBy(2 * 4 * 12); + const data = await oracle.getExitRequestLimitFullInfo(); - const emitTx = await oracle.submitExitRequestsData(exitRequest); + expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); + expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); + expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.prevExitRequestsLimit).to.equal(0); + expect(data.currentExitRequestsLimit).to.equal(2); + }); + + it("Should process remaining requests after a day passes", async () => { + const emitTx = await oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS); const timestamp = await oracle.getTime(); for (let i = 3; i < 5; i++) { - const request = exitRequests[i]; + const request = VALIDATORS_DELIVERED_BY_PARTS[i]; await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); @@ -280,15 +296,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should revert when no new requests to deliver", async () => { - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, - ]; - - await expect(oracle.submitExitRequestsData(exitRequest)).to.be.revertedWithCustomError( + await expect(oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS)).to.be.revertedWithCustomError( oracle, "RequestsAlreadyDelivered", ); @@ -302,17 +310,85 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await oracle.connect(authorizedEntity).setMaxRequestsPerBatch(maxRequestsPerBatch); - exitRequests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, - { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, + const exitRequestsRandom = [ + { moduleId: 100, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 101, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 102, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 103, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 104, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, ]; - await expect(oracle.submitExitRequestsData(exitRequest)) + const exitRequestRandom = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequestsRandom), + }; + + const exitRequestHashRandom = hashExitRequest(exitRequestRandom); + + await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHashRandom); + + await expect(oracle.submitExitRequestsData(exitRequestRandom)) .to.be.revertedWithCustomError(oracle, "MaxRequestsBatchSizeExceeded") - .withArgs(exitRequests.length, maxRequestsPerBatch); + .withArgs(exitRequestsRandom.length, maxRequestsPerBatch); + }); + + it("Current limit should be equal to 0", async () => { + const data = await oracle.getExitRequestLimitFullInfo(); + + expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); + expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); + expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.prevExitRequestsLimit).to.equal(0); + expect(data.currentExitRequestsLimit).to.equal(0); + }); + + it("Should set maxExitRequestsLimit equal to 0 and return as currentExitRequestsLimit type(uint256).max", async () => { + // can't set just maxExitRequestsLimit to 0, as it will be less than exitsPerFrame + const exitLimitTx = await oracle.connect(authorizedEntity).setExitRequestLimit(0, 0, FRAME_DURATION); + await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(0, 0, FRAME_DURATION); + + const data = await oracle.getExitRequestLimitFullInfo(); + + expect(data.maxExitRequestsLimit).to.equal(0); + expect(data.exitsPerFrame).to.equal(0); + expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.prevExitRequestsLimit).to.equal(0); + expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); + }); + + it("Should not check limit, if maxLimitRequests equal to 0 (means limit was not set)", async () => { + const exitRequestsRandom = [ + { moduleId: 100, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 101, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + ]; + + const exitRequestRandom = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequestsRandom), + }; + + const exitRequestRandomHash = hashExitRequest(exitRequestRandom); + + await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestRandomHash); + + const emitTx = await oracle.submitExitRequestsData(exitRequestRandom); + const timestamp = await oracle.getTime(); + + for (let i = 0; i < 2; i++) { + const request = exitRequestsRandom[i]; + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + + const data = await oracle.getExitRequestLimitFullInfo(); + + expect(data.maxExitRequestsLimit).to.equal(0); + expect(data.exitsPerFrame).to.equal(0); + expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.prevExitRequestsLimit).to.equal(0); + // as time is mocked and we didnt change it since last consume, currentExitRequestsLimit was not increased + expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); }); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 615810db8e..8694847a3e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -232,4 +232,15 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }), ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); }); + + // it("should revert id request was not started to deliver", async () => { + // await expect( + // oracle.triggerExits( + // { data: reportFields.data, dataFormat: reportFields.dataFormat }, + // [0, 1, 2, 3], + // ZERO_ADDRESS, + // { value: 4 }, + // ), + // ).to.be.revertedWithCustomError(oracle, "DeliveryWasNotStarted"); + // }); }); From 2a8f2e2d2974a838438b871448cd820407f062cd Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 21 May 2025 20:09:47 +0200 Subject: [PATCH 150/405] feat: cover validator exit delay verifier with unit tests --- .../ValidatorsExitBusOracle_Mock.sol | 2 + .../0.8.25/validatorExitDelayVerifier.test.ts | 304 +++++++++++------- .../report-validator-exit-delay.ts | 62 ++++ 3 files changed, 251 insertions(+), 117 deletions(-) diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 39c0e67e2d..0bde4ffb5e 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -22,10 +22,12 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { ) external { _hash = exitRequestsHash; + delete _deliveryHistory; for (uint256 i = 0; i < deliveryHistory.length; i++) { _deliveryHistory.push(deliveryHistory[i]); } + delete _data; for (uint256 i = 0; i < data.length; i++) { _data.push(data[i]); } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 201facf18c..8ed3dfcc3b 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import { ContractTransactionResponse } from "ethers"; import { ethers } from "hardhat"; import { StakingRouter_Mock, ValidatorExitDelayVerifier, ValidatorsExitBusOracle_Mock } from "typechain-types"; @@ -98,6 +99,27 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]), ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidPivotSlot"); }); + + it("reverts with 'ZeroLidoLocatorAddress' if lidoLocator is zero address", async () => { + await expect( + ethers.deployContract("ValidatorExitDelayVerifier", [ + ethers.ZeroAddress, // Zero address for locator + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_HISTORICAL_SUMMARIES_PREV, + GI_HISTORICAL_SUMMARIES_CURR, + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]), + ).to.be.revertedWithCustomError( + await ethers.getContractFactory("ValidatorExitDelayVerifier"), + "ZeroLidoLocatorAddress", + ); + }); }); describe("verifyValidatorExitDelay method", () => { @@ -148,58 +170,16 @@ describe("ValidatorExitDelayVerifier.sol", () => { SECONDS_PER_SLOT; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; - const moduleId = 1; - const nodeOpId = 2; const exitRequests: ExitRequest[] = [ { - moduleId, - nodeOpId, + moduleId: 11, + nodeOpId: 11, valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests( - encodedExitRequestsHash, - [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - exitRequests, - ); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - - const tx = await validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ); - - const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); - expect(events.length).to.equal(1); - - const event = events[0]; - expect(event.args[0]).to.equal(moduleId); - expect(event.args[1]).to.equal(nodeOpId); - expect(event.args[2]).to.equal(proofSlotTimestamp); - expect(event.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); - expect(event.args[4]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); - }); - - it("accepts a valid historical proof and does not revert", async () => { - const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; - const veboExitRequestTimestamp = - GENESIS_TIME + - (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; - const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; - - const moduleId = 1; - const nodeOpId = 2; - const exitRequests: ExitRequest[] = [ { - moduleId, - nodeOpId, + moduleId: 22, + nodeOpId: 22, valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, }, @@ -212,25 +192,45 @@ describe("ValidatorExitDelayVerifier.sol", () => { exitRequests, ); - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(2); + + const firstEvent = events[0]; + expect(firstEvent.args[0]).to.equal(11); + expect(firstEvent.args[1]).to.equal(11); + expect(firstEvent.args[2]).to.equal(proofSlotTimestamp); + expect(firstEvent.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(firstEvent.args[4]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + + const secondEvent = events[1]; + expect(secondEvent.args[0]).to.equal(22); + expect(secondEvent.args[1]).to.equal(22); + expect(secondEvent.args[2]).to.equal(proofSlotTimestamp); + expect(secondEvent.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(secondEvent.args[4]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + }; - const tx = await validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ); + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); - expect(events.length).to.equal(1); + await verifyExitDelayEvents( + await validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], + encodedExitRequests, + ), + ); - const event = events[0]; - expect(event.args[0]).to.equal(moduleId); - expect(event.args[1]).to.equal(nodeOpId); - expect(event.args[2]).to.equal(proofSlotTimestamp); - expect(event.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); - expect(event.args[4]).to.equal(intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT); + await verifyExitDelayEvents( + await validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], + encodedExitRequests, + ), + ); }); it("reverts with 'UnsupportedSlot' when slot < FIRST_SUPPORTED_SLOT", async () => { @@ -250,6 +250,40 @@ describe("ValidatorExitDelayVerifier.sol", () => { EMPTY_REPORT, ), ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + { + rootsTimestamp: 1n, + header: invalidHeader, + }, + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); + }); + + it("reverts with 'UnsupportedSlot' if for historical proof the oldBlock slot < FIRST_SUPPORTED_SLOT", async () => { + const invalidHeader = { + ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + slot: 0, + }; + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + { + header: invalidHeader, + rootGIndex: ACTIVE_VALIDATOR_PROOF.historicalSummariesGI, + proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, + }, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); }); it("reverts with 'RootNotFound' if the staticcall to the block roots contract fails/returns empty", async () => { @@ -264,6 +298,18 @@ describe("ValidatorExitDelayVerifier.sol", () => { EMPTY_REPORT, ), ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "RootNotFound"); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + { + rootsTimestamp: badTimestamp, + header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + }, + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "RootNotFound"); }); it("reverts with 'InvalidBlockHeader' if the block root from contract doesn't match the header root", async () => { @@ -277,6 +323,15 @@ describe("ValidatorExitDelayVerifier.sol", () => { EMPTY_REPORT, ), ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + EMPTY_REPORT, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); it("reverts if the validator proof is incorrect", async () => { @@ -321,93 +376,108 @@ describe("ValidatorExitDelayVerifier.sol", () => { encodedExitRequests, ), ).to.be.reverted; - }); - - it("reverts with 'UnsupportedSlot' if beaconBlock slot < FIRST_SUPPORTED_SLOT", async () => { - const invalidHeader = { - ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, - slot: 0, - }; await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - { - rootsTimestamp: 1n, - header: invalidHeader, - }, + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, timestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - EMPTY_REPORT, + [badWitness], + encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); + ).to.be.reverted; }); - it("reverts with 'UnsupportedSlot' if oldBlock slot < FIRST_SUPPORTED_SLOT", async () => { - const invalidHeader = { - ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, - slot: 0, - }; + it("reverts with 'InvalidGIndex' if oldBlock.rootGIndex is not under the historicalSummaries root", async () => { + // Provide an obviously wrong rootGIndex that won't match the parent's + const invalidRootGIndex = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), { - header: invalidHeader, - rootGIndex: ACTIVE_VALIDATOR_PROOF.historicalSummariesGI, + header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, + rootGIndex: invalidRootGIndex, }, - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], EMPTY_REPORT, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnsupportedSlot"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); }); - it("reverts with 'RootNotFound' if block root contract call fails", async () => { - const badTimestamp = 999_999_999; + it("reverts with 'KeyWasNotUnpacked' if exit request index is not in delivery history", async () => { + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId: 1, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + { + moduleId: 2, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + { + moduleId: 3, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + const unpackedExitRequestIndex = 2; + + // Report not unpacked. + await vebo.setExitRequests(encodedExitRequestsHash, [], exitRequests); + expect((await vebo.getExitRequestsDeliveryHistory(encodedExitRequestsHash)).length).to.equal(0); + await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, badTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - EMPTY_REPORT, + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], + encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "RootNotFound"); - }); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); - it("reverts with 'InvalidBlockHeader' if returned root doesn't match the new block header root", async () => { - const bogusBlockRoot = "0xbadbadbad0000000000000000000000000000000000000000000000000000000"; - const mismatchTimestamp = await updateBeaconBlockRoot(bogusBlockRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, mismatchTimestamp), + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - EMPTY_REPORT, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], + encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); - }); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); - it("reverts with 'InvalidGIndex' if oldBlock.rootGIndex is not under the historicalSummaries root", async () => { - // Provide an obviously wrong rootGIndex that won't match the parent's - const invalidRootGIndex = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + // Report not fully unpacked. + await vebo.setExitRequests(encodedExitRequestsHash, [{ timestamp: 0n, lastDeliveredKeyIndex: 1n }], exitRequests); + expect((await vebo.getExitRequestsDeliveryHistory(encodedExitRequestsHash)).length).to.equal(1); - const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), - { - header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, - proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, - rootGIndex: invalidRootGIndex, - }, - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], - EMPTY_REPORT, + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], + encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); }); it("reverts if the oldBlock proof is corrupted", async () => { diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index ad48a6b9a5..96e8e864f5 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -230,6 +230,17 @@ describe("Report Validator Exit Delay", () => { encodedExitRequests, ), ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + witnesses, + encodedExitRequests, + ), + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); }); it("Should revert when exit request hash is not submitted", async () => { @@ -245,6 +256,7 @@ describe("Report Validator Exit Delay", () => { ]; const { encodedExitRequests } = encodeExitRequestsDataListWithFormat(exitRequests); + // Note that we don't submit the hash to ValidatorsExitBusOracle const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -256,6 +268,56 @@ describe("Report Validator Exit Delay", () => { encodedExitRequests, ), ).to.be.revertedWithCustomError(await validatorsExitBusOracle, "ExitHashNotSubmitted"); + + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(await validatorsExitBusOracle, "ExitHashNotSubmitted"); + }); + + it("Should revert when exit request was not unpacked", async () => { + const { validatorExitDelayVerifier, validatorsExitBusOracle } = ctx.contracts; + + const exitRequests = [ + { + moduleId, + nodeOpId: 2, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + // Note that we don't submit actual report, only hash + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "KeyWasNotUnpacked"); + + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "KeyWasNotUnpacked"); }); it("Should revert when submitting validator exit delay with invalid beacon block root", async () => { From ea5fe124ef777da5af842c177833a87981826f8e Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 10:35:05 +0200 Subject: [PATCH 151/405] refactor: reporting window logic in NOR --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 9bb3a0a99d..2c567281c5 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -247,7 +247,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { address _locator, bytes32 _type, uint256 _exitDeadlineThresholdInSeconds, - uint256 _reportingWindow + uint256 _reportingWindowThresholdInSeconds ) public onlyInit { // Initializations for v1 --> v2 _initialize_v2(_locator, _type); @@ -256,7 +256,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _initialize_v3(); // Initializations for v3 --> v4 - _initialize_v4(_exitDeadlineThresholdInSeconds, _reportingWindow); + _initialize_v4(_exitDeadlineThresholdInSeconds, _reportingWindowThresholdInSeconds); initialized(); } @@ -282,11 +282,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _initialize_v4( uint256 _exitDeadlineThresholdInSeconds, - uint256 _reportingWindow + uint256 _reportingWindowThresholdInSeconds ) internal { _setContractVersion(4); - _setExitDeadlineThreshold(_exitDeadlineThresholdInSeconds, _reportingWindow); + _setExitDeadlineThreshold(_exitDeadlineThresholdInSeconds, _reportingWindowThresholdInSeconds); } /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. @@ -300,14 +300,14 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @notice Finalizes upgrade to version 4 by initializing new exit-related parameters. /// @param _exitDeadlineThresholdInSeconds Exit deadline threshold in seconds for validator exits. - /// @param _exitPenaltyCutoffTimestamp Cutoff timestamp before which validators cannot be penalized. + /// @param _reportingWindowInSeconds Threshold in seconds for bot report. function finalizeUpgrade_v4( uint256 _exitDeadlineThresholdInSeconds, - uint256 _exitPenaltyCutoffTimestamp + uint256 _reportingWindowInSeconds ) external { require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); _checkContractVersion(3); - _initialize_v4(_exitDeadlineThresholdInSeconds, _exitPenaltyCutoffTimestamp); + _initialize_v4(_exitDeadlineThresholdInSeconds, _reportingWindowInSeconds); } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators @@ -1073,20 +1073,31 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _removeUnusedSigningKeys(_nodeOperatorId, _fromIndex, _keysCount); } - function _isValidatorExitingKeyReported(bytes32 _keyHash) internal view returns (bool) { - return _validatorExitProcessedKeys[_keyHash]; + /// @notice Returns true if the given validator public key has already been reported as exiting. + /// @dev The function hashes the input public key using keccak256 and checks if it exists in the _validatorExitProcessedKeys mapping. + /// @param _publicKey The BLS public key of the validator (serialized as bytes). + /// @return True if the validator exit for the provided key has been reported, false otherwise. + function isValidatorExitingKeyReported(bytes _publicKey) public view returns (bool) { + bytes32 processedKeyHash = keccak256(_publicKey); + return _validatorExitProcessedKeys[processedKeyHash]; } - function _markValidatorExitingKeyAsReported(bytes32 _keyHash) internal { + function _markValidatorExitingKeyAsReported(bytes _publicKey) internal { + bytes32 processedKeyHash = keccak256(_publicKey); // Require that key is currently NotProcessed - require(_validatorExitProcessedKeys[_keyHash] == false, + require(_validatorExitProcessedKeys[processedKeyHash] == false, "VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); - _validatorExitProcessedKeys[_keyHash] = true; + _validatorExitProcessedKeys[processedKeyHash] = true; } /// @notice Returns the number of seconds after which a validator is considered late. + /// @dev The operatorId argument is ignored and present only to comply with the IStakingModule interface. /// @return uint256 The exit deadline threshold in seconds for all node operators. - function exitDeadlineThreshold() public view returns (uint256) { + function exitDeadlineThreshold(uint256 /* operatorId */) public view returns (uint256) { + return _exitDeadlineThreshold(); + } + + function _exitDeadlineThreshold() internal view returns (uint256) { return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); } @@ -1108,7 +1119,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) internal { require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); - require(_reportingWindow > 0, "INVALID_REPORTING_WINDOW"); EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_threshold); @@ -1148,12 +1158,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { bytes _publicKey, uint256 _eligibleToExitInSec ) external view returns (bool) { - bytes32 processedKeyHash = keccak256(_publicKey); // Check if the key is already reported - if (_isValidatorExitingKeyReported(processedKeyHash)) { + if (isValidatorExitingKeyReported(_publicKey)) { return false; } - return _eligibleToExitInSec >= exitDeadlineThreshold() + return _eligibleToExitInSec >= _exitDeadlineThreshold() && _proofSlotTimestamp >= exitPenaltyCutoffTimestamp(); } @@ -1174,11 +1183,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { require(_publicKey.length == 48, "INVALID_PUBLIC_KEY"); // Check if exit delay exceeds the threshold - require(_eligibleToExitInSec >= exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); - require(_proofSlotTimestamp >= exitPenaltyCutoffTimestamp(), "EXIT_PENALTY_CUTOFF_NOT_REACHED"); + require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); + require(_proofSlotTimestamp + _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "EXIT_PENALTY_CUTOFF_NOT_REACHED"); - bytes32 processedKeyHash = keccak256(_publicKey); - _markValidatorExitingKeyAsReported(processedKeyHash); + _markValidatorExitingKeyAsReported(_publicKey); emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } From f6c1aca73218cdc1d1f103767e03a64610b295f8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 22 May 2025 13:00:33 +0400 Subject: [PATCH 152/405] fix: triggerExits coverage --- ...-bus-oracle.submitExitRequestsData.test.ts | 1 - ...dator-exit-bus-oracle.triggerExits.test.ts | 339 ++++++++++-------- 2 files changed, 189 insertions(+), 151 deletions(-) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 31ae8945ea..fcd7088b70 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -24,7 +24,6 @@ const PUBKEYS = [ // ----------------------------------------------------------------------------- // Encoding // ----------------------------------------------------------------------------- - interface ExitRequest { moduleId: number; nodeOpId: number; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 8694847a3e..b1f460944b 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -13,6 +13,10 @@ import { CONSENSUS_VERSION, de0x, numberToHex } from "lib"; import { DATA_FORMAT_LIST, deployVEBO, initVEBO, SECONDS_PER_FRAME } from "test/deploy"; +// ----------------------------------------------------------------------------- +// Constants & helpers +// ----------------------------------------------------------------------------- + const PUBKEYS = [ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", @@ -23,6 +27,59 @@ const PUBKEYS = [ const ZERO_ADDRESS = ethers.ZeroAddress; +const LAST_PROCESSING_REF_SLOT = 1; + +// ----------------------------------------------------------------------------- +// Encoding +// ----------------------------------------------------------------------------- + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +interface ReportFields { + consensusVersion: bigint; + refSlot: bigint; + requestsCount: number; + dataFormat: number; + data: string; +} + +const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { + const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; + const reportDataHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), + ); + return reportDataHash; +}; + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); +}; + +const createValidatorDataList = (requests: ExitRequest[]) => { + return requests.map((request) => ({ + stakingModuleId: request.moduleId, + nodeOperatorId: request.nodeOpId, + pubkey: request.valPubkey, + })); +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let consensus: HashConsensus__Harness; let oracle: ValidatorsExitBus__Harness; @@ -30,56 +87,11 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let triggerableWithdrawalsGateway: TriggerableWithdrawalsGateway__MockForVEB; let oracleVersion: bigint; - let exitRequests: ExitRequest[]; - let reportFields: ReportFields; - let reportHash: string; let member1: HardhatEthersSigner; let member2: HardhatEthersSigner; let member3: HardhatEthersSigner; - - const LAST_PROCESSING_REF_SLOT = 1; - - interface ExitRequest { - moduleId: number; - nodeOpId: number; - valIndex: number; - valPubkey: string; - } - - interface ReportFields { - consensusVersion: bigint; - refSlot: bigint; - requestsCount: number; - dataFormat: number; - data: string; - } - - const calcValidatorsExitBusReportDataHash = (items: ReportFields) => { - const reportData = [items.consensusVersion, items.refSlot, items.requestsCount, items.dataFormat, items.data]; - const reportDataHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["(uint256,uint256,uint256,uint256,bytes)"], [reportData]), - ); - return reportDataHash; - }; - - const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; - }; - - const encodeExitRequestsDataList = (requests: ExitRequest[]) => { - return "0x" + requests.map(encodeExitRequestHex).join(""); - }; - - const createValidatorDataList = (requests: ExitRequest[]) => { - return requests.map((request) => ({ - stakingModuleId: request.moduleId, - nodeOperatorId: request.nodeOpId, - pubkey: request.valPubkey, - })); - }; + let authorizedEntity: HardhatEthersSigner; const deploy = async () => { const deployed = await deployVEBO(admin.address); @@ -103,7 +115,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }; before(async () => { - [admin, member1, member2, member3] = await ethers.getSigners(); + [admin, member1, member2, member3, authorizedEntity] = await ethers.getSigners(); await deploy(); }); @@ -115,132 +127,159 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { expect((await consensus.getConsensusState()).consensusReport).to.equal(hash); }; - it("some time passes", async () => { - await consensus.advanceTimeBy(24 * 60 * 60); - }); - - it("committee reaches consensus on a report hash", async () => { - const { refSlot } = await consensus.getCurrentFrame(); - - exitRequests = [ + describe("Submit via oracle flow ", async () => { + const exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, { moduleId: 2, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[3] }, ]; - reportFields = { - consensusVersion: CONSENSUS_VERSION, - refSlot: refSlot, - requestsCount: exitRequests.length, - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(exitRequests), - }; + let reportFields: ReportFields; + let reportHash: string; - reportHash = calcValidatorsExitBusReportDataHash(reportFields); + it("some time passes", async () => { + await consensus.advanceTimeBy(24 * 60 * 60); + }); - await triggerConsensusOnHash(reportHash); - }); + it("committee reaches consensus on a report hash", async () => { + const { refSlot } = await consensus.getCurrentFrame(); - it("some time passes", async () => { - await consensus.advanceTimeBy(SECONDS_PER_FRAME / 3n); - }); + reportFields = { + consensusVersion: CONSENSUS_VERSION, + refSlot: refSlot, + requestsCount: exitRequests.length, + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; - it("a committee member submits the report data, exit requests are emitted", async () => { - const tx = await oracle.connect(member1).submitReportData(reportFields, oracleVersion); + reportHash = calcValidatorsExitBusReportDataHash(reportFields); - await expect(tx).to.emit(oracle, "ProcessingStarted").withArgs(reportFields.refSlot, reportHash); - expect((await oracle.getConsensusReport()).processingStarted).to.equal(true); + await triggerConsensusOnHash(reportHash); + }); - const timestamp = await oracle.getTime(); + it("some time passes", async () => { + await consensus.advanceTimeBy(SECONDS_PER_FRAME / 3n); + }); - for (const request of exitRequests) { - await expect(tx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); - } - }); + it("a committee member submits the report data, exit requests are emitted", async () => { + const tx = await oracle.connect(member1).submitReportData(reportFields, oracleVersion); - it("should triggers exits for all validators in exit request", async () => { - const tx = await oracle.triggerExits( - { data: reportFields.data, dataFormat: reportFields.dataFormat }, - [0, 1, 2, 3], - ZERO_ADDRESS, - { value: 4 }, - ); + const timestamp = await oracle.getTime(); - const requests = createValidatorDataList(exitRequests); + for (const request of exitRequests) { + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } + }); - await expect(tx) - .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests.length, admin.address, 1); - }); + it("should triggers exits for all validators in exit request", async () => { + const tx = await oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 2, 3], + ZERO_ADDRESS, + { value: 4 }, + ); - it("should triggers exits only for validators in selected request indexes", async () => { - const tx = await oracle.triggerExits( - { data: reportFields.data, dataFormat: reportFields.dataFormat }, - [0, 1, 3], - ZERO_ADDRESS, - { - value: 10, - }, - ); - - const requests = createValidatorDataList(exitRequests.filter((req, i) => [0, 1, 3].includes(i))); - - await expect(tx) - .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests.length, admin.address, 1); - }); + const requests = createValidatorDataList(exitRequests); + + await expect(tx) + .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") + .withArgs(requests.length, admin.address, 1); + }); - it("should revert with error if the hash of `requestsData` was not previously submitted in the VEB", async () => { - await expect( - oracle.triggerExits( + it("should triggers exits only for validators in selected request indexes", async () => { + const tx = await oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 3], + ZERO_ADDRESS, { - data: "0x0000030000000000000000000000005a894d712b61ee6d5da473f87d9c8175c4022fd05a8255b6713dc75388b099a85514ceca78a52b9122d09aecda9010c047", - dataFormat: reportFields.dataFormat, + value: 10, }, - [0], - ZERO_ADDRESS, - { value: 2 }, - ), - ).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); - }); + ); - it("should revert with error if requested index out of range", async () => { - await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], ZERO_ADDRESS, { - value: 2, - }), - ) - .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") // TODO: fix in code return "ExitDataIndexOutOfRange") - .withArgs(5, 3); // 4 - }); + const requests = createValidatorDataList(exitRequests.filter((req, i) => [0, 1, 3].includes(i))); - it("should revert with an error if the key index array contains duplicates", async () => { - await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { - value: 2, - }), - ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); - }); + await expect(tx) + .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") + .withArgs(requests.length, admin.address, 1); + }); - it("should revert with an error if the key index array is not strictly increasing", async () => { - await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { - value: 2, - }), - ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); + it("should revert with error if the hash of `requestsData` was not previously submitted in the VEB", async () => { + await expect( + oracle.triggerExits( + { + data: "0x0000030000000000000000000000005a894d712b61ee6d5da473f87d9c8175c4022fd05a8255b6713dc75388b099a85514ceca78a52b9122d09aecda9010c047", + dataFormat: reportFields.dataFormat, + }, + [0], + ZERO_ADDRESS, + { value: 2 }, + ), + ).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); + }); + + it("should revert with error if requested index out of range", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [5], ZERO_ADDRESS, { + value: 2, + }), + ) + .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") // TODO: fix in code return "ExitDataIndexOutOfRange") + .withArgs(5, 3); // 4 + }); + + it("should revert with an error if the key index array contains duplicates", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { + value: 2, + }), + ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); + }); + + it("should revert with an error if the key index array is not strictly increasing", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { + value: 2, + }), + ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); + }); }); - // it("should revert id request was not started to deliver", async () => { - // await expect( - // oracle.triggerExits( - // { data: reportFields.data, dataFormat: reportFields.dataFormat }, - // [0, 1, 2, 3], - // ZERO_ADDRESS, - // { value: 4 }, - // ), - // ).to.be.revertedWithCustomError(oracle, "DeliveryWasNotStarted"); - // }); + // the only difference in this checks, is that it is possible to get DeliveryWasNotStarted error because of partial delivery + describe("Submit via trustfull method", () => { + const exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + ]; + + const exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(exitRequests), + }; + + const exitRequestHash: string = hashExitRequest(exitRequest); + + it("Should store exit hash for authorized entity", async () => { + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + + await oracle.grantRole(role, authorizedEntity); + + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); + + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); + }); + + it("should revert if request was not started to deliver", async () => { + await expect( + oracle.triggerExits( + { data: exitRequest.data, dataFormat: exitRequest.dataFormat }, + [0, 1, 2, 3], + ZERO_ADDRESS, + { value: 4 }, + ), + ).to.be.revertedWithCustomError(oracle, "DeliveryWasNotStarted"); + }); + }); }); From 4d76f8d09de881068ebf122713a0f532c2a39db9 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 11:26:55 +0200 Subject: [PATCH 153/405] refactor: finalize_v4 remove unnecessary parameter --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 25 ++++++---------- .../0120-initialize-non-aragon-contracts.ts | 2 -- test/0.4.24/nor/nor.aux.test.ts | 4 +-- test/0.4.24/nor/nor.exit.manager.test.ts | 30 ++++++++++++------- test/0.4.24/nor/nor.limits.test.ts | 4 +-- test/0.4.24/nor/nor.management.flow.test.ts | 4 +-- .../nor/nor.rewards.penalties.flow.test.ts | 3 +- test/0.4.24/nor/nor.signing.keys.test.ts | 3 +- test/0.4.24/nor/nor.staking.limit.test.ts | 3 +- 9 files changed, 37 insertions(+), 41 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 2c567281c5..ed499968a2 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -246,8 +246,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function initialize( address _locator, bytes32 _type, - uint256 _exitDeadlineThresholdInSeconds, - uint256 _reportingWindowThresholdInSeconds + uint256 _exitDeadlineThresholdInSeconds ) public onlyInit { // Initializations for v1 --> v2 _initialize_v2(_locator, _type); @@ -256,7 +255,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _initialize_v3(); // Initializations for v3 --> v4 - _initialize_v4(_exitDeadlineThresholdInSeconds, _reportingWindowThresholdInSeconds); + _initialize_v4(_exitDeadlineThresholdInSeconds); initialized(); } @@ -280,13 +279,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.Distributed); } - function _initialize_v4( - uint256 _exitDeadlineThresholdInSeconds, - uint256 _reportingWindowThresholdInSeconds - ) internal { + function _initialize_v4(uint256 _exitDeadlineThresholdInSeconds) internal { _setContractVersion(4); - - _setExitDeadlineThreshold(_exitDeadlineThresholdInSeconds, _reportingWindowThresholdInSeconds); + /// @dev The reportingWindowThreshold is set to 0 because it is not required during cold start. + /// This parameter is only relevant when changing _exitDeadlineThresholdInSeconds in future upgrades. + _setExitDeadlineThreshold(_exitDeadlineThresholdInSeconds, 0); } /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. @@ -300,14 +297,10 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @notice Finalizes upgrade to version 4 by initializing new exit-related parameters. /// @param _exitDeadlineThresholdInSeconds Exit deadline threshold in seconds for validator exits. - /// @param _reportingWindowInSeconds Threshold in seconds for bot report. - function finalizeUpgrade_v4( - uint256 _exitDeadlineThresholdInSeconds, - uint256 _reportingWindowInSeconds - ) external { + function finalizeUpgrade_v4(uint256 _exitDeadlineThresholdInSeconds) external { require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); _checkContractVersion(3); - _initialize_v4(_exitDeadlineThresholdInSeconds, _reportingWindowInSeconds); + _initialize_v4(_exitDeadlineThresholdInSeconds); } /// @notice Add node operator named `name` with reward address `rewardAddress` and staking limit = 0 validators @@ -1184,7 +1177,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Check if exit delay exceeds the threshold require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); - require(_proofSlotTimestamp + _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "EXIT_PENALTY_CUTOFF_NOT_REACHED"); + require(_proofSlotTimestamp + _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "TOO_LATE_FOR_EXIT_DELAY_REPORT"); _markValidatorExitingKeyAsReported(_publicKey); diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index 0bc8273638..f4b10394a7 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -51,7 +51,6 @@ export async function main() { lidoLocatorAddress, encodeStakingModuleTypeId(nodeOperatorsRegistryParams.stakingModuleTypeId), nodeOperatorsRegistryParams.exitDeadlineThresholdInSeconds, - nodeOperatorsRegistryParams.exitsReportingWindow, ], { from: deployer }, ); @@ -64,7 +63,6 @@ export async function main() { lidoLocatorAddress, encodeStakingModuleTypeId(simpleDvtRegistryParams.stakingModuleTypeId), simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, - simpleDvtRegistryParams.exitDeadlineThresholdInSeconds, ], { from: deployer }, ); diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index db0717bfa1..df103ff93a 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -72,7 +72,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; + const contractVersionV2 = 2n; const contractVersionV3 = 3n; @@ -120,7 +120,7 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 60f9b41d25..27c05a8558 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -57,11 +57,10 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; const testPublicKey = "0x" + "0".repeat(48 * 2); const eligibleToExitInSec = 86400n; // 2 days - const proofSlotTimestamp = (1n << 256n) - 1n; // 2^256 - 1 max value for uint256 + let proofSlotTimestamp = 0n; const withdrawalRequestPaidFee = 100000n; const exitType = 1n; @@ -104,10 +103,10 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) - .to.emit(nor, "RewardDistributionStateChanged") - .withArgs(RewardDistributionState.Distributed); - + const tx = nor.initialize(locator, moduleType, exitDeadlineThreshold); + await expect(tx).to.emit(nor, "RewardDistributionStateChanged").withArgs(RewardDistributionState.Distributed); + const txRes = await tx; + proofSlotTimestamp = BigInt((await txRes.getBlock())!.timestamp); // Add a node operator for testing expect(await addNodeOperator(nor, nodeOperatorsManager, NODE_OPERATORS[firstNodeOperatorId])).to.be.equal( firstNodeOperatorId, @@ -176,7 +175,11 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("emits an event when called by sender with STAKING_ROUTER_ROLE", async () => { expect(await acl["hasPermission(address,address,bytes32)"](stakingRouter, nor, await nor.STAKING_ROUTER_ROLE())) .to.be.true; - + console.log( + await nor.connect(stakingRouter).exitPenaltyCutoffTimestamp(), + proofSlotTimestamp, + eligibleToExitInSec, + ); await expect( nor .connect(stakingRouter) @@ -197,7 +200,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { context("exitDeadlineThreshold", () => { it("returns the expected value", async () => { - const threshold = await nor.exitDeadlineThreshold(); + const threshold = await nor.exitDeadlineThreshold(0); expect(threshold).to.equal(86400n); }); }); @@ -234,7 +237,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { context("exitPenaltyCutoffTimestamp", () => { const threshold = 86400n; // 1 day - // eslint-disable-next-line @typescript-eslint/no-shadow + const reportingWindow = 3600n; // 1 hour // eslint-disable-next-line @typescript-eslint/no-shadow const eligibleToExitInSec = threshold + 100n; @@ -290,8 +293,13 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { await expect( nor .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, cutoff - 1n, testPublicKey, eligibleToExitInSec), - ).to.be.revertedWith("EXIT_PENALTY_CUTOFF_NOT_REACHED"); + .reportValidatorExitDelay( + firstNodeOperatorId, + cutoff - eligibleToExitInSec * 2n, + testPublicKey, + eligibleToExitInSec, + ), + ).to.be.revertedWith("TOO_LATE_FOR_EXIT_DELAY_REPORT"); }); it("emits event when reportValidatorExitDelay is called with _proofSlotTimestamp >= cutoff", async () => { diff --git a/test/0.4.24/nor/nor.limits.test.ts b/test/0.4.24/nor/nor.limits.test.ts index 3cb383cb3c..b39d035644 100644 --- a/test/0.4.24/nor/nor.limits.test.ts +++ b/test/0.4.24/nor/nor.limits.test.ts @@ -78,7 +78,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; + const contractVersionV2 = 2n; const contractVersionV3 = 3n; @@ -123,7 +123,7 @@ describe("NodeOperatorsRegistry.sol:validatorsLimits", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .to.emit(nor, "ContractVersionSet") diff --git a/test/0.4.24/nor/nor.management.flow.test.ts b/test/0.4.24/nor/nor.management.flow.test.ts index 5dd848712d..2943730683 100644 --- a/test/0.4.24/nor/nor.management.flow.test.ts +++ b/test/0.4.24/nor/nor.management.flow.test.ts @@ -88,7 +88,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; + const contractVersion = 2n; before(async () => { @@ -135,7 +135,7 @@ describe("NodeOperatorsRegistry.sol:management", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts index 3ebcab3288..f1c0868dac 100644 --- a/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts +++ b/test/0.4.24/nor/nor.rewards.penalties.flow.test.ts @@ -79,7 +79,6 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; const contractVersion = 2n; before(async () => { @@ -129,7 +128,7 @@ describe("NodeOperatorsRegistry.sol:rewards-penalties", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.signing.keys.test.ts b/test/0.4.24/nor/nor.signing.keys.test.ts index 7e971ded91..d2898c4fc6 100644 --- a/test/0.4.24/nor/nor.signing.keys.test.ts +++ b/test/0.4.24/nor/nor.signing.keys.test.ts @@ -99,7 +99,6 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; const contractVersion = 2n; const firstNOKeys = new FakeValidatorKeys(5, { kFill: "a", sFill: "b" }); @@ -150,7 +149,7 @@ describe("NodeOperatorsRegistry.sol:signing-keys", () => { locator = await ethers.getContractAt("LidoLocator", await lido.getLidoLocator(), deployer); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") diff --git a/test/0.4.24/nor/nor.staking.limit.test.ts b/test/0.4.24/nor/nor.staking.limit.test.ts index 244c21ab46..87f8fedb42 100644 --- a/test/0.4.24/nor/nor.staking.limit.test.ts +++ b/test/0.4.24/nor/nor.staking.limit.test.ts @@ -82,7 +82,6 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { const moduleType = encodeBytes32String("curated-onchain-v1"); const exitDeadlineThreshold = 86400n; - const reportingWindow = 86400n; const contractVersion = 2n; before(async () => { @@ -126,7 +125,7 @@ describe("NodeOperatorsRegistry.sol:stakingLimit", () => { locator = LidoLocator__factory.connect(await lido.getLidoLocator(), user); // Initialize the nor's proxy. - await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold, reportingWindow)) + await expect(nor.initialize(locator, moduleType, exitDeadlineThreshold)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersion) .and.to.emit(nor, "LocatorContractSet") From 1dba2f4a73a5edf4289e789c88a75fa5b95b1a6e Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 11:30:35 +0200 Subject: [PATCH 154/405] fix: remove redundant parameters in nor.initialize calls --- test/0.4.24/nor/nor.initialize.upgrade.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 846b794a91..003f53ad57 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -88,26 +88,26 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { }); it("Reverts if Locator is zero address", async () => { - await expect(nor.initialize(ZeroAddress, moduleType, 86400n, 86400n)).to.be.reverted; + await expect(nor.initialize(ZeroAddress, moduleType, 86400n)).to.be.reverted; }); it("Reverts if was initialized with v1", async () => { await nor.harness__initialize(1n); - await expect(nor.initialize(locator, moduleType, 86400n, 86400n)).to.be.revertedWith("INIT_ALREADY_INITIALIZED"); + await expect(nor.initialize(locator, moduleType, 86400n)).to.be.revertedWith("INIT_ALREADY_INITIALIZED"); }); it("Reverts if already initialized", async () => { - await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), 86400n, 86400n); + await nor.initialize(locator, encodeBytes32String("curated-onchain-v1"), 86400n); - await expect(nor.initialize(locator, moduleType, 86400n, 86400n)).to.be.revertedWith("INIT_ALREADY_INITIALIZED"); + await expect(nor.initialize(locator, moduleType, 86400n)).to.be.revertedWith("INIT_ALREADY_INITIALIZED"); }); it("Makes the contract initialized to v4", async () => { const burnerAddress = await locator.burner(); const latestBlock = BigInt(await time.latestBlock()); - await expect(nor.initialize(locator, moduleType, 86400n, 86400n)) + await expect(nor.initialize(locator, moduleType, 86400n)) .to.emit(nor, "ContractVersionSet") .withArgs(contractVersionV2) .and.to.emit(nor, "LocatorContractSet") From 0ba1a59d9dccf884b97be8813f5c82fecb23eb6e Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 11:33:41 +0200 Subject: [PATCH 155/405] refactor: update event checks in initialize NOR tests --- test/0.4.24/nor/nor.exit.manager.test.ts | 6 +----- test/0.4.24/nor/nor.initialize.upgrade.test.ts | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 27c05a8558..4e3165d599 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -175,11 +175,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("emits an event when called by sender with STAKING_ROUTER_ROLE", async () => { expect(await acl["hasPermission(address,address,bytes32)"](stakingRouter, nor, await nor.STAKING_ROUTER_ROLE())) .to.be.true; - console.log( - await nor.connect(stakingRouter).exitPenaltyCutoffTimestamp(), - proofSlotTimestamp, - eligibleToExitInSec, - ); + await expect( nor .connect(stakingRouter) diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 003f53ad57..8422bd1271 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -115,7 +115,9 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { .and.to.emit(nor, "StakingModuleTypeSet") .withArgs(moduleType) .to.emit(nor, "RewardDistributionStateChanged") - .withArgs(RewardDistributionState.Distributed); + .withArgs(RewardDistributionState.Distributed) + .to.emit(nor, "ExitDeadlineThresholdChanged") + .withArgs(86400n, 0n); expect(await nor.getLocator()).to.equal(await locator.getAddress()); expect(await nor.getInitializationBlock()).to.equal(latestBlock + 1n); From c766b4c22950639e1114f43c6a700257e6d0aa94 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 11:40:54 +0200 Subject: [PATCH 156/405] test: add validation checks for reporting validator exit delays --- test/0.4.24/nor/nor.exit.manager.test.ts | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 4e3165d599..8e839c2b5d 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -150,6 +150,18 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, "0x", eligibleToExitInSec), ).to.be.revertedWith("INVALID_PUBLIC_KEY"); }); + + it("reverts when reporting the same validator key twice", async () => { + await nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); + + await expect( + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec) + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + }); }); context("onValidatorExitTriggered", () => { @@ -308,4 +320,33 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff); }); }); + + context("isValidatorExitingKeyReported", () => { + it("returns false for keys that haven't been reported yet", async () => { + const result = await nor.isValidatorExitingKeyReported(testPublicKey); + expect(result).to.be.false; + }); + + it("returns true for keys that have been reported", async () => { + expect(await nor.isValidatorExitingKeyReported(testPublicKey)).to.be.false; + + await nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); + + expect(await nor.isValidatorExitingKeyReported(testPublicKey)).to.be.true; + }); + + it("correctly distinguishes between different validator keys", async () => { + const testPublicKey2 = "0x" + "1".repeat(48 * 2); + + await nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); + + expect(await nor.isValidatorExitingKeyReported(testPublicKey)).to.be.true; + + expect(await nor.isValidatorExitingKeyReported(testPublicKey2)).to.be.false; + }); + }); }); From ce9ca62d29ac00da354fdcbe964d80bbb6ea1c37 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 13:19:41 +0200 Subject: [PATCH 157/405] fix: add cutoff logic to view method in NOR --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 2 +- test/0.4.24/nor/nor.exit.manager.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index ed499968a2..2c9651908b 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1156,7 +1156,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return false; } return _eligibleToExitInSec >= _exitDeadlineThreshold() - && _proofSlotTimestamp >= exitPenaltyCutoffTimestamp(); + && _proofSlotTimestamp + _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(); } /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 8e839c2b5d..99dea8495d 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -270,7 +270,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns false when _proofSlotTimestamp < cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff - 1n, + cutoff - eligibleToExitInSec - 1n, testPublicKey, eligibleToExitInSec, ); From 011b1245d4fab7c6043ae038e5031ee6e7b13cac Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 14:35:54 +0200 Subject: [PATCH 158/405] fix: cutoff window calculation --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 4 +- test/0.4.24/nor/nor.exit.manager.test.ts | 43 +++++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 2c9651908b..b029bec12a 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1156,7 +1156,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return false; } return _eligibleToExitInSec >= _exitDeadlineThreshold() - && _proofSlotTimestamp + _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(); + && _proofSlotTimestamp - _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(); } /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. @@ -1177,7 +1177,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Check if exit delay exceeds the threshold require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); - require(_proofSlotTimestamp + _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "TOO_LATE_FOR_EXIT_DELAY_REPORT"); + require(_proofSlotTimestamp - _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "TOO_LATE_FOR_EXIT_DELAY_REPORT"); _markValidatorExitingKeyAsReported(_publicKey); diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 99dea8495d..e78d5a3410 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -59,7 +59,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { const exitDeadlineThreshold = 86400n; const testPublicKey = "0x" + "0".repeat(48 * 2); - const eligibleToExitInSec = 86400n; // 2 days + const ONE_DAY = 86400n; + const eligibleToExitInSec = ONE_DAY; + let proofSlotTimestamp = 0n; const withdrawalRequestPaidFee = 100000n; const exitType = 1n; @@ -159,7 +161,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { await expect( nor .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec), ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); }); }); @@ -217,17 +219,17 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns true when eligible to exit time exceeds the threshold", async () => { const shouldPenalize = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - proofSlotTimestamp, + proofSlotTimestamp + ONE_DAY, testPublicKey, - 172800n, // Equal to the threshold + eligibleToExitInSec, // Equal to the threshold ); expect(shouldPenalize).to.be.true; const shouldPenalizeMore = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - proofSlotTimestamp, + proofSlotTimestamp + ONE_DAY, testPublicKey, - 172801n, // Greater than the threshold + eligibleToExitInSec + 1n, // Greater than the threshold ); expect(shouldPenalizeMore).to.be.true; }); @@ -235,7 +237,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns false when eligible to exit time is less than the threshold", async () => { const shouldPenalize = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - proofSlotTimestamp, + proofSlotTimestamp + ONE_DAY, testPublicKey, 1n, // Less than the threshold ); @@ -244,11 +246,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { }); context("exitPenaltyCutoffTimestamp", () => { - const threshold = 86400n; // 1 day + const threshold = ONE_DAY; const reportingWindow = 3600n; // 1 hour - // eslint-disable-next-line @typescript-eslint/no-shadow - const eligibleToExitInSec = threshold + 100n; let cutoff: bigint; @@ -270,9 +270,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns false when _proofSlotTimestamp < cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff - eligibleToExitInSec - 1n, + cutoff + threshold - 1n, testPublicKey, - eligibleToExitInSec, + threshold, ); expect(result).to.be.false; }); @@ -280,9 +280,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns true when _proofSlotTimestamp == cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff, + cutoff + threshold, testPublicKey, - eligibleToExitInSec, + threshold, ); expect(result).to.be.true; }); @@ -290,9 +290,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns true when _proofSlotTimestamp > cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff + 1n, + cutoff + threshold + 1n, testPublicKey, - eligibleToExitInSec, + threshold, ); expect(result).to.be.true; }); @@ -303,7 +303,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .connect(stakingRouter) .reportValidatorExitDelay( firstNodeOperatorId, - cutoff - eligibleToExitInSec * 2n, + cutoff + threshold - 1n, testPublicKey, eligibleToExitInSec, ), @@ -314,10 +314,15 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { await expect( nor .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, cutoff, testPublicKey, eligibleToExitInSec), + .reportValidatorExitDelay( + firstNodeOperatorId, + cutoff + threshold, + testPublicKey, + eligibleToExitInSec, + ), ) .to.emit(nor, "ValidatorExitStatusUpdated") - .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff); + .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff + threshold); }); }); From b3574aa49f4c7bf96fb1c77a291229005fd325bc Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 14:52:45 +0200 Subject: [PATCH 159/405] fix: update exitDeadlineThreshold call to include parameter --- test/integration/report-validator-exit-delay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 96e8e864f5..12ec804281 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -369,7 +369,7 @@ describe("Report Validator Exit Delay", () => { const proofSlotTimestamp = (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); - const exitDeadlineThreshold = await nor.exitDeadlineThreshold(); + const exitDeadlineThreshold = await nor.exitDeadlineThreshold(0); await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - exitDeadlineThreshold); await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); From 60364f27c9a5da8ec00159b3bec8436dbadb2b5d Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 22 May 2025 14:54:32 +0200 Subject: [PATCH 160/405] feat: cover ValidatorExitDelayVerifier contract with unit tests --- .../0.8.25/validatorExitDelayVerifier.test.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 8ed3dfcc3b..b4a08b6fb2 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -233,6 +233,69 @@ describe("ValidatorExitDelayVerifier.sol", () => { ); }); + it("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { + const activationEpochTimestamp = + GENESIS_TIME + Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + const earliestPossibleVoluntaryExitTimestamp = + activationEpochTimestamp + Number(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()); + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; + const expectedSecondsSinceEligibleExit = proofSlotTimestamp - earliestPossibleVoluntaryExitTimestamp; + + //The exit request happens before the earliest possible exit time! + const veboExitRequestTimestamp = earliestPossibleVoluntaryExitTimestamp - 1000; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests( + encodedExitRequestsHash, + [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], + exitRequests, + ); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + + const event = events[0]; + expect(event.args[0]).to.equal(moduleId); + expect(event.args[1]).to.equal(nodeOpId); + expect(event.args[2]).to.equal(proofSlotTimestamp); + expect(event.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(event.args[4]).to.equal(expectedSecondsSinceEligibleExit); + }; + + await verifyExitDelayEvents( + await validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ); + + await verifyExitDelayEvents( + await validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ); + }); + it("reverts with 'UnsupportedSlot' when slot < FIRST_SUPPORTED_SLOT", async () => { // Use a slot smaller than FIRST_SUPPORTED_SLOT const invalidHeader = { @@ -334,6 +397,52 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); + it("reverts with 'ExitRequestNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { + const intervalInSecondsAfterProofSlot = 1; + + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; + const veboExitRequestTimestamp = proofSlotTimestamp + intervalInSecondsAfterProofSlot; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests( + encodedExitRequestsHash, + [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], + exitRequests, + ); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitRequestNotEligibleOnProvableBeaconBlock"); + + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitRequestNotEligibleOnProvableBeaconBlock"); + }); + it("reverts if the validator proof is incorrect", async () => { const intervalInSecondsBetweenProvableBlockAndExitRequest = 1000; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); From dddc3bba7e3724cc104dab5438a8cb0e287669c8 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 22 May 2025 15:32:29 +0200 Subject: [PATCH 161/405] fix: TriggerableWithdrawalsGateway deployment parameters --- scripts/triggerable-withdrawals/tw-deploy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index d225fd4170..372016f592 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -116,9 +116,9 @@ async function main() { const triggerableWithdrawalsGateway = await deployImplementation( Sk.triggerableWithdrawalsGateway, - "triggerableWithdrawalsGateway", + "TriggerableWithdrawalsGateway", deployer, - [deployer, locator.address], + [deployer, locator.address, 13000, 1, 48], ); log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); log.emptyLine(); From b222ab7123ccfa7f4385eeb4b4f555c3e646e08f Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 22 May 2025 17:43:11 +0400 Subject: [PATCH 162/405] fix: pause coverage --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 7 +- .../validator-exit-bus-oracle.pause.test.ts | 158 ++++++++++++++++++ ...-bus-oracle.submitExitRequestsData.test.ts | 51 ++++++ 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.pause.test.ts diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d5b43b2fbc..68474c965a 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -265,11 +265,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; uint32 lastDeliveredIndex = requestStatus.lastDeliveredExitDataIndex; - // maybe this check is extra - if (requestStatus.deliveryHistoryLength != 0 && lastDeliveredIndex >= totalItemsCount) { - revert DeliveredIndexOutOfBounds(); - } - uint256 startIndex = requestStatus.deliveryHistoryLength == 0 ? 0 : lastDeliveredIndex + 1; uint256 undeliveredItemsCount = totalItemsCount - startIndex; @@ -315,7 +310,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ExitRequestsData calldata exitsData, uint256[] calldata exitDataIndexes, address refundRecipient - ) external payable { + ) external payable whenResumed { if (msg.value == 0) revert ZeroArgument("msg.value"); // If the refund recipient is not set, use the sender as the refund recipient diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.pause.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.pause.test.ts new file mode 100644 index 0000000000..d0d3e0197f --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.pause.test.ts @@ -0,0 +1,158 @@ +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { HashConsensus__Harness, ValidatorsExitBus__Harness } from "typechain-types"; + +import { de0x, numberToHex } from "lib"; + +import { DATA_FORMAT_LIST, deployVEBO, initVEBO } from "test/deploy"; + +// ----------------------------------------------------------------------------- +// Constants & helpers +// ----------------------------------------------------------------------------- + +const LAST_PROCESSING_REF_SLOT = 1; + +const EXIT = [ + { + moduleId: 1, + nodeOpId: 0, + valIndex: 0, + valPubkey: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, +]; + +// ----------------------------------------------------------------------------- +// Encoding +// ----------------------------------------------------------------------------- +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const encodeExitRequestsDataList = (requests: ExitRequest[]) => { + return "0x" + requests.map(encodeExitRequestHex).join(""); +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + +const EXIT_DATA = { dataFormat: DATA_FORMAT_LIST, data: encodeExitRequestsDataList(EXIT) }; +const EXIT_DATA_HASH = hashExitRequest(EXIT_DATA); + +describe("ValidatorsExitBus: pause checks", () => { + let oracle: ValidatorsExitBus__Harness; + let consensus: HashConsensus__Harness; + let admin: HardhatEthersSigner; + let pauser: HardhatEthersSigner; + let resumer: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + beforeEach(async () => { + [admin, pauser, resumer, stranger] = await ethers.getSigners(); + + const deployed = await deployVEBO(admin.address); + oracle = deployed.oracle; + consensus = deployed.consensus; + + await initVEBO({ + admin: admin.address, + oracle, + consensus, + resumeAfterDeploy: true, + lastProcessingRefSlot: LAST_PROCESSING_REF_SLOT, + }); + + await oracle.grantRole(await oracle.SUBMIT_REPORT_HASH_ROLE(), admin); + await oracle.grantRole(await oracle.PAUSE_ROLE(), pauser); + await oracle.grantRole(await oracle.RESUME_ROLE(), resumer); + }); + + it("Should not allow pauseFor call without PAUSE_ROLE", async () => { + await expect(oracle.connect(stranger).pauseFor(60)).to.be.revertedWithOZAccessControlError( + stranger.address, + await oracle.PAUSE_ROLE(), + ); + }); + + it("Should not allow pauseUntil call without PAUSE_ROLE", async () => { + await expect(oracle.connect(stranger).pauseUntil(60)).to.be.revertedWithOZAccessControlError( + stranger.address, + await oracle.PAUSE_ROLE(), + ); + }); + + it("pauseFor(0) → ZeroPauseDuration", async () => { + await expect(oracle.connect(pauser).pauseFor(0)).to.be.revertedWithCustomError(oracle, "ZeroPauseDuration"); + }); + + it("pauseFor blocks protected calls until resumed", async () => { + // pause 1 h + await oracle.connect(pauser).pauseFor(3600); + + await expect(oracle.submitExitRequestsHash(EXIT_DATA_HASH)).to.be.revertedWithCustomError( + oracle, + "ResumedExpected", + ); + await expect(oracle.submitExitRequestsData(EXIT_DATA)).to.be.revertedWithCustomError(oracle, "ResumedExpected"); + + await expect(oracle.triggerExits(EXIT_DATA, [0], ZeroAddress, { value: 4 })).to.be.revertedWithCustomError( + oracle, + "ResumedExpected", + ); + + // stranger can’t resume + await expect(oracle.connect(stranger).resume()).to.be.revertedWithOZAccessControlError( + stranger.address, + await oracle.RESUME_ROLE(), + ); + + // authorised resume + await oracle.connect(resumer).resume(); + + // calls work again + await oracle.submitExitRequestsHash(EXIT_DATA_HASH); + await oracle.submitExitRequestsData(EXIT_DATA); + await oracle.triggerExits(EXIT_DATA, [0], ZeroAddress, { value: 4 }); + }); + + it("second pause while already paused → ResumedExpected", async () => { + await oracle.connect(pauser).pauseFor(100); + await expect(oracle.connect(pauser).pauseFor(100)).to.be.revertedWithCustomError(oracle, "ResumedExpected"); + }); + + it("resume when not paused → PausedExpected", async () => { + await expect(oracle.connect(resumer).resume()).to.be.revertedWithCustomError(oracle, "PausedExpected"); + }); + + it("pauseUntil blocks only until the timestamp", async () => { + const until = 5692495050; + + await oracle.connect(pauser).pauseUntil(until); + + await expect(oracle.submitExitRequestsHash(EXIT_DATA_HASH)).to.be.revertedWithCustomError( + oracle, + "ResumedExpected", + ); + await expect(oracle.submitExitRequestsData(EXIT_DATA)).to.be.revertedWithCustomError(oracle, "ResumedExpected"); + + await expect(oracle.triggerExits(EXIT_DATA, [0], ZeroAddress, { value: 4 })).to.be.revertedWithCustomError( + oracle, + "ResumedExpected", + ); + }); +}); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index fcd7088b70..4d7371454d 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -181,6 +181,46 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .withArgs(2); }); + it("Should revert if contains duplicates", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, + ]; + + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: encodeExitRequestsDataList(requests), + }; + const hash = hashExitRequest(exitRequestData); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataSortOrder", + ); + }); + + it("Should revert if data is not sorted in ascending order", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 0, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, + ]; + + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: encodeExitRequestsDataList(requests), + }; + const hash = hashExitRequest(exitRequestData); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataSortOrder", + ); + }); + describe("Exit Request Limits", function () { before(async () => { const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); @@ -301,6 +341,16 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); + it("Should not give to set new maximum requests per batch value without MAX_VALIDATORS_PER_BATCH_ROLE role", async () => { + const maxRequestsPerBatch = 4; + await expect( + oracle.connect(stranger).setMaxRequestsPerBatch(maxRequestsPerBatch), + ).to.be.revertedWithOZAccessControlError( + await stranger.getAddress(), + await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(), + ); + }); + it("Should revert if maxBatchSize exceeded", async () => { const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); await oracle.grantRole(role, authorizedEntity); @@ -308,6 +358,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const maxRequestsPerBatch = 4; await oracle.connect(authorizedEntity).setMaxRequestsPerBatch(maxRequestsPerBatch); + expect(await oracle.connect(authorizedEntity).getMaxRequestsPerBatch()).to.equal(maxRequestsPerBatch); const exitRequestsRandom = [ { moduleId: 100, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, From 96bccb3daab30f25c407e31f75df33f40a77b6d5 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 22 May 2025 17:08:17 +0200 Subject: [PATCH 163/405] feat: add withdrawal vault pause tests --- .../withdrawalVault/withdrawalVault.test.ts | 340 ++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 2d7e163fa4..76f7423bf4 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -17,12 +17,16 @@ import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, p import { Snapshot } from "test/suite"; +import { advanceChainTime, getCurrentBlockTimestamp } from "../../../lib/time"; + import { encodeEIP7002Payload, findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; import { generateWithdrawalRequestPayload } from "./utils"; const PETRIFIED_VERSION = MAX_UINT256; const ADD_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_WITHDRAWAL_REQUEST_ROLE"); +const PAUSE_ROLE = streccak("PAUSE_ROLE"); +const RESUME_ROLE = streccak("RESUME_ROLE"); describe("WithdrawalVault.sol", () => { let owner: HardhatEthersSigner; @@ -607,4 +611,340 @@ describe("WithdrawalVault.sol", () => { }); }); }); + + context("pausable until", () => { + beforeEach(async () => { + // Initialize the vault and set up necessary roles + await vault.initialize(owner); + await vault.connect(owner).grantRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + await vault.connect(owner).grantRole(PAUSE_ROLE, owner); + await vault.connect(owner).grantRole(RESUME_ROLE, owner); + }); + + context("resume", () => { + it("should revert if the sender does not have the RESUME_ROLE", async () => { + // First pause the contract + await vault.connect(owner).pauseFor(1000n); + + // Try to resume without the RESUME_ROLE + await expect(vault.connect(stranger).resume()).to.be.revertedWithOZAccessControlError( + stranger.address, + RESUME_ROLE, + ); + }); + + it("should revert if the contract is not paused", async () => { + // Contract is initially not paused + await expect(vault.connect(owner).resume()).to.be.revertedWithCustomError(vault, "PausedExpected"); + }); + + it("should resume the contract when paused and emit Resumed event", async () => { + // First pause the contract + await vault.connect(owner).pauseFor(1000n); + expect(await vault.isPaused()).to.equal(true); + + // Resume the contract + await expect(vault.connect(owner).resume()).to.emit(vault, "Resumed"); + + // Verify contract is resumed + expect(await vault.isPaused()).to.equal(false); + }); + + it("should allow withdrawal requests after resuming", async () => { + const requestCount = 1; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // First pause and then resume the contract + await vault.connect(owner).pauseFor(1000n); + await vault.connect(owner).resume(); + + // Should be able to add withdrawal requests + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + pubkeys, + mixedWithdrawalAmounts, + expectedFee, + ); + }); + }); + + context("pauseFor", () => { + it("should revert if the sender does not have the PAUSE_ROLE", async () => { + await expect(vault.connect(stranger).pauseFor(1000n)).to.be.revertedWithOZAccessControlError( + stranger.address, + PAUSE_ROLE, + ); + }); + + it("should revert if the contract is already paused", async () => { + // First pause the contract + await vault.connect(owner).pauseFor(1000n); + + // Try to pause again + await expect(vault.connect(owner).pauseFor(500n)).to.be.revertedWithCustomError(vault, "ResumedExpected"); + }); + + it("should revert if pause duration is zero", async () => { + await expect(vault.connect(owner).pauseFor(0n)).to.be.revertedWithCustomError(vault, "ZeroPauseDuration"); + }); + + it("should pause the contract for the specified duration and emit Paused event", async () => { + await expect(vault.connect(owner).pauseFor(1000n)).to.emit(vault, "Paused").withArgs(1000n); + + expect(await vault.isPaused()).to.equal(true); + }); + + it("should pause the contract indefinitely with PAUSE_INFINITELY", async () => { + const pauseInfinitely = await vault.PAUSE_INFINITELY(); + + // Pause the contract indefinitely + await expect(vault.connect(owner).pauseFor(pauseInfinitely)).to.emit(vault, "Paused").withArgs(pauseInfinitely); + + // Verify contract is paused + expect(await vault.isPaused()).to.equal(true); + + // Advance time significantly + await advanceChainTime(1_000_000_000n); + + // Contract should still be paused + expect(await vault.isPaused()).to.equal(true); + }); + + it("should automatically resume after the pause duration passes", async () => { + // Pause the contract for 100 seconds + await vault.connect(owner).pauseFor(100n); + expect(await vault.isPaused()).to.equal(true); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Contract should be automatically resumed + expect(await vault.isPaused()).to.equal(false); + }); + + it("should prevent withdrawal requests while paused", async () => { + const requestCount = 1; + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Pause the contract + await vault.connect(owner).pauseFor(1000n); + + // Try to add withdrawal request while paused + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + ).to.be.revertedWithCustomError(vault, "ResumedExpected"); + }); + }); + + context("pauseUntil", () => { + it("should revert if the sender does not have the PAUSE_ROLE", async () => { + const timestamp = await getCurrentBlockTimestamp(); + await expect(vault.connect(stranger).pauseUntil(timestamp + 1000n)).to.be.revertedWithOZAccessControlError( + stranger.address, + PAUSE_ROLE, + ); + }); + + it("should revert if the contract is already paused", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // First pause the contract + await vault.connect(owner).pauseFor(1000n); + + // Try to pause again with pauseUntil + await expect(vault.connect(owner).pauseUntil(timestamp + 500n)).to.be.revertedWithCustomError( + vault, + "ResumedExpected", + ); + }); + + it("should revert if timestamp is in the past", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // Try to pause until a past timestamp + await expect(vault.connect(owner).pauseUntil(timestamp - 100n)).to.be.revertedWithCustomError( + vault, + "PauseUntilMustBeInFuture", + ); + }); + + it("should pause the contract until the specified timestamp and emit Paused event", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // Pause the contract until timestamp + 1000 + await expect(vault.connect(owner).pauseUntil(timestamp + 1000n)).to.emit(vault, "Paused"); + + // Verify contract is paused + expect(await vault.isPaused()).to.equal(true); + }); + + it("should pause the contract indefinitely with PAUSE_INFINITELY", async () => { + const pauseInfinitely = await vault.PAUSE_INFINITELY(); + + // Pause the contract indefinitely + await expect(vault.connect(owner).pauseUntil(pauseInfinitely)).to.emit(vault, "Paused"); + + // Verify contract is paused + expect(await vault.isPaused()).to.equal(true); + + // Advance time significantly + await advanceChainTime(100000n); + + // Contract should still be paused + expect(await vault.isPaused()).to.equal(true); + }); + + it("should automatically resume after the pause timestamp passes", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // Pause the contract until timestamp + 100 + await vault.connect(owner).pauseUntil(timestamp + 100n); + expect(await vault.isPaused()).to.equal(true); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Contract should be automatically resumed + expect(await vault.isPaused()).to.equal(false); + }); + }); + + context("Interaction with addWithdrawalRequests", () => { + it("pauseFor: should prevent withdrawal requests immediately after pausing", async () => { + const requestCount = 1; + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Initially contract should be resumed + expect(await vault.isPaused()).to.equal(false); + + // Pause the contract + await vault.connect(owner).pauseFor(1000n); + + // Attempt to add withdrawal request should fail + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + ).to.be.revertedWithCustomError(vault, "ResumedExpected"); + }); + + it("pauseUntil: should prevent withdrawal requests immediately after pausing", async () => { + const requestCount = 1; + const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Initially contract should be resumed + expect(await vault.isPaused()).to.equal(false); + + // Pause the contract + const timestamp = await getCurrentBlockTimestamp(); + await vault.connect(owner).pauseUntil(timestamp + 100n); + + // Attempt to add withdrawal request should fail + await expect( + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + ).to.be.revertedWithCustomError(vault, "ResumedExpected"); + }); + + it("pauseFor: should allow withdrawal requests immediately after resuming", async () => { + const requestCount = 1; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Pause and then resume the contract + await vault.connect(owner).pauseFor(1000n); + await vault.connect(owner).resume(); + + // Should be able to add withdrawal requests immediately + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + pubkeys, + mixedWithdrawalAmounts, + expectedFee, + ); + }); + + it("pauseUntil: should allow withdrawal requests immediately after resuming", async () => { + const requestCount = 1; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Pause and then resume the contract + const timestamp = await getCurrentBlockTimestamp(); + await vault.connect(owner).pauseUntil(timestamp + 100n); + await vault.connect(owner).resume(); + + // Should be able to add withdrawal requests immediately + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + pubkeys, + mixedWithdrawalAmounts, + expectedFee, + ); + }); + + it("pauseFor: should allow withdrawal requests after pause duration automatically expires", async () => { + const requestCount = 1; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Pause for 100 seconds + await vault.connect(owner).pauseFor(100n); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Should be able to add withdrawal requests after pause expires + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + pubkeys, + mixedWithdrawalAmounts, + expectedFee, + ); + }); + + it("pauseUntil: should allow withdrawal requests after pause duration automatically expires", async () => { + const requestCount = 1; + const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const expectedFee = await getFee(); + + // Pause for 100 seconds + const timestamp = await getCurrentBlockTimestamp(); + await vault.connect(owner).pauseUntil(timestamp + 100n); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Should be able to add withdrawal requests after pause expires + await testEIP7002Mock( + () => + vault + .connect(validatorsExitBus) + .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + pubkeys, + mixedWithdrawalAmounts, + expectedFee, + ); + }); + }); + }); }); From e2e7fb33ff15152488c953ed60b0cc8531bf3b24 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 22 May 2025 17:55:10 +0200 Subject: [PATCH 164/405] feat: remove duplicated withdrawal vault test --- .../withdrawalVault/withdrawalVault.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 76f7423bf4..51ca22ec91 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -725,22 +725,6 @@ describe("WithdrawalVault.sol", () => { // Contract should be automatically resumed expect(await vault.isPaused()).to.equal(false); }); - - it("should prevent withdrawal requests while paused", async () => { - const requestCount = 1; - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Pause the contract - await vault.connect(owner).pauseFor(1000n); - - // Try to add withdrawal request while paused - await expect( - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), - ).to.be.revertedWithCustomError(vault, "ResumedExpected"); - }); }); context("pauseUntil", () => { From a473b8b16eb9fc779154f148a0ead5f1295249b6 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 23 May 2025 00:25:19 +0400 Subject: [PATCH 165/405] fix: cover length & contract version check --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 5 + .../contracts/ValidatorsExitBus__Harness.sol | 4 + ...-bus-oracle.submitExitRequestsData.test.ts | 319 +++++++++++------- ...dator-exit-bus-oracle.triggerExits.test.ts | 130 ++++++- 4 files changed, 337 insertions(+), 121 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 68474c965a..32610bc0c3 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -331,8 +331,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa memory triggerableExitData = new ITriggerableWithdrawalsGateway.ValidatorData[](exitDataIndexes.length); uint256 lastExitDataIndex = type(uint256).max; + uint256 requestsCount = exitsData.data.length / PACKED_REQUEST_LENGTH; for (uint256 i = 0; i < exitDataIndexes.length; i++) { + if (exitDataIndexes[i] >= requestsCount ) { + revert ExitDataIndexOutOfRange(exitDataIndexes[i], requestsCount); + } + if (exitDataIndexes[i] > requestStatus.lastDeliveredExitDataIndex) { revert ExitDataWasNotDelivered(exitDataIndexes[i], requestStatus.lastDeliveredExitDataIndex); } diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index c2ceb1145f..6b5834bc8b 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -62,4 +62,8 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { ) external { _storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); } + + function setContractVersion(uint256 version) external { + _updateContractVersion(version); + } } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 4d7371454d..d661a4e335 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -87,145 +87,195 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); }; - before(async () => { - [admin, authorizedEntity, stranger] = await ethers.getSigners(); + describe("Common case", () => { + // tests in this section related to ExitRequestsData mistakes + // also here we tests successfull case - await deploy(); - }); + before(async () => { + [admin, authorizedEntity, stranger] = await ethers.getSigners(); - it("Initially, report was not submitted", async () => { - await expect(oracle.submitExitRequestsData(exitRequest)) - .to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted") - .withArgs(); - }); + await deploy(); + }); - it("Should revert without SUBMIT_REPORT_HASH_ROLE role", async () => { - await expect( - oracle.connect(stranger).submitExitRequestsHash(exitRequestHash), - ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.SUBMIT_REPORT_HASH_ROLE()); - }); + it("Initially, report was not submitted", async () => { + await expect(oracle.submitExitRequestsData(exitRequest)) + .to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted") + .withArgs(); + }); - it("Should store exit hash for authorized entity", async () => { - const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + it("Should revert without SUBMIT_REPORT_HASH_ROLE role", async () => { + await expect( + oracle.connect(stranger).submitExitRequestsHash(exitRequestHash), + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), await oracle.SUBMIT_REPORT_HASH_ROLE()); + }); - await oracle.grantRole(role, authorizedEntity); + it("Should store exit hash for authorized entity", async () => { + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); - const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); + await oracle.grantRole(role, authorizedEntity); - await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); - }); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); - it("Should revert if hash was already submitted", async () => { - await expect( - oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash), - ).to.be.revertedWithCustomError(oracle, "ExitHashAlreadySubmitted"); - }); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); + }); - it("Emit ValidatorExit event", async () => { - const emitTx = await oracle.submitExitRequestsData(exitRequest); - const timestamp = await oracle.getTime(); - - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[0].moduleId, - exitRequests[0].nodeOpId, - exitRequests[0].valIndex, - exitRequests[0].valPubkey, - timestamp, - ); + it("Should revert if hash was already submitted", async () => { + await expect( + oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash), + ).to.be.revertedWithCustomError(oracle, "ExitHashAlreadySubmitted"); + }); - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[1].moduleId, - exitRequests[1].nodeOpId, - exitRequests[1].valIndex, - exitRequests[1].valPubkey, - timestamp, - ); + it("Emit ValidatorExit event", async () => { + const emitTx = await oracle.submitExitRequestsData(exitRequest); + const timestamp = await oracle.getTime(); - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[2].moduleId, - exitRequests[2].nodeOpId, - exitRequests[2].valIndex, - exitRequests[2].valPubkey, - timestamp, - ); + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[0].moduleId, + exitRequests[0].nodeOpId, + exitRequests[0].valIndex, + exitRequests[0].valPubkey, + timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[1].moduleId, + exitRequests[1].nodeOpId, + exitRequests[1].valIndex, + exitRequests[1].valPubkey, + timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[2].moduleId, + exitRequests[2].nodeOpId, + exitRequests[2].valIndex, + exitRequests[2].valPubkey, + timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[3].moduleId, + exitRequests[3].nodeOpId, + exitRequests[3].valIndex, + exitRequests[3].valPubkey, + timestamp, + ); + }); + + it("Should revert if wrong DATA_FORMAT", async () => { + const exitRequestWrongDataFormat: ExitRequestData = { + dataFormat: 2, + data: encodeExitRequestsDataList(exitRequests), + }; + const hash = hashExitRequest(exitRequestWrongDataFormat); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - exitRequests[3].moduleId, - exitRequests[3].nodeOpId, - exitRequests[3].valIndex, - exitRequests[3].valPubkey, - timestamp, + await expect(oracle.submitExitRequestsData(exitRequestWrongDataFormat)) + .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") + .withArgs(2); + }); + + it("Should revert if contains duplicates", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, + ]; + + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: encodeExitRequestsDataList(requests), + }; + const hash = hashExitRequest(exitRequestData); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataSortOrder", ); - }); + }); - it("Should revert if wrong DATA_FORMAT", async () => { - const exitRequestWrongDataFormat: ExitRequestData = { - dataFormat: 2, - data: encodeExitRequestsDataList(exitRequests), - }; - const hash = hashExitRequest(exitRequestWrongDataFormat); - const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + it("Should revert if data is not sorted in ascending order", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 0, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, + ]; - await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: encodeExitRequestsDataList(requests), + }; + const hash = hashExitRequest(exitRequestData); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); - await expect(oracle.submitExitRequestsData(exitRequestWrongDataFormat)) - .to.be.revertedWithCustomError(oracle, "UnsupportedRequestsDataFormat") - .withArgs(2); - }); + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataSortOrder", + ); + }); - it("Should revert if contains duplicates", async () => { - const requests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, - ]; + it("Should revert with if length of requests is equal to 0", async () => { + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: "0x", + }; + const hash = hashExitRequest(exitRequestData); - const exitRequestData: ExitRequestData = { - dataFormat: 1, - data: encodeExitRequestsDataList(requests), - }; - const hash = hashExitRequest(exitRequestData); - const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); - await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); - await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( - oracle, - "InvalidRequestsDataSortOrder", - ); - }); + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataLength", + ); + }); - it("Should revert if data is not sorted in ascending order", async () => { - const requests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 0, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, - ]; + it("Should revert with if length of requests is equal to 0", async () => { + // 64 - length of request in bytes + const request = + "0x00000100000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".slice( + 0, + 2 + 64 * 2 - 4, + ); + + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: request, + }; + const hash = hashExitRequest(exitRequestData); - const exitRequestData: ExitRequestData = { - dataFormat: 1, - data: encodeExitRequestsDataList(requests), - }; - const hash = hashExitRequest(exitRequestData); - const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); - await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); - await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( - oracle, - "InvalidRequestsDataSortOrder", - ); + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsDataLength", + ); + }); }); - describe("Exit Request Limits", function () { + describe("Exit Request Limits", () => { before(async () => { - const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); - await oracle.grantRole(role, authorizedEntity); + [admin, authorizedEntity, stranger] = await ethers.getSigners(); + + await deploy(); + const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(reportLimitRole, authorizedEntity); await consensus.advanceTimeBy(24 * 60 * 60); + + const submitRole = await oracle.SUBMIT_REPORT_HASH_ROLE(); + await oracle.grantRole(submitRole, authorizedEntity); }); // ----------------------------------------------------------------------------- @@ -441,4 +491,47 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); }); }); + + describe("Version changed", () => { + // version can be changed during deploy + // but we will change it via accessing storage + + const VALIDATORS: ExitRequest[] = [{ moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }]; + + const REQUEST = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(VALIDATORS), + }; + + const HASH_REQUEST = hashExitRequest(REQUEST); + + before(async () => { + [admin, authorizedEntity, stranger] = await ethers.getSigners(); + + await deploy(); + + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + await oracle.grantRole(role, authorizedEntity); + }); + + it("Check version", async () => { + // set in initialize in deployVEBO + expect(await oracle.getContractVersion()).to.equal(2); + }); + + it("Store exit hash", async () => { + await oracle.connect(authorizedEntity).submitExitRequestsHash(HASH_REQUEST); + }); + + it("set new version", async () => { + await oracle.setContractVersion(3); + expect(await oracle.getContractVersion()).to.equal(3); + }); + + it("Should revert if request has old contract version", async () => { + await expect(oracle.submitExitRequestsData(REQUEST)) + .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") + .withArgs(3, 2); + }); + }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index b1f460944b..977662531e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -114,12 +114,6 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await consensus.addMember(member3, 2); }; - before(async () => { - [admin, member1, member2, member3, authorizedEntity] = await ethers.getSigners(); - - await deploy(); - }); - const triggerConsensusOnHash = async (hash: string) => { const { refSlot } = await consensus.getCurrentFrame(); await consensus.connect(member1).submitReport(refSlot, hash, CONSENSUS_VERSION); @@ -138,6 +132,12 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let reportFields: ReportFields; let reportHash: string; + before(async () => { + [admin, member1, member2, member3, authorizedEntity] = await ethers.getSigners(); + + await deploy(); + }); + it("some time passes", async () => { await consensus.advanceTimeBy(24 * 60 * 60); }); @@ -226,8 +226,8 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { value: 2, }), ) - .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") // TODO: fix in code return "ExitDataIndexOutOfRange") - .withArgs(5, 3); // 4 + .to.be.revertedWithCustomError(oracle, "ExitDataIndexOutOfRange") + .withArgs(5, 4); }); it("should revert with an error if the key index array contains duplicates", async () => { @@ -249,9 +249,14 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { // the only difference in this checks, is that it is possible to get DeliveryWasNotStarted error because of partial delivery describe("Submit via trustfull method", () => { + const MAX_EXIT_REQUESTS_LIMIT = 2; + const EXITS_PER_FRAME = 1; + const FRAME_DURATION = 48; + const exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, ]; const exitRequest = { @@ -261,6 +266,12 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { const exitRequestHash: string = hashExitRequest(exitRequest); + before(async () => { + [admin, member1, member2, member3, authorizedEntity] = await ethers.getSigners(); + + await deploy(); + }); + it("Should store exit hash for authorized entity", async () => { const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); @@ -281,5 +292,108 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { ), ).to.be.revertedWithCustomError(oracle, "DeliveryWasNotStarted"); }); + + it("Should deliver part of requests", async () => { + // set limit + const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + await oracle.grantRole(reportLimitRole, authorizedEntity); + + await oracle + .connect(authorizedEntity) + .setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION); + + const emitTx = await oracle.submitExitRequestsData(exitRequest); + const timestamp = await oracle.getTime(); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[0].moduleId, + exitRequests[0].nodeOpId, + exitRequests[0].valIndex, + exitRequests[0].valPubkey, + timestamp, + ); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[1].moduleId, + exitRequests[1].nodeOpId, + exitRequests[1].valIndex, + exitRequests[1].valPubkey, + timestamp, + ); + }); + + it("should revert with error if requested index out of range", async () => { + await expect( + oracle.triggerExits({ data: exitRequest.data, dataFormat: exitRequest.dataFormat }, [0, 1, 2], ZERO_ADDRESS, { + value: 4, + }), + ) + .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") + .withArgs(2, 1); + }); + }); + + describe("Version changed", () => { + // version can be changed during deploy + // but we will change it via accessing storage + + const VALIDATORS: ExitRequest[] = [{ moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }]; + + const REQUEST = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(VALIDATORS), + }; + + const HASH_REQUEST = hashExitRequest(REQUEST); + + before(async () => { + [admin, authorizedEntity] = await ethers.getSigners(); + + await deploy(); + + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + await oracle.grantRole(role, authorizedEntity); + }); + + it("Check version", async () => { + // set in initialize in deployVEBO + expect(await oracle.getContractVersion()).to.equal(2); + }); + + it("Store exit hash", async () => { + await oracle.connect(authorizedEntity).submitExitRequestsHash(HASH_REQUEST); + + const emitTx = await oracle.submitExitRequestsData(REQUEST); + const timestamp = await oracle.getTime(); + + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + VALIDATORS[0].moduleId, + VALIDATORS[0].nodeOpId, + VALIDATORS[0].valIndex, + VALIDATORS[0].valPubkey, + timestamp, + ); + }); + + it("set new version", async () => { + await oracle.setContractVersion(3); + expect(await oracle.getContractVersion()).to.equal(3); + }); + + it("Should revert if request has old contract version", async () => { + await expect( + oracle.triggerExits({ data: REQUEST.data, dataFormat: REQUEST.dataFormat }, [0], ZERO_ADDRESS, { + value: 4, + }), + ) + .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") + .withArgs(3, 2); + }); }); }); From ab84df61cb81af38cf8786316ff672b65b3c8053 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 23 May 2025 17:18:40 +0400 Subject: [PATCH 166/405] fix: sanity check for history --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 12 ++++++++++- ...-bus-oracle.submitExitRequestsData.test.ts | 4 ++-- .../oracle/validator-exit-bus.helpers.test.ts | 20 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 32610bc0c3..0512a08d0e 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -291,6 +291,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa newLastDeliveredIndex, _getTimestamp() ); + + _validateDeliveryState(exitRequestsHash, requestStatus); } /** @@ -334,7 +336,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 requestsCount = exitsData.data.length / PACKED_REQUEST_LENGTH; for (uint256 i = 0; i < exitDataIndexes.length; i++) { - if (exitDataIndexes[i] >= requestsCount ) { + if (exitDataIndexes[i] >= requestsCount) { revert ExitDataIndexOutOfRange(exitDataIndexes[i], requestsCount); } @@ -436,6 +438,8 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert ExitHashNotSubmitted(); } + _validateDeliveryState(exitRequestsHash, storedRequest); + if (storedRequest.deliveryHistoryLength == 0) { DeliveryHistory[] memory deliveryHistory; return deliveryHistory; @@ -649,6 +653,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } + function _validateDeliveryState(bytes32 hash, RequestStatus storage status) internal view { + if (status.deliveryHistoryLength > 1) { + require(_storageDeliveryHistory()[hash].length == status.deliveryHistoryLength, "DeliveryHistoryMismatch"); + } + } + /// Methods for reading data from tightly packed validator exit requests /// Format DATA_FORMAT_LIST = 1; diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index d661a4e335..0d4330d5df 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -225,7 +225,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); - it("Should revert with if length of requests is equal to 0", async () => { + it("Should revert with InvalidRequestsDataLength if length of requests is equal to 0", async () => { const exitRequestData: ExitRequestData = { dataFormat: 1, data: "0x", @@ -241,7 +241,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); - it("Should revert with if length of requests is equal to 0", async () => { + it("Should revert with InvalidRequestsDataLength if length of requests is not divided by request length without remainder", async () => { // 64 - length of request in bytes const request = "0x00000100000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".slice( diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 3ebba6ee2d..623d22bb94 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -191,7 +191,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); }); - it("Returns array with multiple reconrds if deliveryHistoryLength is equal to ", async () => { + it("Returns array with multiple records if deliveryHistoryLength is equal to ", async () => { const exitRequestsHash = keccak256("0x3333"); const deliveryHistoryLength = 2; const timestamp = await oracle.getTime(); @@ -219,5 +219,23 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { expect(secondDelivery.lastDeliveredExitDataIndex).to.equal(1); expect(secondDelivery.timestamp).to.equal(timestamp + 1n); }); + + it("reverts if deliveryHistoryLength > 1 but actual history array is smaller", async () => { + const hash = keccak256("0xdead"); + const contractVersion = 42; + + await oracle.storeNewHashRequestStatus( + hash, + contractVersion, + 2, // deliveryHistoryLength = 2 + 5, + 123456, + ); + + // Only add 1 entry (mismatch) + await oracle.storeDeliveryEntry(hash, 1, 123456); + + await expect(oracle.getExitRequestsDeliveryHistory(hash)).to.be.revertedWith("DeliveryHistoryMismatch"); + }); }); }); From e8827e08ff3b965cc78d1bd9f372a796ae7c7ab8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 25 May 2025 23:31:48 +0400 Subject: [PATCH 167/405] fix: test finalize_v2 in vebo& test deploy twg & dont emit event in twg --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 21 +-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 29 ++- test/0.8.9/contracts/RefundReverter.sol | 8 + ...TriggerableWithdrawalsGateway__Harness.sol | 13 ++ .../contracts/ValidatorsExitBus__Harness.sol | 21 ++- ...idator-exit-bus-oracle.finalize_v2.test.ts | 69 +++++++ ...-bus-oracle.submitExitRequestsData.test.ts | 46 ++++- ...dator-exit-bus-oracle.triggerExits.test.ts | 74 +++++++- .../oracle/validator-exit-bus.helpers.test.ts | 141 +++++++++++++++ ...riggerableWithdrawalGateway.deploy.test.ts | 51 ++++++ ...awalGateway.triggerFullWithdrawals.test.ts | 171 +++++++++++++++--- 11 files changed, 579 insertions(+), 65 deletions(-) create mode 100644 test/0.8.9/contracts/RefundReverter.sol create mode 100644 test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts create mode 100644 test/0.8.9/triggerableWithdrawalGateway.deploy.test.ts diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 58ac8dc0f1..eee72b7bbb 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -55,19 +55,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @notice Thrown when a withdrawal fee refund failed */ error TriggerableWithdrawalFeeRefundFailed(); - /** - * @notice Emitted when an entity with the ADD_FULL_WITHDRAWAL_REQUEST_ROLE requests to process a TWR (triggerable withdrawal request). - * @param stakingModuleId Module id. - * @param nodeOperatorId Operator id. - * @param validatorPubkey Validator public key. - * @param timestamp Block timestamp. - */ - event TriggerableExitRequest( - uint256 indexed stakingModuleId, - uint256 indexed nodeOperatorId, - bytes validatorPubkey, - uint256 timestamp - ); /** * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) @@ -133,8 +120,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param refundRecipient The address that will receive any excess ETH sent for fees. * @param exitType A parameter indicating the type of exit, passed to the Staking Module. * - * Emits `TriggerableExitRequest` event for each validator in list. - * * @notice Reverts if: * - The caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE` * - The total fee value sent is insufficient to cover all provided TW requests. @@ -165,8 +150,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { ValidatorData memory data = triggerableExitsData[i]; _copyPubkey(data.pubkey, pubkeys, i); _notifyStakingModule(data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalFee, exitType); - - emit TriggerableExitRequest(data.stakingModuleId, data.nodeOperatorId, data.pubkey, _getTimestamp()); } _addWithdrawalRequest(requestsCount, withdrawalFee, pubkeys, refundRecipient); @@ -260,7 +243,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { _refundFee(requestsCount * withdrawalFee, refundRecipient); } - function _refundFee(uint256 fee, address recipient) internal returns (uint256) { + function _refundFee(uint256 fee, address recipient) internal { uint256 refund = msg.value - fee; if (refund > 0) { @@ -270,8 +253,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { revert TriggerableWithdrawalFeeRefundFailed(); } } - - return refund; } function _getTimestamp() internal view virtual returns (uint256) { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 0512a08d0e..d898394aeb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -213,6 +213,15 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa // If deliveryHistoryLength == 1, delivery info is read from RequestStatus; otherwise, from DeliveryHistory[]. // Both mappings use the same key (exitRequestsHash). + uint256 public constant EXIT_TYPE = 2; + + /// @dev Ensures the contract’s ETH balance is unchanged. + modifier preservesEthBalance() { + uint256 balanceBeforeCall = address(this).balance - msg.value; + _; + assert(address(this).balance == balanceBeforeCall); + } + constructor(address lidoLocator) { LOCATOR = ILidoLocator(lidoLocator); } @@ -292,6 +301,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _getTimestamp() ); + // TODO: is this check extra? _validateDeliveryState(exitRequestsHash, requestStatus); } @@ -312,7 +322,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ExitRequestsData calldata exitsData, uint256[] calldata exitDataIndexes, address refundRecipient - ) external payable whenResumed { + ) external payable whenResumed preservesEthBalance { if (msg.value == 0) revert ZeroArgument("msg.value"); // If the refund recipient is not set, use the sender as the refund recipient @@ -351,6 +361,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa lastExitDataIndex = exitDataIndexes[i]; ValidatorData memory validatorData = _getValidatorData(exitsData.data, exitDataIndexes[i]); + if (validatorData.moduleId == 0) revert InvalidRequestsData(); triggerableExitData[i] = ITriggerableWithdrawalsGateway.ValidatorData( @@ -362,7 +373,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ITriggerableWithdrawalsGateway(LOCATOR.triggerableWithdrawalsGateway()).triggerFullWithdrawals{ value: msg.value - }(triggerableExitData, refundRecipient, 1); + }(triggerableExitData, refundRecipient, uint8(EXIT_TYPE)); } /** @@ -573,7 +584,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { - require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_EXITS_PER_FRAME"); uint256 timestamp = _getTimestamp(); @@ -734,6 +745,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa // totalling to 64 bytes offset := add(offset, 64) } + + uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + + if (moduleId == 0) { + revert InvalidRequestsData(); + } + // dataWithoutPubkey // MSB <---------------------------------------------------------------------- LSB // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | @@ -743,11 +761,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint64 valIndex = uint64(dataWithoutPubkey); uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); - uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - if (moduleId == 0) { - revert InvalidRequestsData(); - } lastDataWithoutPubkey = dataWithoutPubkey; emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); diff --git a/test/0.8.9/contracts/RefundReverter.sol b/test/0.8.9/contracts/RefundReverter.sol new file mode 100644 index 0000000000..dbd813ab5b --- /dev/null +++ b/test/0.8.9/contracts/RefundReverter.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.9; + +contract RefundReverter { + receive() external payable { + revert("nope"); + } +} diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol index f0abea1363..bdab0a7e6c 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol @@ -24,4 +24,17 @@ contract TriggerableWithdrawalsGateway__Harness is TriggerableWithdrawalsGateway function advanceTimeBy(uint256 timeAdvance) external { _time += timeAdvance; } + + // Wrap internal functions for testing + function refundFee(uint256 fee, address recipient) external payable { + _refundFee(fee, recipient); + } + + // function getCurrentExitLimit() external view returns (uint256) { + // return _getCurrentExitLimit(); + // } + + // function checkExitRequestLimit(uint256 requestsCount) external { + // _checkExitRequestLimit(requestsCount); + // } } diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 6b5834bc8b..0e07d4f4c6 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -64,6 +64,25 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { } function setContractVersion(uint256 version) external { - _updateContractVersion(version); + CONTRACT_VERSION_POSITION.setStorageUint256(version); + } + + function updateRequestStatus( + bytes32 exitRequestHash, + uint256 deliveryHistoryLength, + uint256 lastDeliveredExitDataIndex, + uint256 lastDeliveredExitDataTimestamp + ) external { + RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestHash]; + _updateRequestStatus( + requestStatus, + deliveryHistoryLength, + lastDeliveredExitDataIndex, + lastDeliveredExitDataTimestamp + ); + } + + function getRequestStatus(bytes32 exitRequestHash) external view returns (RequestStatus memory requestStatus) { + requestStatus = _storageRequestStatus()[exitRequestHash]; } } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts new file mode 100644 index 0000000000..99b1681788 --- /dev/null +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts @@ -0,0 +1,69 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { LidoLocator, ValidatorsExitBus__Harness } from "typechain-types"; + +import { CONSENSUS_VERSION, EPOCHS_PER_FRAME, INITIAL_FAST_LANE_LENGTH_SLOTS, SLOTS_PER_EPOCH } from "lib"; + +import { deployLidoLocator } from "test/deploy"; +import { Snapshot } from "test/suite"; + +describe("ValidatorsExitBusOracle.sol:finalizeUpgrade_v2", () => { + let originalState: string; + let locator: LidoLocator; + let oracle: ValidatorsExitBus__Harness; + let admin: HardhatEthersSigner; + + before(async () => { + locator = await deployLidoLocator(); + [admin] = await ethers.getSigners(); + oracle = await ethers.deployContract("ValidatorsExitBus__Harness", [12n, 100n, await locator.getAddress()]); + + const consensus = await ethers.deployContract("HashConsensus__Harness", [ + SLOTS_PER_EPOCH, + 12, + 100n, + EPOCHS_PER_FRAME, + INITIAL_FAST_LANE_LENGTH_SLOTS, + admin, + await oracle.getAddress(), + ]); + + await oracle.initialize(admin, await consensus.getAddress(), CONSENSUS_VERSION, 0, 10, 100, 1, 48); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + // contract version + it("should revert if set wrong version", async () => { + await expect(oracle.finalizeUpgrade_v2(10, 100, 1, 48)).to.be.revertedWithCustomError( + oracle, + "InvalidContractVersionIncrement", + ); + }); + + it("should successfully finalize upgrade", async () => { + await oracle.setContractVersion(1); + + await oracle.finalizeUpgrade_v2(15, 150, 1, 48); + + expect(await oracle.getContractVersion()).to.equal(2); + + const exitRequestLimitData = await oracle.getExitRequestLimitFullInfo(); + expect(exitRequestLimitData.maxExitRequestsLimit).to.equal(150); + expect(exitRequestLimitData.exitsPerFrame).to.equal(1); + expect(exitRequestLimitData.frameDuration).to.equal(48); + + expect(await oracle.getMaxRequestsPerBatch()).to.equal(15); + + // should not allow to run finalizeUpgrade_v2 again + await expect(oracle.finalizeUpgrade_v2(10, 100, 1, 48)).to.be.revertedWithCustomError( + oracle, + "InvalidContractVersionIncrement", + ); + }); +}); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 0d4330d5df..8598d31fe0 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -207,8 +207,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { it("Should revert if data is not sorted in ascending order", async () => { const requests = [ - { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, - { moduleId: 0, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, ]; const exitRequestData: ExitRequestData = { @@ -263,6 +263,26 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { "InvalidRequestsDataLength", ); }); + + it("Should revert if module id is equal to 0", async () => { + const requests = [ + { moduleId: 0, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[1] }, + ]; + + const exitRequestData: ExitRequestData = { + dataFormat: 1, + data: encodeExitRequestsDataList(requests), + }; + const hash = hashExitRequest(exitRequestData); + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(hash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(hash); + + await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( + oracle, + "InvalidRequestsData", + ); + }); }); describe("Exit Request Limits", () => { @@ -301,6 +321,28 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const HASH_REQUEST_DELIVERED_BY_PARTS = hashExitRequest(REQUEST_DELIVERED_BY_PARTS); + it("Should not allow to set limit without role", async () => { + const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + + await expect( + oracle.connect(stranger).setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION), + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), reportLimitRole); + }); + + it("Should not allow to set limit without role", async () => { + const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + + await expect( + oracle.connect(stranger).setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION), + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), reportLimitRole); + }); + + it("Should not allow to set exits per frame bigger than max limit", async () => { + await expect(oracle.connect(authorizedEntity).setExitRequestLimit(10, 12, FRAME_DURATION)).to.be.revertedWith( + "TOO_LARGE_EXITS_PER_FRAME", + ); + }); + it("Should deliver request fully as it is below limit", async () => { const exitLimitTx = await oracle .connect(authorizedEntity) diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 977662531e..60b67263dc 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -92,6 +92,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let member2: HardhatEthersSigner; let member3: HardhatEthersSigner; let authorizedEntity: HardhatEthersSigner; + let stranger: HardhatEthersSigner; const deploy = async () => { const deployed = await deployVEBO(admin.address); @@ -133,7 +134,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { let reportHash: string; before(async () => { - [admin, member1, member2, member3, authorizedEntity] = await ethers.getSigners(); + [admin, member1, member2, member3, authorizedEntity, stranger] = await ethers.getSigners(); await deploy(); }); @@ -174,19 +175,29 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { } }); - it("should triggers exits for all validators in exit request", async () => { + it("should revert with ZeroArgument error if msg.value == 0", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [0], ZERO_ADDRESS, { + value: 0, + }), + ) + .to.be.revertedWithCustomError(oracle, "ZeroArgument") + .withArgs("msg.value"); + }); + + it("should refund fee to recipient address", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, [0, 1, 2, 3], - ZERO_ADDRESS, - { value: 4 }, + stranger, + { value: 6 }, ); const requests = createValidatorDataList(exitRequests); await expect(tx) .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests.length, admin.address, 1); + .withArgs(requests.length, stranger.address, await oracle.EXIT_TYPE()); }); it("should triggers exits only for validators in selected request indexes", async () => { @@ -203,7 +214,23 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await expect(tx) .to.emit(triggerableWithdrawalsGateway, "Mock__triggerFullWithdrawalsTriggered") - .withArgs(requests.length, admin.address, 1); + .withArgs(requests.length, admin.address, await oracle.EXIT_TYPE()); + }); + + it("preserves eth balance when calling triggerExits", async () => { + const ethBefore = await ethers.provider.getBalance(oracle.getAddress()); + + await oracle.triggerExits( + { data: reportFields.data, dataFormat: reportFields.dataFormat }, + [0, 1, 3], + ZERO_ADDRESS, + { + value: 10, + }, + ); + + const ethAfter = await ethers.provider.getBalance(oracle.getAddress()); + expect(ethAfter).to.equal(ethBefore); }); it("should revert with error if the hash of `requestsData` was not previously submitted in the VEB", async () => { @@ -326,7 +353,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { ); }); - it("should revert with error if requested index out of range", async () => { + it("should revert with error if requested index was not delivered yet", async () => { await expect( oracle.triggerExits({ data: exitRequest.data, dataFormat: exitRequest.dataFormat }, [0, 1, 2], ZERO_ADDRESS, { value: 4, @@ -335,6 +362,39 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") .withArgs(2, 1); }); + + it("some time passes", async () => { + await consensus.advanceTimeBy(2 * 48); + }); + + it("should revert with error if module id is equal to 0", async () => { + const requests = [ + { moduleId: 0, nodeOpId: 1, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + ]; + + const request = { + dataFormat: DATA_FORMAT_LIST, + data: encodeExitRequestsDataList(requests), + }; + + const requestHash: string = hashExitRequest(request); + + await oracle.storeNewHashRequestStatus( + requestHash, + 2, + 2, // deliveryHistoryLength = 2 + 1, + 123456, + ); + + await expect( + oracle.triggerExits(request, [0, 1, 2], ZERO_ADDRESS, { + value: 4, + }), + ).to.be.revertedWithCustomError(oracle, "InvalidRequestsData"); + }); }); describe("Version changed", () => { diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 623d22bb94..3ee091707c 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -238,4 +238,145 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { await expect(oracle.getExitRequestsDeliveryHistory(hash)).to.be.revertedWith("DeliveryHistoryMismatch"); }); }); + + context("_updateRequestStatus", () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it("updates fields correctly when valid values provided", async () => { + const hash = keccak256("0xaaaa"); + const contractVersion = 42; + const deliveryHistoryLength = 0; + const lastDeliveredExitDataIndex = 0; + const timestamp = 0; + + await oracle.storeNewHashRequestStatus( + hash, + contractVersion, + deliveryHistoryLength, + lastDeliveredExitDataIndex, + timestamp, + ); + + const newDeliveryHistoryLength = 10; + const newLastDeliveredExitDataIndex = 100; + const newLastDeliveredExitDataTimestamp = 12345; + + await oracle.updateRequestStatus( + hash, + newDeliveryHistoryLength, + newLastDeliveredExitDataIndex, + newLastDeliveredExitDataTimestamp, + ); + + await expect( + oracle.updateRequestStatus( + hash, + newDeliveryHistoryLength, + newLastDeliveredExitDataIndex, + newLastDeliveredExitDataTimestamp, + ), + ).to.not.be.reverted; + + const requestStatus = await oracle.getRequestStatus(hash); + expect(requestStatus.deliveryHistoryLength).to.equal(newDeliveryHistoryLength); + expect(requestStatus.lastDeliveredExitDataIndex).to.equal(newLastDeliveredExitDataIndex); + expect(requestStatus.lastDeliveredExitDataTimestamp).to.equal(newLastDeliveredExitDataTimestamp); + }); + + it("reverts if deliveryHistoryLength exceeds uint32 max", async () => { + const hash = keccak256("0xbbbb"); + await expect(oracle.updateRequestStatus(hash, 2n ** 32n, 0, 0)).to.be.revertedWith( + "DELIVERY_HISTORY_LENGTH_OVERFLOW", + ); + }); + + it("reverts if lastDeliveredExitDataIndex exceeds uint32 max", async () => { + const hash = keccak256("0xcccc"); + await expect(oracle.updateRequestStatus(hash, 0, 2n ** 32n, 0)).to.be.revertedWith( + "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW", + ); + }); + + it("reverts if lastDeliveredExitDataTimestamp exceeds uint32 max", async () => { + const hash = keccak256("0xdddd"); + await expect(oracle.updateRequestStatus(hash, 0, 0, 2n ** 32n)).to.be.revertedWith( + "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW", + ); + }); + }); + + context("storeDeliveryEntry", () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + }); + + after(async () => await Snapshot.restore(originalState)); + + it("adds a delivery entry to an empty history", async () => { + const exitRequestsHash = keccak256("0x1111"); + const lastDeliveredExitDataIndex = 0; + const lastDeliveredExitDataTimestamp = 123456; + + await oracle.storeNewHashRequestStatus( + exitRequestsHash, + 1, + 1, + lastDeliveredExitDataIndex, + lastDeliveredExitDataTimestamp, + ); + await oracle.storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); + + const history = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(history.length).to.equal(1); + expect(history[0].lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); + expect(history[0].timestamp).to.equal(lastDeliveredExitDataTimestamp); + }); + + it("appends multiple entries for the same hash", async () => { + const exitRequestsHash = keccak256("0x2222"); + + const lastDeliveredExitDataIndex = 1; + const lastDeliveredExitDataTimestamp = 12345; + const historyLength = 2; + + await oracle.storeNewHashRequestStatus( + exitRequestsHash, + 1, + historyLength, + lastDeliveredExitDataIndex, + lastDeliveredExitDataTimestamp, + ); + + await oracle.storeDeliveryEntry(exitRequestsHash, 0, lastDeliveredExitDataTimestamp - 1); + await oracle.storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); + + const history = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(history.length).to.equal(2); + + expect(history[0].lastDeliveredExitDataIndex).to.equal(0); + expect(history[0].timestamp).to.equal(lastDeliveredExitDataTimestamp - 1); + }); + + it("reverts if lastDeliveredExitDataIndex exceeds uint32 max", async () => { + const exitRequestsHash = keccak256("0x3333"); + await expect(oracle.storeDeliveryEntry(exitRequestsHash, 2n ** 32n, 0)).to.be.revertedWith( + "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW", + ); + }); + + it("reverts if timestamp exceeds uint32 max", async () => { + const exitRequestsHash = keccak256("0x4444"); + await expect(oracle.storeDeliveryEntry(exitRequestsHash, 0, 2n ** 32n)).to.be.revertedWith( + "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW", + ); + }); + }); }); diff --git a/test/0.8.9/triggerableWithdrawalGateway.deploy.test.ts b/test/0.8.9/triggerableWithdrawalGateway.deploy.test.ts new file mode 100644 index 0000000000..9afd57bfe5 --- /dev/null +++ b/test/0.8.9/triggerableWithdrawalGateway.deploy.test.ts @@ -0,0 +1,51 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { StakingRouter__MockForTWG, WithdrawalVault__MockForTWG } from "typechain-types"; + +import { deployLidoLocator, updateLidoLocatorImplementation } from "../deploy/locator"; + +describe("TriggerableWithdrawalsGateway.sol: deployment", () => { + let withdrawalVault: WithdrawalVault__MockForTWG; + let stakingRouter: StakingRouter__MockForTWG; + + before(async () => { + const locator = await deployLidoLocator(); + const locatorAddr = await locator.getAddress(); + + withdrawalVault = await ethers.deployContract("WithdrawalVault__MockForTWG"); + stakingRouter = await ethers.deployContract("StakingRouter__MockForTWG"); + + await updateLidoLocatorImplementation(locatorAddr, { + withdrawalVault: await withdrawalVault.getAddress(), + stakingRouter: await stakingRouter.getAddress(), + }); + }); + + it("should deploy successfully with valid admin", async () => { + const [admin] = await ethers.getSigners(); + const locatorAddr = (await deployLidoLocator()).getAddress(); + + const gateway = await ethers.deployContract("TriggerableWithdrawalsGateway__Harness", [ + admin.address, + locatorAddr, + 100, + 1, + 48, + ]); + + const adminRole = await gateway.DEFAULT_ADMIN_ROLE(); + expect(await gateway.hasRole(adminRole, admin.address)).to.be.true; + }); + + it("should revert if admin is zero address", async () => { + const locatorAddr = (await deployLidoLocator()).getAddress(); + + await expect( + ethers.deployContract("TriggerableWithdrawalsGateway__Harness", [ethers.ZeroAddress, locatorAddr, 100, 1, 48]), + ).to.be.revertedWithCustomError( + await ethers.getContractFactory("TriggerableWithdrawalsGateway__Harness"), + "AdminCannotBeZero", + ); + }); +}); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index b98d8874cb..5afc74494e 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -40,6 +40,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { let stakingRouter: StakingRouter__MockForTWG; let admin: HardhatEthersSigner; let authorizedEntity: HardhatEthersSigner; + let stranger: HardhatEthersSigner; const createValidatorDataList = (requests: ExitRequest[]) => { return requests.map((request) => ({ @@ -50,7 +51,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }; before(async () => { - [admin, authorizedEntity] = await ethers.getSigners(); + [admin, authorizedEntity, stranger] = await ethers.getSigners(); const locator = await deployLidoLocator(); const locatorAddr = await locator.getAddress(); @@ -70,23 +71,33 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { 1, 48, ]); + + const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); }); it("should revert if caller does not have the `ADD_FULL_WITHDRAWAL_REQUEST_ROLE", async () => { const requests = createValidatorDataList(exitRequests); const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await expect( + triggerableWithdrawalsGateway.connect(stranger).triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 10 }), + ).to.be.revertedWithOZAccessControlError(stranger.address, role); + }); + + it("should revert with ZeroArgument error if msg.value == 0", async () => { + const requests = createValidatorDataList(exitRequests); + await expect( triggerableWithdrawalsGateway .connect(authorizedEntity) - .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 10 }), - ).to.be.revertedWithOZAccessControlError(await authorizedEntity.getAddress(), role); + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 0 }), + ) + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ZeroArgument") + .withArgs("msg.value"); }); it("should revert if total fee value sent is insufficient to cover all provided TW requests ", async () => { - const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); - await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); - const requests = createValidatorDataList(exitRequests); await expect( @@ -98,6 +109,14 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .withArgs(3, 1); }); + it("should not allow to set limit without role TW_EXIT_REPORT_LIMIT_ROLE", async () => { + const reportLimitRole = await triggerableWithdrawalsGateway.TW_EXIT_REPORT_LIMIT_ROLE(); + + await expect( + triggerableWithdrawalsGateway.connect(stranger).setExitRequestLimit(4, 1, 48), + ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), reportLimitRole); + }); + it("set limit", async () => { const role = await triggerableWithdrawalsGateway.TW_EXIT_REPORT_LIMIT_ROLE(); await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); @@ -125,10 +144,6 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .join(""); for (const request of exitRequests) { - await expect(tx) - .to.emit(triggerableWithdrawalsGateway, "TriggerableExitRequest") - .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, timestamp); - await expect(tx) .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, 1, 0); @@ -166,18 +181,6 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .withArgs(3, 1); }); - it("should revert if limit doesnt cover requests count", async () => { - const requests = createValidatorDataList(exitRequests); - - await expect( - triggerableWithdrawalsGateway - .connect(authorizedEntity) - .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }), - ) - .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ExitRequestsLimit") - .withArgs(3, 1); - }); - it("rewind time", async () => { await triggerableWithdrawalsGateway.advanceTimeBy(2 * 48); }); @@ -198,7 +201,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { expect(data[4]).to.equal(3); }); - it("should add withdrawal request ias limit is enough for processing all requests", async () => { + it("should add withdrawal request as limit is enough for processing all requests", async () => { const requests = createValidatorDataList(exitRequests); const tx = await triggerableWithdrawalsGateway @@ -217,10 +220,6 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .join(""); for (const request of exitRequests) { - await expect(tx) - .to.emit(triggerableWithdrawalsGateway, "TriggerableExitRequest") - .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, timestamp); - await expect(tx) .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, 1, 0); @@ -228,4 +227,122 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(pubkeys); } }); + + it("rewind time", async () => { + await triggerableWithdrawalsGateway.advanceTimeBy(3 * 48); + }); + + it("should refund fee to recipient address", async () => { + const prevBalance = await ethers.provider.getBalance(stranger); + const requests = createValidatorDataList(exitRequests); + + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, stranger, 0, { value: 3 + 7 }); + + const newBalance = await ethers.provider.getBalance(stranger); + + expect(newBalance).to.equal(prevBalance + 7n); + }); + + it("rewind time", async () => { + await triggerableWithdrawalsGateway.advanceTimeBy(3 * 48); + }); + + it("should refund fee to sender address", async () => { + const SENDER_ADDR = authorizedEntity.address; + const prevBalance = await ethers.provider.getBalance(SENDER_ADDR); + + const requests = createValidatorDataList(exitRequests); + + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 3 + 7 }); + + const receipt = await tx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + + const newBalance = await ethers.provider.getBalance(SENDER_ADDR); + expect(newBalance).to.equal(prevBalance - gasUsed - 3n); + }); + + it("rewind time", async () => { + await triggerableWithdrawalsGateway.advanceTimeBy(3 * 48); + }); + + it("preserves eth balance when calling triggerFullWithdrawals", async () => { + const requests = createValidatorDataList(exitRequests); + const refundRecipient = ZERO_ADDRESS; + const exitType = 2; + const ethBefore = await ethers.provider.getBalance(triggerableWithdrawalsGateway.getAddress()); + + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, refundRecipient, exitType, { value: 4 }); + + const ethAfter = await ethers.provider.getBalance(triggerableWithdrawalsGateway.getAddress()); + expect(ethAfter).to.equal(ethBefore); + }); + + it("should not make refund if refund is zero", async () => { + const fee = 10n; + const prevBalance = await ethers.provider.getBalance(authorizedEntity.address); + + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .refundFee(fee, authorizedEntity.address, { value: fee }); + + const receipt = await tx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + + const newBalance = await ethers.provider.getBalance(authorizedEntity.address); + + expect(newBalance).to.equal(prevBalance - gasUsed - fee); + }); + + it("should refund ETH if refund > 0", async () => { + const fee = 6n; + const totalValue = 10n; + const refundRecipient = authorizedEntity; + + const prevBalance = await ethers.provider.getBalance(refundRecipient.address); + + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .refundFee(fee, refundRecipient.address, { value: totalValue }); + + const receipt = await tx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + + const newBalance = await ethers.provider.getBalance(refundRecipient.address); + expect(newBalance).to.equal(prevBalance - gasUsed - fee); + }); + + it("should reverts if recipient refuses ETH", async () => { + const RefundReverterFactory = await ethers.getContractFactory("RefundReverter"); + const refundReverter = await RefundReverterFactory.deploy(); + + await expect( + triggerableWithdrawalsGateway.connect(authorizedEntity).refundFee(5, refundReverter.getAddress(), { value: 10 }), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "TriggerableWithdrawalFeeRefundFailed"); + }); + + it("should set maxExitRequestsLimit equal to 0 and return as currentExitRequestsLimit type(uint256).max", async () => { + const tx = await triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(0, 0, 48); + await expect(tx).to.emit(triggerableWithdrawalsGateway, "ExitRequestsLimitSet").withArgs(0, 0, 48); + + const data = await triggerableWithdrawalsGateway.getExitRequestLimitFullInfo(); + + expect(data.maxExitRequestsLimit).to.equal(0); + expect(data.exitsPerFrame).to.equal(0); + expect(data.frameDuration).to.equal(48); + expect(data.prevExitRequestsLimit).to.equal(0); + expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); + }); + + it("Should not allow to set exitsPerFrame bigger than maxExitRequestsLimit", async () => { + await expect( + triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(0, 1, 48), + ).to.be.revertedWith("TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + }); }); From 926d7f978875a3bb123ac84a1a91d0496b199f0a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 25 May 2025 23:33:54 +0400 Subject: [PATCH 168/405] fix: remove mock functions --- .../contracts/TriggerableWithdrawalsGateway__Harness.sol | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol index bdab0a7e6c..41e54a5000 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol @@ -29,12 +29,4 @@ contract TriggerableWithdrawalsGateway__Harness is TriggerableWithdrawalsGateway function refundFee(uint256 fee, address recipient) external payable { _refundFee(fee, recipient); } - - // function getCurrentExitLimit() external view returns (uint256) { - // return _getCurrentExitLimit(); - // } - - // function checkExitRequestLimit(uint256 requestsCount) external { - // _checkExitRequestLimit(requestsCount); - // } } From 83112ff2dc42d435680ac4faab0f633eae851cb7 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 25 May 2025 23:40:40 +0400 Subject: [PATCH 169/405] fix: lint --- ...riggerableWithdrawalGateway.triggerFullWithdrawals.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 5afc74494e..ed43fd65bd 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -132,8 +132,6 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const timestamp = await triggerableWithdrawalsGateway.getTimestamp(); - const pubkeys = "0x" + exitRequests @@ -208,8 +206,6 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const timestamp = await triggerableWithdrawalsGateway.getTimestamp(); - const pubkeys = "0x" + exitRequests From ac8c545221c3177fa6c997a43f185af8fd03993f Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 25 May 2025 23:57:43 +0400 Subject: [PATCH 170/405] fix: add twg test for unlimited requests --- ...awalGateway.triggerFullWithdrawals.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index ed43fd65bd..791f3230c1 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -336,6 +336,44 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); }); + it("should add unlimited amount of withdrawal requests", async () => { + const requests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 1, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 2, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 4, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 5, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 6, valIndex: 1, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 7, valIndex: 1, valPubkey: PUBKEYS[2] }, + ]; + + const requestData = createValidatorDataList(requests); + + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requestData, ZERO_ADDRESS, 0, { value: 10 }); + + const pubkeys = + "0x" + + requests + .map((request) => { + const pubkeyHex = de0x(request.valPubkey); + return pubkeyHex; + }) + .join(""); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(stakingRouter, "Mock__onValidatorExitTriggered") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey, 1, 0); + + await expect(tx).to.emit(withdrawalVault, "AddFullWithdrawalRequestsCalled").withArgs(pubkeys); + } + }); + it("Should not allow to set exitsPerFrame bigger than maxExitRequestsLimit", async () => { await expect( triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(0, 1, 48), From 7179bd2a10d2906cf8196e9d28cacac0c250884d Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 26 May 2025 18:28:03 +0200 Subject: [PATCH 171/405] refactor: remove support for deprecated EXTRA_DATA_TYPE_STUCK_VALIDATORS and update related tests --- contracts/0.8.9/oracle/AccountingOracle.sol | 18 ++--- lib/oracle.ts | 5 +- .../oracle/accountingOracle.happyPath.test.ts | 41 ------------ .../accountingOracle.submitReport.test.ts | 5 -- ...untingOracle.submitReportExtraData.test.ts | 66 +++++++++++++++---- 5 files changed, 63 insertions(+), 72 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 8225667b25..d26c53ecb2 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -96,6 +96,7 @@ contract AccountingOracle is BaseOracle { error InvalidExitedValidatorsData(); error UnsupportedExtraDataFormat(uint256 format); error UnsupportedExtraDataType(uint256 itemIndex, uint256 dataType); + error DeprecatedExtraDataType(uint256 itemIndex, uint256 dataType); error CannotSubmitExtraDataBeforeMainData(); error ExtraDataAlreadyProcessed(); error UnexpectedExtraDataHash(bytes32 consensusHash, bytes32 receivedHash); @@ -815,9 +816,13 @@ contract AccountingOracle is BaseOracle { iter.itemType = itemType; iter.dataOffset = dataOffset; - if (itemType == EXTRA_DATA_TYPE_EXITED_VALIDATORS || - itemType == EXTRA_DATA_TYPE_STUCK_VALIDATORS - ) { + /// @dev The EXTRA_DATA_TYPE_STUCK_VALIDATORS item type was deprecated in the Triggerable Withdrawals update. + /// The mechanism for handling stuck validator keys is no longer supported and has been removed. + if (itemType == EXTRA_DATA_TYPE_STUCK_VALIDATORS) { + revert DeprecatedExtraDataType(index, itemType); + } + + if (itemType == EXTRA_DATA_TYPE_EXITED_VALIDATORS) { uint256 nodeOpsProcessed = _processExtraDataItem(data, iter); if (nodeOpsProcessed > maxNodeOperatorsPerItem) { @@ -912,13 +917,8 @@ contract AccountingOracle is BaseOracle { revert InvalidExtraDataItem(iter.index); } - if (iter.itemType == EXTRA_DATA_TYPE_STUCK_VALIDATORS) { - IStakingRouter(iter.stakingRouter) - .reportStakingModuleStuckValidatorsCountByNodeOperator(moduleId, nodeOpIds, valuesCounts); - } else { - IStakingRouter(iter.stakingRouter) + IStakingRouter(iter.stakingRouter) .reportStakingModuleExitedValidatorsCountByNodeOperator(moduleId, nodeOpIds, valuesCounts); - } iter.dataOffset = dataOffset; return nodeOpsCount; diff --git a/lib/oracle.ts b/lib/oracle.ts index 8fc8ccefc4..a29c8fc241 100644 --- a/lib/oracle.ts +++ b/lib/oracle.ts @@ -23,7 +23,7 @@ export type OracleReport = AccountingOracle.ReportDataStruct; export type ReportAsArray = ReturnType; export type KeyType = { moduleId: number; nodeOpIds: number[]; keysCounts: number[] }; -export type ExtraDataType = { stuckKeys: KeyType[]; exitedKeys: KeyType[] }; +export type ExtraDataType = { exitedKeys: KeyType[] }; export type ItemType = KeyType & { type: bigint }; @@ -162,7 +162,6 @@ export function encodeExtraDataItems(data: ExtraDataType) { const toItemWithType = (keys: KeyType[], type: bigint) => keys.map((item) => ({ ...item, type })); - itemsWithType.push(...toItemWithType(data.stuckKeys, EXTRA_DATA_TYPE_STUCK_VALIDATORS)); itemsWithType.push(...toItemWithType(data.exitedKeys, EXTRA_DATA_TYPE_EXITED_VALIDATORS)); return encodeExtraDataItemsArray(itemsWithType); @@ -206,7 +205,7 @@ function isItemTypeArray(items: unknown[]): items is ItemType[] { } function isExtraDataType(data: unknown): data is ExtraDataType { - return isObjectType(data) && "stuckKeys" in data && "exitedKeys" in data; + return isObjectType(data) && "exitedKeys" in data; } function isStringArray(items: unknown[]): items is string[] { diff --git a/test/0.8.9/oracle/accountingOracle.happyPath.test.ts b/test/0.8.9/oracle/accountingOracle.happyPath.test.ts index 907c7b9520..0be6542177 100644 --- a/test/0.8.9/oracle/accountingOracle.happyPath.test.ts +++ b/test/0.8.9/oracle/accountingOracle.happyPath.test.ts @@ -124,11 +124,6 @@ describe("AccountingOracle.sol:happyPath", () => { const { refSlot } = await consensus.getCurrentFrame(); extraData = { - stuckKeys: [ - { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - { moduleId: 2, nodeOpIds: [0], keysCounts: [2] }, - { moduleId: 3, nodeOpIds: [2], keysCounts: [3] }, - ], exitedKeys: [ { moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }, { moduleId: 3, nodeOpIds: [1], keysCounts: [2] }, @@ -300,7 +295,6 @@ describe("AccountingOracle.sol:happyPath", () => { it(`an extra data not matching the consensus hash cannot be submitted`, async () => { const invalidExtraData = { - stuckKeys: [...extraData.stuckKeys], exitedKeys: [...extraData.exitedKeys], }; invalidExtraData.exitedKeys[0].keysCounts = [...invalidExtraData.exitedKeys[0].keysCounts]; @@ -355,31 +349,6 @@ describe("AccountingOracle.sol:happyPath", () => { expect(call2.keysCounts).to.equal("0x" + [2].map((i) => numberToHex(i, 16)).join("")); }); - it("Staking router got the stuck keys by node op report", async () => { - const totalReportCalls = await mockStakingRouter.totalCalls_reportStuckKeysByNodeOperator(); - expect(totalReportCalls).to.equal(3); - - const call1 = await mockStakingRouter.calls_reportStuckKeysByNodeOperator(0); - expect(call1.stakingModuleId).to.equal(1); - expect(call1.nodeOperatorIds).to.equal("0x" + [0].map((i) => numberToHex(i, 8)).join("")); - expect(call1.keysCounts).to.equal("0x" + [1].map((i) => numberToHex(i, 16)).join("")); - - const call2 = await mockStakingRouter.calls_reportStuckKeysByNodeOperator(1); - expect(call2.stakingModuleId).to.equal(2); - expect(call2.nodeOperatorIds).to.equal("0x" + [0].map((i) => numberToHex(i, 8)).join("")); - expect(call2.keysCounts).to.equal("0x" + [2].map((i) => numberToHex(i, 16)).join("")); - - const call3 = await mockStakingRouter.calls_reportStuckKeysByNodeOperator(2); - expect(call3.stakingModuleId).to.equal(3); - expect(call3.nodeOperatorIds).to.equal("0x" + [2].map((i) => numberToHex(i, 8)).join("")); - expect(call3.keysCounts).to.equal("0x" + [3].map((i) => numberToHex(i, 16)).join("")); - }); - - it("Staking router was told that stuck and exited keys updating is finished", async () => { - const totalFinishedCalls = await mockStakingRouter.totalCalls_onValidatorsCountsByNodeOperatorReportingFinished(); - expect(totalFinishedCalls).to.equal(1); - }); - it(`extra data for the same reference slot cannot be re-submitted`, async () => { await expect(oracle.connect(member1).submitReportExtraDataList(extraDataList)).to.be.revertedWithCustomError( oracle, @@ -468,16 +437,6 @@ describe("AccountingOracle.sol:happyPath", () => { expect(totalReportCalls).to.equal(2); }); - it(`Staking router didn't get the stuck keys by node op report`, async () => { - const totalReportCalls = await mockStakingRouter.totalCalls_reportStuckKeysByNodeOperator(); - expect(totalReportCalls).to.equal(3); - }); - - it("Staking router was told that stuck and exited keys updating is finished", async () => { - const totalFinishedCalls = await mockStakingRouter.totalCalls_onValidatorsCountsByNodeOperatorReportingFinished(); - expect(totalFinishedCalls).to.equal(2); - }); - it(`extra data for the same reference slot cannot be re-submitted`, async () => { await expect(oracle.connect(member1).submitReportExtraDataEmpty()).to.be.revertedWithCustomError( oracle, diff --git a/test/0.8.9/oracle/accountingOracle.submitReport.test.ts b/test/0.8.9/oracle/accountingOracle.submitReport.test.ts index e2ce7691d3..fb7242533b 100644 --- a/test/0.8.9/oracle/accountingOracle.submitReport.test.ts +++ b/test/0.8.9/oracle/accountingOracle.submitReport.test.ts @@ -86,11 +86,6 @@ describe("AccountingOracle.sol:submitReport", () => { const { refSlot } = await deployed.consensus.getCurrentFrame(); extraData = { - stuckKeys: [ - { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - { moduleId: 2, nodeOpIds: [0], keysCounts: [2] }, - { moduleId: 3, nodeOpIds: [2], keysCounts: [3] }, - ], exitedKeys: [ { moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }, { moduleId: 3, nodeOpIds: [1], keysCounts: [2] }, diff --git a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts index 89351ca035..42f16bbe28 100644 --- a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts +++ b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts @@ -40,11 +40,11 @@ import { deployAndConfigureAccountingOracle } from "test/deploy"; import { Snapshot } from "test/suite"; const getDefaultExtraData = (): ExtraDataType => ({ - stuckKeys: [ - { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - { moduleId: 2, nodeOpIds: [0], keysCounts: [2] }, - { moduleId: 3, nodeOpIds: [2], keysCounts: [3] }, - ], + // stuckKeys: [ + // { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, + // { moduleId: 2, nodeOpIds: [0], keysCounts: [2] }, + // { moduleId: 3, nodeOpIds: [2], keysCounts: [3] }, + // ], exitedKeys: [ { moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }, { moduleId: 3, nodeOpIds: [1], keysCounts: [2] }, @@ -218,7 +218,37 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { }); }); - context("submitReportExtraDataList", () => { + context("deprecated invariants", () => { + it("reverts when trying to submit deprecated EXTRA_DATA_TYPE_STUCK_VALIDATORS", async () => { + await consensus.advanceTimeToNextFrameStart(); + + // Manually encode a STUCK_VALIDATORS item + const invalidItem = encodeExtraDataItem(0, EXTRA_DATA_TYPE_STUCK_VALIDATORS, 1, [1], [1]); + + const extraDataList = packExtraDataList([invalidItem]); + const extraDataHash = calcExtraDataListHash(extraDataList); + + const reportFields = getDefaultReportFields({ + extraDataHash, + extraDataFormat: EXTRA_DATA_FORMAT_LIST, + extraDataItemsCount: 1, + refSlot: (await consensus.getCurrentFrame()).refSlot, + }); + + const reportHash = calcReportDataHash(getReportDataItems(reportFields)); + + // Submit the report hash and data + await consensus.connect(member1).submitReport(reportFields.refSlot, reportHash, CONSENSUS_VERSION); + await oracle.connect(member1).submitReportData(reportFields, oracleVersion); + + // Verify it reverts with DeprecatedExtraDataType + await expect(oracle.connect(member1).submitReportExtraDataList(extraDataList)) + .to.be.revertedWithCustomError(oracle, "DeprecatedExtraDataType") + .withArgs(0, EXTRA_DATA_TYPE_STUCK_VALIDATORS); + }); + }); + // TODO: Rewrite tests to account for the fact that the stuck item type is no longer supported + context.skip("submitReportExtraDataList", () => { beforeEach(takeSnapshot); afterEach(rollback); @@ -261,8 +291,11 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { expect(state1.lastSortingKey).to.be.equal( calcSortingKey( EXTRA_DATA_TYPE_STUCK_VALIDATORS, - defaultExtraData.stuckKeys[2].moduleId, - defaultExtraData.stuckKeys[2].nodeOpIds[0], + defaultExtraData.exitedKeys[2].moduleId, + defaultExtraData.exitedKeys[2].nodeOpIds[0], + + // defaultExtraData.stuckKeys[2].moduleId, + // defaultExtraData.stuckKeys[2].nodeOpIds[0], ), ); expect(state1.dataHash).to.be.equal(extraDataChunkHashes[1]); @@ -412,7 +445,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { const { reportFields, extraDataHash } = await submitReportHash(); await oracle.connect(member1).submitReportData(reportFields, oracleVersion); const incorrectExtraData = getDefaultExtraData(); - ++incorrectExtraData.stuckKeys[0].nodeOpIds[0]; + // ++incorrectExtraData.stuckKeys[0].nodeOpIds[0]; const incorrectExtraDataItems = encodeExtraDataItems(incorrectExtraData); const incorrectExtraDataList = packExtraDataList(incorrectExtraDataItems); const incorrectExtraDataHash = calcExtraDataListHash(incorrectExtraDataList); @@ -562,7 +595,8 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { await consensus.advanceTimeToNextFrameStart(); const { reportFields: emptyReport, reportHash: emptyReportHash } = await constructOracleReportWithSingeExtraDataTransactionForCurrentRefSlot({ - extraData: { stuckKeys: [], exitedKeys: [] }, + // extraData: { stuckKeys: [], exitedKeys: [] }, + extraData: { exitedKeys: [] }, }); const { extraDataList } = await constructOracleReportWithSingeExtraDataTransactionForCurrentRefSlot(); @@ -616,7 +650,8 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("reverts with ExtraDataAlreadyProcessed if empty extraData has already been processed", async () => { const { report: emptyReport } = await constructOracleReportForCurrentFrameAndSubmitReportHash({ - extraData: { stuckKeys: [], exitedKeys: [] }, + // extraData: { stuckKeys: [], exitedKeys: [] }, + extraData: { exitedKeys: [] }, }); await oracleMemberSubmitReportData(emptyReport); @@ -903,7 +938,8 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("should revert in case when items count exceed limit", async () => { const maxItemsPerChunk = 3; const extraData = getDefaultExtraData(); - const itemsCount = extraData.exitedKeys.length + extraData.stuckKeys.length; + // const itemsCount = extraData.exitedKeys.length + extraData.stuckKeys.length; + const itemsCount = extraData.exitedKeys.length; const { report, extraDataChunks } = await constructOracleReportForCurrentFrameAndSubmitReportHash({ extraData, config: { maxItemsPerChunk }, @@ -1051,11 +1087,13 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { const callsCount = await stakingRouter.totalCalls_reportStuckKeysByNodeOperator(); const extraDataValue = reportInput.extraDataValue as ExtraDataType; - expect(callsCount).to.equal(extraDataValue.stuckKeys.length); + // expect(callsCount).to.equal(extraDataValue.stuckKeys.length); + expect(callsCount).to.equal(extraDataValue.exitedKeys.length); for (let i = 0; i < callsCount; i++) { const call = await stakingRouter.calls_reportStuckKeysByNodeOperator(i); - const item = extraDataValue.stuckKeys[i]; + // const item = extraDataValue.stuckKeys[i]; + const item = extraDataValue.exitedKeys[i]; expect(call.stakingModuleId).to.equal(item.moduleId); expect(call.nodeOperatorIds).to.equal("0x" + item.nodeOpIds.map((id) => numberToHex(id, 8)).join("")); expect(call.keysCounts).to.equal("0x" + item.keysCounts.map((count) => numberToHex(count, 16)).join("")); From 1b1ddb98ccca97d1103b27aceed8eddd4bf93ced Mon Sep 17 00:00:00 2001 From: KRogLA Date: Mon, 26 May 2025 21:37:02 +0200 Subject: [PATCH 172/405] refactor: exit limit lib --- contracts/0.8.9/lib/ExitLimitUtils.sol | 43 +++++++------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index 29b93c7323..5ae982cd3e 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -2,15 +2,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; -import {UnstructuredStorage} from "./UnstructuredStorage.sol"; - -// MSB ---------------------------------------------------------------------------------------> LSB -// 160___________________128_____________________96______________64_____________32_______________ 0 -// |______________________|_______________________|_______________|_______________|_______________| -// | maxExitRequestsLimit | prevExitRequestsLimit | prevTimestamp | frameDuration | exitsPerFrame | -// |<------ 32 bits ----->|<------ 32 bits ------>|<-- 32 bits -->|<-- 32 bits -->|<-- 32 bits -->| -// - struct ExitRequestLimitData { uint32 maxExitRequestsLimit; // Maximum limit uint32 prevExitRequestsLimit; // Limit left after previous requests @@ -20,32 +11,22 @@ struct ExitRequestLimitData { } library ExitLimitUtilsStorage { - using UnstructuredStorage for bytes32; - - uint256 internal constant EXITS_PER_FRAME_OFFSET = 0; - uint256 internal constant FRAME_DURATION_OFFSET = 32; - uint256 internal constant PREV_TIMESTAMP_OFFSET = 64; - uint256 internal constant PREV_EXIT_REQUESTS_LIMIT_OFFSET = 96; - uint256 internal constant MAX_EXIT_REQUESTS_LIMIT_OFFSET = 128; - - function getStorageExitRequestLimit(bytes32 _position) internal view returns (ExitRequestLimitData memory data) { - uint256 slot = _position.getStorageUint256(); - - data.exitsPerFrame = uint32(slot >> EXITS_PER_FRAME_OFFSET); - data.frameDuration = uint32(slot >> FRAME_DURATION_OFFSET); - data.prevTimestamp = uint32(slot >> PREV_TIMESTAMP_OFFSET); - data.prevExitRequestsLimit = uint32(slot >> PREV_EXIT_REQUESTS_LIMIT_OFFSET); - data.maxExitRequestsLimit = uint32(slot >> MAX_EXIT_REQUESTS_LIMIT_OFFSET); + struct DataStorage { + ExitRequestLimitData _exitRequestLimitData; + } + + function getStorageExitRequestLimit(bytes32 _position) internal view returns (ExitRequestLimitData memory) { + return _getDataStorage(_position)._exitRequestLimitData; } function setStorageExitRequestLimit(bytes32 _position, ExitRequestLimitData memory _data) internal { - uint256 value = (uint256(_data.exitsPerFrame) << EXITS_PER_FRAME_OFFSET) | - (uint256(_data.frameDuration) << FRAME_DURATION_OFFSET) | - (uint256(_data.prevTimestamp) << PREV_TIMESTAMP_OFFSET) | - (uint256(_data.prevExitRequestsLimit) << PREV_EXIT_REQUESTS_LIMIT_OFFSET) | - (uint256(_data.maxExitRequestsLimit) << MAX_EXIT_REQUESTS_LIMIT_OFFSET); + _getDataStorage(_position)._exitRequestLimitData = _data; + } - _position.setStorageUint256(value); + function _getDataStorage(bytes32 _position) private pure returns (DataStorage storage $) { + assembly { + $.slot := _position + } } } From 52888f263d82917d401be95691f3a8bb3a84ba7c Mon Sep 17 00:00:00 2001 From: KRogLA Date: Mon, 26 May 2025 21:39:19 +0200 Subject: [PATCH 173/405] refactor: simplify code --- contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 14 ++++---------- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 15 +++++---------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index eee72b7bbb..dc8d187130 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -193,7 +193,10 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { exitsPerFrame = exitRequestLimitData.exitsPerFrame; frameDuration = exitRequestLimitData.frameDuration; prevExitRequestsLimit = exitRequestLimitData.prevExitRequestsLimit; - currentExitRequestsLimit = _getCurrentExitLimit(); + + currentExitRequestsLimit = exitRequestLimitData.isExitLimitSet() + ? exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()) + : type(uint256).max; } /// Internal functions @@ -259,15 +262,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { return block.timestamp; // solhint-disable-line not-rely-on-time } - function _getCurrentExitLimit() internal view returns (uint256) { - ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); - if (!twrLimitData.isExitLimitSet()) { - return type(uint256).max; - } - - return twrLimitData.calculateCurrentExitLimit(_getTimestamp()); - } - function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d898394aeb..5851c85fe4 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -151,6 +151,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa uint256 valIndex; bytes pubkey; } + struct RequestStatus { uint32 contractVersion; uint32 deliveryHistoryLength; @@ -414,7 +415,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa exitsPerFrame = exitRequestLimitData.exitsPerFrame; frameDuration = exitRequestLimitData.frameDuration; prevExitRequestsLimit = exitRequestLimitData.prevExitRequestsLimit; - currentExitRequestsLimit = _getCurrentExitLimit(); + + currentExitRequestsLimit = exitRequestLimitData.isExitLimitSet() + ? exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()) + : type(uint256).max; } /** @@ -562,15 +566,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _getCurrentExitLimit() internal view returns (uint256) { - ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - if (!exitRequestLimitData.isExitLimitSet()) { - return type(uint256).max; - } - - return exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); - } - function _getTimestamp() internal view virtual returns (uint256) { return block.timestamp; // solhint-disable-line not-rely-on-time } From 3b27659b7f286596ad77cd1fa0931d489c8e45a2 Mon Sep 17 00:00:00 2001 From: KRogLA Date: Tue, 27 May 2025 01:28:10 +0200 Subject: [PATCH 174/405] refactor: withdrawal vault + tw gateway --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 125 +++++++----------- contracts/0.8.9/WithdrawalVault.sol | 60 ++++++++- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 107 ++++----------- 3 files changed, 133 insertions(+), 159 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index dc8d187130..1854234262 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -7,7 +7,7 @@ import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; interface IWithdrawalVault { - function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amounts) external payable; + function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) external payable; function getWithdrawalRequestFee() external view returns (uint256); } @@ -50,23 +50,25 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param feeRequired Amount of fee required to cover withdrawal request * @param passedValue Amount of fee sent to cover withdrawal request */ - error InsufficientWithdrawalFee(uint256 feeRequired, uint256 passedValue); + error Insufficientfee(uint256 feeRequired, uint256 passedValue); /** * @notice Thrown when a withdrawal fee refund failed */ - error TriggerableWithdrawalFeeRefundFailed(); + error TriggerablefeeRefundFailed(); /** * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) * @param exitsPerFrame The number of exits that can be restored per frame. * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ + event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); /** * @notice Thrown when remaining exit requests limit is not enough to cover sender requests * @param requestsCount Amount of requests that were sent for processing * @param remainingLimit Amount of requests that still can be processed at current day */ + error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); struct ValidatorData { @@ -112,7 +114,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @dev Submits Triggerable Withdrawal Requests to the Withdrawal Vault as full withdrawal requests * for the specified validator public keys. * - * @param triggerableExitsData An array of `ValidatorData` structs, each representing a validator + * @param validatorsData An array of `ValidatorData` structs, each representing a validator * for which a withdrawal request will be submitted. Each entry includes: * - `stakingModuleId`: ID of the staking module. * - `nodeOperatorId`: ID of the node operator. @@ -126,33 +128,30 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * - There is not enough limit quota left in the current frame to process all requests. */ function triggerFullWithdrawals( - ValidatorData[] calldata triggerableExitsData, + ValidatorData[] calldata validatorsData, address refundRecipient, uint8 exitType ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { if (msg.value == 0) revert ZeroArgument("msg.value"); - - // If the refund recipient is not set, use the sender as the refund recipient - if (refundRecipient == address(0)) { - refundRecipient = msg.sender; - } - - uint256 requestsCount = triggerableExitsData.length; + uint256 requestsCount = validatorsData.length; + if (requestsCount == 0) revert ZeroArgument("validatorsData"); _checkExitRequestLimit(requestsCount); - uint256 withdrawalFee = IWithdrawalVault(LOCATOR.withdrawalVault()).getWithdrawalRequestFee(); - _checkFee(requestsCount, withdrawalFee); - - bytes memory pubkeys = new bytes(requestsCount * PUBLIC_KEY_LENGTH); + IWithdrawalVault withdrawalVault = IWithdrawalVault(LOCATOR.withdrawalVault()); + uint256 fee = withdrawalVault.getWithdrawalRequestFee(); + uint256 totalFee = requestsCount * fee; + uint256 refund = _checkFee(totalFee); + bytes[] memory pubkeys = new bytes[](requestsCount); for (uint256 i = 0; i < requestsCount; ++i) { - ValidatorData memory data = triggerableExitsData[i]; - _copyPubkey(data.pubkey, pubkeys, i); - _notifyStakingModule(data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalFee, exitType); + pubkeys[i] = validatorsData[i].pubkey; } - _addWithdrawalRequest(requestsCount, withdrawalFee, pubkeys, refundRecipient); + withdrawalVault.addWithdrawalRequests{value: totalFee}(pubkeys, new uint64[](requestsCount)); + + _notifyStakingModules(validatorsData, fee, exitType); + _refundFee(refund, refundRecipient); } /** @@ -161,11 +160,10 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param exitsPerFrame The number of exits that can be restored per frame. * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ - function setExitRequestLimit( - uint256 maxExitRequestsLimit, - uint256 exitsPerFrame, - uint256 frameDuration - ) external onlyRole(TW_EXIT_REPORT_LIMIT_ROLE) { + function setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) + external + onlyRole(TW_EXIT_REPORT_LIMIT_ROLE) + { _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); } @@ -201,59 +199,40 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { /// Internal functions - function _checkFee(uint256 requestsCount, uint256 withdrawalFee) internal { - if (msg.value < requestsCount * withdrawalFee) { - revert InsufficientWithdrawalFee(requestsCount * withdrawalFee, msg.value); + function _checkFee(uint256 fee) internal returns (uint256 refund) { + if (msg.value < fee) { + revert Insufficientfee(fee, msg.value); } - } - - function _copyPubkey(bytes memory pubkey, bytes memory pubkeys, uint256 index) internal pure { - assembly { - let pubkeyMemPtr := add(pubkey, 32) - let pubkeysOffset := add(pubkeys, add(32, mul(PUBLIC_KEY_LENGTH, index))) - mstore(pubkeysOffset, mload(pubkeyMemPtr)) - mstore(add(pubkeysOffset, 32), mload(add(pubkeyMemPtr, 32))) + unchecked { + refund = msg.value - fee; } } - function _notifyStakingModule( - uint256 stakingModuleId, - uint256 nodeOperatorId, - bytes memory pubkey, + function _notifyStakingModules( + ValidatorData[] calldata validatorsData, uint256 withdrawalRequestPaidFee, uint8 exitType ) internal { - IStakingRouter(LOCATOR.stakingRouter()).onValidatorExitTriggered( - stakingModuleId, - nodeOperatorId, - pubkey, - withdrawalRequestPaidFee, - exitType - ); - } - - function _addWithdrawalRequest( - uint256 requestsCount, - uint256 withdrawalFee, - bytes memory pubkeys, - address refundRecipient - ) internal { - IWithdrawalVault(LOCATOR.withdrawalVault()).addWithdrawalRequests{value: requestsCount * withdrawalFee}( - pubkeys, - new uint64[](requestsCount) - ); - - _refundFee(requestsCount * withdrawalFee, refundRecipient); + IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); + ValidatorData calldata data; + for (uint256 i = 0; i < validatorsData.length; ++i) { + data = validatorsData[i]; + stakingRouter.onValidatorExitTriggered( + data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalRequestPaidFee, exitType + ); + } } - function _refundFee(uint256 fee, address recipient) internal { - uint256 refund = msg.value - fee; - + function _refundFee(uint256 refund, address recipient) internal { if (refund > 0) { - (bool success, ) = recipient.call{value: refund}(""); + // If the refund recipient is not set, use the sender as the refund recipient + if (recipient == address(0)) { + recipient = msg.sender; + } + (bool success,) = recipient.call{value: refund}(""); if (!success) { - revert TriggerableWithdrawalFeeRefundFailed(); + revert TriggerablefeeRefundFailed(); } } } @@ -262,17 +241,16 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { return block.timestamp; // solhint-disable-line not-rely-on-time } - function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { + function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) + internal + { require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); uint256 timestamp = _getTimestamp(); TWR_LIMIT_POSITION.setStorageExitRequestLimit( TWR_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( - maxExitRequestsLimit, - exitsPerFrame, - frameDuration, - timestamp + maxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp ) ); @@ -285,15 +263,14 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { return; } - uint256 timestamp = _getTimestamp(); - uint256 limit = twrLimitData.calculateCurrentExitLimit(timestamp); + uint256 limit = twrLimitData.calculateCurrentExitLimit(_getTimestamp()); if (limit < requestsCount) { revert ExitRequestsLimit(requestsCount, limit); } TWR_LIMIT_POSITION.setStorageExitRequestLimit( - twrLimitData.updatePrevExitLimit(limit - requestsCount, timestamp) + twrLimitData.updatePrevExitLimit(limit - requestsCount, _getTimestamp()) ); } } diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 4b0eb9b4d5..21c2a5f9b7 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -10,8 +10,8 @@ import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; +import {PausableUntil} from "./utils/PausableUntil.sol"; -import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; import {WithdrawalVaultEIP7685} from "./WithdrawalVaultEIP7685.sol"; interface ILido { @@ -26,9 +26,13 @@ interface ILido { /** * @title A vault for temporary storage of withdrawals */ -contract WithdrawalVault is AccessControlEnumerable, Versioned, WithdrawalVaultEIP7685 { +contract WithdrawalVault is AccessControlEnumerable, Versioned, PausableUntil, WithdrawalVaultEIP7685 { using SafeERC20 for IERC20; + bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); + bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); + bytes32 public constant ADD_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_WITHDRAWAL_REQUEST_ROLE"); + ILido public immutable LIDO; address public immutable TREASURY; @@ -63,6 +67,13 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, WithdrawalVaultE TREASURY = _treasury; } + /// @dev Ensures the contract’s ETH balance is unchanged. + modifier preservesEthBalance() { + uint256 balanceBeforeCall = address(this).balance - msg.value; + _; + assert(address(this).balance == balanceBeforeCall); + } + /// @notice Initializes the contract. Can be called only once. /// @param _admin Lido DAO Aragon agent contract address. /// @dev Proxy initialization method. @@ -84,6 +95,40 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, WithdrawalVaultE _updateContractVersion(2); } + /** + * @dev Resumes the general purpose execution layer requests. + * @notice Reverts if: + * - The contract is not paused. + * - The sender does not have the `RESUME_ROLE`. + */ + function resume() external onlyRole(RESUME_ROLE) { + _resume(); + } + + /** + * @notice Pauses the general purpose execution layer requests placement for a specified duration. + * @param _duration The pause duration in seconds (use `PAUSE_INFINITELY` for unlimited). + * @dev Reverts if: + * - The contract is already paused. + * - The sender does not have the `PAUSE_ROLE`. + * - A zero duration is passed. + */ + function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { + _pauseFor(_duration); + } + + /** + * @notice Pauses the general purpose execution layer requests placement until a specified timestamp. + * @param _pauseUntilInclusive The last second to pause until (inclusive). + * @dev Reverts if: + * - The timestamp is in the past. + * - The sender does not have the `PAUSE_ROLE`. + * - The contract is already paused. + */ + function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { + _pauseUntil(_pauseUntilInclusive); + } + /** * @notice Withdraw `_amount` of accumulated withdrawals to Lido contract * @dev Can be called only by the Lido contract @@ -143,4 +188,15 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, WithdrawalVaultE _onlyNonZeroAddress(_admin); _setupRole(DEFAULT_ADMIN_ROLE, _admin); } + + /// Withdrawals EIP-7002 + function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) + external + payable + onlyRole(ADD_WITHDRAWAL_REQUEST_ROLE) + whenResumed + preservesEthBalance + { + _addWithdrawalRequests(pubkeys, amounts); + } } diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index 10f886d604..ba224fe7bb 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -4,74 +4,22 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; -import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; -import {PausableUntil} from "./utils/PausableUntil.sol"; - /** * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests * @dev This contract enables validators to submit EIP-7002 withdrawal requests. */ -abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUntil { +abstract contract WithdrawalVaultEIP7685 { address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; - bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); - bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - bytes32 public constant ADD_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_WITHDRAWAL_REQUEST_ROLE"); - - uint256 internal constant PUBLIC_KEY_LENGTH = 48; - event WithdrawalRequestAdded(bytes request); error ZeroArgument(string name); - error MalformedPubkeysArray(); error ArraysLengthMismatch(uint256 firstArrayLength, uint256 secondArrayLength); - error FeeReadFailed(); error FeeInvalidData(); error IncorrectFee(uint256 providedFee, uint256 requiredFee); - error RequestAdditionFailed(bytes callData); - /// @dev Ensures the contract’s ETH balance is unchanged. - modifier preservesEthBalance() { - uint256 balanceBeforeCall = address(this).balance - msg.value; - _; - assert(address(this).balance == balanceBeforeCall); - } - - /** - * @dev Resumes the general purpose execution layer requests. - * @notice Reverts if: - * - The contract is not paused. - * - The sender does not have the `RESUME_ROLE`. - */ - function resume() external onlyRole(RESUME_ROLE) { - _resume(); - } - - /** - * @notice Pauses the general purpose execution layer requests placement for a specified duration. - * @param _duration The pause duration in seconds (use `PAUSE_INFINITELY` for unlimited). - * @dev Reverts if: - * - The contract is already paused. - * - The sender does not have the `PAUSE_ROLE`. - * - A zero duration is passed. - */ - function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { - _pauseFor(_duration); - } - - /** - * @notice Pauses the general purpose execution layer requests placement until a specified timestamp. - * @param _pauseUntilInclusive The last second to pause until (inclusive). - * @dev Reverts if: - * - The timestamp is in the past. - * - The sender does not have the `PAUSE_ROLE`. - * - The contract is already paused. - */ - function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { - _pauseUntil(_pauseUntilInclusive); - } /** * @dev Submits EIP-7002 full or partial withdrawal requests for the specified public keys. @@ -92,38 +40,16 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt * - The provided public key and amount arrays are not of equal length. * - The provided total withdrawal fee value is invalid. */ - function addWithdrawalRequests( - bytes calldata pubkeys, - uint64[] calldata amounts - ) external payable onlyRole(ADD_WITHDRAWAL_REQUEST_ROLE) whenResumed preservesEthBalance { - if (pubkeys.length == 0) revert ZeroArgument("pubkeys"); - if (pubkeys.length % PUBLIC_KEY_LENGTH != 0) revert MalformedPubkeysArray(); - - uint256 requestsCount = pubkeys.length / PUBLIC_KEY_LENGTH; + function _addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) internal { + uint256 requestsCount = pubkeys.length; + if (requestsCount == 0) revert ZeroArgument("pubkeys"); if (requestsCount != amounts.length) revert ArraysLengthMismatch(requestsCount, amounts.length); - uint256 feePerRequest = _getRequestFee(WITHDRAWAL_REQUEST); - uint256 totalFee = requestsCount * feePerRequest; - - if (totalFee != msg.value) { - revert IncorrectFee(msg.value, totalFee); - } - - bytes memory request = new bytes(56); - for (uint256 i = 0; i < requestsCount; i++) { - uint64 amount = amounts[i]; - assembly { - calldatacopy(add(request, 32), add(pubkeys.offset, mul(i, PUBLIC_KEY_LENGTH)), PUBLIC_KEY_LENGTH) - mstore(add(request, 80), shl(192, amount)) - } - - (bool success, ) = WITHDRAWAL_REQUEST.call{value: feePerRequest}(request); - - if (!success) { - revert RequestAdditionFailed(request); - } + uint256 fee = getWithdrawalRequestFee(); + _checkFee(requestsCount * fee); - emit WithdrawalRequestAdded(request); + for (uint256 i = 0; i < requestsCount; ++i) { + _callAddWithdrawalRequest(fee, abi.encodePacked(pubkeys[i], amounts[i])); } } @@ -131,7 +57,7 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt * @dev Retrieves the current EIP-7002 withdrawal fee. * @return The minimum fee required per withdrawal request. */ - function getWithdrawalRequestFee() external view returns (uint256) { + function getWithdrawalRequestFee() public view returns (uint256) { return _getRequestFee(WITHDRAWAL_REQUEST); } @@ -148,4 +74,19 @@ abstract contract WithdrawalVaultEIP7685 is AccessControlEnumerable, PausableUnt return abi.decode(feeData, (uint256)); } + + function _callAddWithdrawalRequest(uint256 fee, bytes memory request) internal { + (bool success,) = WITHDRAWAL_REQUEST.call{value: fee}(request); + if (!success) { + revert RequestAdditionFailed(request); + } + + emit WithdrawalRequestAdded(request); + } + + function _checkFee(uint256 fee) internal view { + if (msg.value != fee) { + revert IncorrectFee(msg.value, fee); + } + } } From f7cb34412baafb0ee9383ec86155c7ca7c692e74 Mon Sep 17 00:00:00 2001 From: KRogLA Date: Tue, 27 May 2025 03:38:54 +0200 Subject: [PATCH 175/405] refactor: veb --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 35 ++++++++++--------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 31 ++++++++-------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 5851c85fe4..7e04041e2f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -269,10 +269,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkExitSubmitted(requestStatus); _checkExitRequestData(request.data, request.dataFormat); - _checkMaxBatchSize(request.data); + uint256 totalItemsCount = _checkMaxBatchSize(request.data); _checkContractVersion(requestStatus.contractVersion); - uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; uint32 lastDeliveredIndex = requestStatus.lastDeliveredExitDataIndex; uint256 startIndex = requestStatus.deliveryHistoryLength == 0 ? 0 : lastDeliveredIndex + 1; @@ -282,7 +281,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert RequestsAlreadyDelivered(); } - uint256 requestsToDeliver = _consumeLimit(undeliveredItemsCount); + uint256 requestsToDeliver = _consumeLimit(undeliveredItemsCount, _applyDeliverLimit); _processExitRequestsList(request.data, startIndex, requestsToDeliver); @@ -545,9 +544,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _checkMaxBatchSize(bytes calldata requests) internal view { + function _checkMaxBatchSize(bytes calldata requests) internal view returns (uint256 requestsCount) { uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); - uint256 requestsCount = requests.length / PACKED_REQUEST_LENGTH; + requestsCount = requests.length / PACKED_REQUEST_LENGTH; if (requestsCount > maxRequestsPerBatch) { revert MaxRequestsBatchSizeExceeded(requestsCount, maxRequestsPerBatch); @@ -595,24 +594,31 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); } - function _consumeLimit(uint256 requestsCount) internal returns (uint256 requestsToDeliver) { + function _consumeLimit(uint256 requestsCount, function(uint256, uint256) internal pure returns(uint256) applyLimit) internal returns (uint256 requestsLimitedCount) { ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (!exitRequestLimitData.isExitLimitSet()) { return requestsCount; } uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); + requestsLimitedCount = applyLimit(limit, requestsCount); - if (limit == 0) { - revert ExitRequestsLimit(requestsCount, 0); - } - - requestsToDeliver = limit >= requestsCount ? requestsCount : limit; EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitLimit(limit - requestsToDeliver, _getTimestamp()) + exitRequestLimitData.updatePrevExitLimit(limit - requestsLimitedCount, _getTimestamp()) ); } + function _applyDeliverLimit(uint256 limit, uint256 count) + internal + pure + returns (uint256 limitedCount) + { + if (limit == 0) { + revert ExitRequestsLimit(count, 0); + } + return limit >= count ? count : limit; + } + function _storeNewHashRequestStatus(bytes32 exitRequestsHash, RequestStatus memory requestStatus) internal { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; @@ -621,10 +627,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert ExitHashAlreadySubmitted(); } - storedRequest.contractVersion = requestStatus.contractVersion; - storedRequest.deliveryHistoryLength = requestStatus.deliveryHistoryLength; - storedRequest.lastDeliveredExitDataIndex = requestStatus.lastDeliveredExitDataIndex; - storedRequest.lastDeliveredExitDataTimestamp = requestStatus.lastDeliveredExitDataTimestamp; + requestStatusMap[exitRequestsHash] = requestStatus; emit RequestsHashSubmitted(exitRequestsHash); } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 88bb37f317..403fdc1319 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -264,26 +264,14 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { revert InvalidRequestsDataLength(); } - IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()) - .checkExitBusOracleReport(data.requestsCount); - - // Check VEB common limit - - ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); - if (exitRequestLimitData.isExitLimitSet()) { - uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); - if (limit < data.requestsCount) { - revert ExitRequestsLimit(data.requestsCount, limit); - } - EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitLimit(limit - data.requestsCount, _getTimestamp()) - ); - } - if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) { revert UnexpectedRequestsDataLength(); } + IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()).checkExitBusOracleReport(data.requestsCount); + + // Check VEB common limit + _consumeLimit(data.requestsCount, _applyOracleLimit); _processExitRequestsList(data.data, 0, data.requestsCount); _storageDataProcessingState().value = DataProcessingState({ @@ -302,7 +290,16 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { ); } - function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 requestsCount, uint256 contractVersion) internal { + function _applyOracleLimit(uint256 limit, uint256 count) internal pure returns (uint256) { + if (limit < count) { + revert ExitRequestsLimit(count, limit); + } + return count; + } + + function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 requestsCount, uint256 contractVersion) + internal + { if (requestsCount == 0) { return; } From d4392879198a9b8b51b7a249132a404d7e958e3c Mon Sep 17 00:00:00 2001 From: KRogLA Date: Tue, 27 May 2025 04:18:39 +0200 Subject: [PATCH 176/405] test: fix twg --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 8 ++--- ...TriggerableWithdrawalsGateway__Harness.sol | 3 +- .../contracts/WithdrawalValut_MockForTWG.sol | 4 +-- ...awalGateway.triggerFullWithdrawals.test.ts | 33 +++---------------- 4 files changed, 13 insertions(+), 35 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 1854234262..0437e7e8fa 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -50,11 +50,11 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param feeRequired Amount of fee required to cover withdrawal request * @param passedValue Amount of fee sent to cover withdrawal request */ - error Insufficientfee(uint256 feeRequired, uint256 passedValue); + error InsufficientFee(uint256 feeRequired, uint256 passedValue); /** * @notice Thrown when a withdrawal fee refund failed */ - error TriggerablefeeRefundFailed(); + error FeeRefundFailed(); /** * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) @@ -201,7 +201,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { function _checkFee(uint256 fee) internal returns (uint256 refund) { if (msg.value < fee) { - revert Insufficientfee(fee, msg.value); + revert InsufficientFee(fee, msg.value); } unchecked { refund = msg.value - fee; @@ -232,7 +232,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { (bool success,) = recipient.call{value: refund}(""); if (!success) { - revert TriggerablefeeRefundFailed(); + revert FeeRefundFailed(); } } } diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol index 41e54a5000..3a7440872e 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol @@ -27,6 +27,7 @@ contract TriggerableWithdrawalsGateway__Harness is TriggerableWithdrawalsGateway // Wrap internal functions for testing function refundFee(uint256 fee, address recipient) external payable { - _refundFee(fee, recipient); + uint256 refund = _checkFee(fee); + _refundFee(refund, recipient); } } diff --git a/test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol b/test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol index 7493aaa090..4af9573ceb 100644 --- a/test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol +++ b/test/0.8.9/contracts/WithdrawalValut_MockForTWG.sol @@ -1,9 +1,9 @@ pragma solidity 0.8.9; contract WithdrawalVault__MockForTWG { - event AddFullWithdrawalRequestsCalled(bytes pubkeys); + event AddFullWithdrawalRequestsCalled(bytes[] pubkeys); - function addWithdrawalRequests(bytes calldata pubkeys, uint64[] calldata amount) external payable { + function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amount) external payable { emit AddFullWithdrawalRequestsCalled(pubkeys); } diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 791f3230c1..559100cb6f 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -9,8 +9,6 @@ import { WithdrawalVault__MockForTWG, } from "typechain-types"; -import { de0x } from "lib"; - import { deployLidoLocator, updateLidoLocatorImplementation } from "../deploy/locator"; interface ExitRequest { @@ -105,7 +103,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 1 }), ) - .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "InsufficientWithdrawalFee") + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "InsufficientFee") .withArgs(3, 1); }); @@ -132,14 +130,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const pubkeys = - "0x" + - exitRequests - .map((request) => { - const pubkeyHex = de0x(request.valPubkey); - return pubkeyHex; - }) - .join(""); + const pubkeys = exitRequests.map((request) => request.valPubkey); for (const request of exitRequests) { await expect(tx) @@ -206,14 +197,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - const pubkeys = - "0x" + - exitRequests - .map((request) => { - const pubkeyHex = de0x(request.valPubkey); - return pubkeyHex; - }) - .join(""); + const pubkeys = exitRequests.map((request) => request.valPubkey); for (const request of exitRequests) { await expect(tx) @@ -320,7 +304,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { await expect( triggerableWithdrawalsGateway.connect(authorizedEntity).refundFee(5, refundReverter.getAddress(), { value: 10 }), - ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "TriggerableWithdrawalFeeRefundFailed"); + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "FeeRefundFailed"); }); it("should set maxExitRequestsLimit equal to 0 and return as currentExitRequestsLimit type(uint256).max", async () => { @@ -356,14 +340,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requestData, ZERO_ADDRESS, 0, { value: 10 }); - const pubkeys = - "0x" + - requests - .map((request) => { - const pubkeyHex = de0x(request.valPubkey); - return pubkeyHex; - }) - .join(""); + const pubkeys = requests.map((request) => request.valPubkey); for (const request of exitRequests) { await expect(tx) From 208811f1ec4698becd7cf2de54f873a58f65e962 Mon Sep 17 00:00:00 2001 From: KRogLA Date: Tue, 27 May 2025 04:36:43 +0200 Subject: [PATCH 177/405] test: fix wv tests --- test/0.8.9/withdrawalVault/utils.ts | 1 + .../withdrawalVault/withdrawalVault.test.ts | 102 +++++++++--------- 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/test/0.8.9/withdrawalVault/utils.ts b/test/0.8.9/withdrawalVault/utils.ts index 64a2e418ea..db5ffa740e 100644 --- a/test/0.8.9/withdrawalVault/utils.ts +++ b/test/0.8.9/withdrawalVault/utils.ts @@ -29,6 +29,7 @@ export function generateWithdrawalRequestPayload(numberOfRequests: number) { return { pubkeysHexString: `0x${pubkeys.join("")}`, + pubkeysHexArray: pubkeys.map((pk) => `0x${pk}`), pubkeys, fullWithdrawalAmounts, partialWithdrawalAmounts, diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 51ca22ec91..5c485f010c 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -317,45 +317,43 @@ describe("WithdrawalVault.sol", () => { it("Should revert if the caller is not Validator Exit Bus", async () => { await expect( - vault.connect(stranger).addWithdrawalRequests("0x1234", [1n]), + vault.connect(stranger).addWithdrawalRequests(["0x1234"], [1n]), ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_WITHDRAWAL_REQUEST_ROLE); }); it("Should revert if empty arrays are provided", async function () { - await expect(vault.connect(validatorsExitBus).addWithdrawalRequests("0x", [], { value: 1n })) + await expect(vault.connect(validatorsExitBus).addWithdrawalRequests([], [], { value: 1n })) .to.be.revertedWithCustomError(vault, "ZeroArgument") .withArgs("pubkeys"); }); it("Should revert if array lengths do not match", async function () { const requestCount = 2; - const { pubkeysHexString } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray } = generateWithdrawalRequestPayload(requestCount); const amounts = [1n]; const totalWithdrawalFee = (await getFee()) * BigInt(requestCount); await expect( - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, amounts, { value: totalWithdrawalFee }), + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, amounts, { value: totalWithdrawalFee }), ) .to.be.revertedWithCustomError(vault, "ArraysLengthMismatch") .withArgs(requestCount, amounts.length); await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, [], { value: totalWithdrawalFee }), + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, [], { value: totalWithdrawalFee }), ) .to.be.revertedWithCustomError(vault, "ArraysLengthMismatch") .withArgs(requestCount, 0); }); it("Should revert if not enough fee is sent", async function () { - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(1); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(1); await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent - await expect(vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts)) + await expect(vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts)) .to.be.revertedWithCustomError(vault, "IncorrectFee") .withArgs(0, 3n); @@ -364,15 +362,15 @@ describe("WithdrawalVault.sol", () => { await expect( vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: insufficientFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: insufficientFee }), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") .withArgs(2n, 3n); }); - it("Should revert if pubkey is not 48 bytes", async function () { + it.skip("Should revert if pubkey is not 48 bytes", async function () { // Invalid pubkey (only 2 bytes) - const invalidPubkeyHexString = "0x1234"; + const invalidPubkeyHexString = ["0x1234"]; const fee = await getFee(); await expect( @@ -380,49 +378,45 @@ describe("WithdrawalVault.sol", () => { ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); }); - it("Should revert if last pubkey not 48 bytes", async function () { + it.skip("Should revert if last pubkey not 48 bytes", async function () { const validPubey = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; const invalidPubkey = "1234"; - const pubkeysHexString = `0x${validPubey}${invalidPubkey}`; + const pubkeysHexArray = [`0x${validPubey}`, `${invalidPubkey}`]; const fee = await getFee(); await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, [1n, 2n], { value: fee }), + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, [1n, 2n], { value: fee }), ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); }); it("Should revert if addition fails at the withdrawal request contract", async function () { - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(1); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(1); const fee = await getFee(); // Set mock to fail on add await withdrawalsPredeployed.mock__setFailOnAddRequest(true); await expect( - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: fee }), + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), ).to.be.revertedWithCustomError(vault, "RequestAdditionFailed"); }); it("Should revert when fee read fails", async function () { await withdrawalsPredeployed.mock__setFailOnGetFee(true); - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); const fee = 10n; await expect( - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: fee }), + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), ).to.be.revertedWithCustomError(vault, "FeeReadFailed"); }); it("Should revert when the provided fee exceeds the required amount", async function () { const requestCount = 3; - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); @@ -431,7 +425,7 @@ describe("WithdrawalVault.sol", () => { await expect( vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: withdrawalFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: withdrawalFee }), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") .withArgs(10n, 9n); @@ -441,20 +435,20 @@ describe("WithdrawalVault.sol", () => { it(`Should revert if unexpected fee value ${unexpectedFee} is returned`, async function () { await withdrawalsPredeployed.mock__setFeeRaw(unexpectedFee); - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(2); const fee = 10n; await expect( vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: fee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), ).to.be.revertedWithCustomError(vault, "FeeInvalidData"); }); }); it("Should accept withdrawal requests when the provided fee matches the exact required amount", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(3n); @@ -462,7 +456,7 @@ describe("WithdrawalVault.sol", () => { await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -477,7 +471,7 @@ describe("WithdrawalVault.sol", () => { await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedLargeTotalWithdrawalFee, }), pubkeys, @@ -488,14 +482,14 @@ describe("WithdrawalVault.sol", () => { it("Should emit withdrawal event", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), ) @@ -509,7 +503,7 @@ describe("WithdrawalVault.sol", () => { it("Should not affect contract balance", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(fee); @@ -519,7 +513,7 @@ describe("WithdrawalVault.sol", () => { await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -531,7 +525,7 @@ describe("WithdrawalVault.sol", () => { it("Should transfer the total calculated fee to the EIP-7002 withdrawal contract", async function () { const requestCount = 3; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const fee = 3n; await withdrawalsPredeployed.mock__setFee(3n); @@ -540,7 +534,7 @@ describe("WithdrawalVault.sol", () => { const initialBalance = await getWithdrawalsPredeployedContractBalance(); await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -553,11 +547,11 @@ describe("WithdrawalVault.sol", () => { it("Should ensure withdrawal requests are encoded as expected with a 48-byte pubkey and 8-byte amount", async function () { const requestCount = 16; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const tx = await vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: 16n }); + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: 16n }); const receipt = await tx.wait(); @@ -585,7 +579,7 @@ describe("WithdrawalVault.sol", () => { testCasesForWithdrawalRequests.forEach(({ requestCount }) => { it(`Should successfully add ${requestCount} requests`, async () => { - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); @@ -594,7 +588,7 @@ describe("WithdrawalVault.sol", () => { const { receipt: receiptPartialWithdrawal } = await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { + vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -652,7 +646,7 @@ describe("WithdrawalVault.sol", () => { it("should allow withdrawal requests after resuming", async () => { const requestCount = 1; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // First pause and then resume the contract @@ -664,7 +658,7 @@ describe("WithdrawalVault.sol", () => { () => vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), pubkeys, mixedWithdrawalAmounts, expectedFee, @@ -803,7 +797,7 @@ describe("WithdrawalVault.sol", () => { context("Interaction with addWithdrawalRequests", () => { it("pauseFor: should prevent withdrawal requests immediately after pausing", async () => { const requestCount = 1; - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // Initially contract should be resumed @@ -816,13 +810,13 @@ describe("WithdrawalVault.sol", () => { await expect( vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), ).to.be.revertedWithCustomError(vault, "ResumedExpected"); }); it("pauseUntil: should prevent withdrawal requests immediately after pausing", async () => { const requestCount = 1; - const { pubkeysHexString, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // Initially contract should be resumed @@ -836,13 +830,13 @@ describe("WithdrawalVault.sol", () => { await expect( vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), ).to.be.revertedWithCustomError(vault, "ResumedExpected"); }); it("pauseFor: should allow withdrawal requests immediately after resuming", async () => { const requestCount = 1; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // Pause and then resume the contract @@ -854,7 +848,7 @@ describe("WithdrawalVault.sol", () => { () => vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), pubkeys, mixedWithdrawalAmounts, expectedFee, @@ -863,7 +857,7 @@ describe("WithdrawalVault.sol", () => { it("pauseUntil: should allow withdrawal requests immediately after resuming", async () => { const requestCount = 1; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // Pause and then resume the contract @@ -876,7 +870,7 @@ describe("WithdrawalVault.sol", () => { () => vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), pubkeys, mixedWithdrawalAmounts, expectedFee, @@ -885,7 +879,7 @@ describe("WithdrawalVault.sol", () => { it("pauseFor: should allow withdrawal requests after pause duration automatically expires", async () => { const requestCount = 1; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // Pause for 100 seconds @@ -899,7 +893,7 @@ describe("WithdrawalVault.sol", () => { () => vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), pubkeys, mixedWithdrawalAmounts, expectedFee, @@ -908,7 +902,7 @@ describe("WithdrawalVault.sol", () => { it("pauseUntil: should allow withdrawal requests after pause duration automatically expires", async () => { const requestCount = 1; - const { pubkeysHexString, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); + const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const expectedFee = await getFee(); // Pause for 100 seconds @@ -923,7 +917,7 @@ describe("WithdrawalVault.sol", () => { () => vault .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexString, mixedWithdrawalAmounts, { value: expectedFee }), + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), pubkeys, mixedWithdrawalAmounts, expectedFee, From b2ae80c7a7d8302dc95421471dca6fa0ad53541c Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 27 May 2025 12:16:06 +0400 Subject: [PATCH 178/405] fix: integration tests for multi-delivering --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 33 +-- ...-bus-oracle.submitExitRequestsData.test.ts | 36 ++- .../validators-exit-bus-multiple-delivery.ts | 266 ++++++++++++++++++ .../validators-exit-bus-single-delivery.ts | 96 +++++++ 4 files changed, 398 insertions(+), 33 deletions(-) create mode 100644 test/integration/validators-exit-bus-multiple-delivery.ts create mode 100644 test/integration/validators-exit-bus-single-delivery.ts diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 32610bc0c3..95ab439de3 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -104,15 +104,9 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa * @param remainingLimit Amount of requests that still can be processed at current day */ error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); - /** - * @notice Thrown when the number of requests submitted via submitExitRequestsData exceeds the allowed maxRequestsPerBatch. - * @param requestsCount The number of requests included in the current call. - * @param maxRequestsPerBatch The maximum number of requests allowed per call to submitExitRequestsData. + * @notice Thrown when submitting was not started for request */ - error MaxRequestsBatchSizeExceeded(uint256 requestsCount, uint256 maxRequestsPerBatch); - - error DeliveredIndexOutOfBounds(); error DeliveryWasNotStarted(); /// @dev Events @@ -259,7 +253,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa _checkExitSubmitted(requestStatus); _checkExitRequestData(request.data, request.dataFormat); - _checkMaxBatchSize(request.data); _checkContractVersion(requestStatus.contractVersion); uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; @@ -272,6 +265,7 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert RequestsAlreadyDelivered(); } + // take min between requestsToDeliver and maxBatchSize uint256 requestsToDeliver = _consumeLimit(undeliveredItemsCount); _processExitRequestsList(request.data, startIndex, requestsToDeliver); @@ -526,15 +520,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } - function _checkMaxBatchSize(bytes calldata requests) internal view { - uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); - uint256 requestsCount = requests.length / PACKED_REQUEST_LENGTH; - - if (requestsCount > maxRequestsPerBatch) { - revert MaxRequestsBatchSizeExceeded(requestsCount, maxRequestsPerBatch); - } - } - function _checkExitSubmitted(RequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion == 0) { revert ExitHashNotSubmitted(); @@ -585,10 +570,12 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); } - function _consumeLimit(uint256 requestsCount) internal returns (uint256 requestsToDeliver) { + function _consumeLimit(uint256 requestsCount) internal returns (uint256) { + uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); + ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (!exitRequestLimitData.isExitLimitSet()) { - return requestsCount; + return min(requestsCount, maxRequestsPerBatch); } uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); @@ -596,11 +583,13 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa if (limit == 0) { revert ExitRequestsLimit(requestsCount, 0); } + uint256 requestsToDeliver = min(min(limit, requestsCount), maxRequestsPerBatch); - requestsToDeliver = limit >= requestsCount ? requestsCount : limit; EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( exitRequestLimitData.updatePrevExitLimit(limit - requestsToDeliver, _getTimestamp()) ); + + return requestsToDeliver; } function _storeNewHashRequestStatus(bytes32 exitRequestsHash, RequestStatus memory requestStatus) internal { @@ -744,6 +733,10 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } } + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return a < b ? a : b; + } + /// Storage helpers function _storageRequestStatus() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index d661a4e335..d9a4a0ea3c 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -372,7 +372,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.currentExitRequestsLimit).to.equal(2); }); - it("Should process remaining requests after a day passes", async () => { + it("Should process remaining requests after 2 frames passes", async () => { const emitTx = await oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS); const timestamp = await oracle.getTime(); @@ -401,7 +401,11 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); - it("Should revert if maxBatchSize exceeded", async () => { + it("Should limit request by MAX_VALIDATORS_PER_BATCH if it is smaller than available vebo limit", async () => { + await consensus.advanceTimeBy(MAX_EXIT_REQUESTS_LIMIT * 4 * 12); + const data = await oracle.getExitRequestLimitFullInfo(); + expect(data.currentExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); + const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); await oracle.grantRole(role, authorizedEntity); @@ -427,19 +431,25 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHashRandom); - await expect(oracle.submitExitRequestsData(exitRequestRandom)) - .to.be.revertedWithCustomError(oracle, "MaxRequestsBatchSizeExceeded") - .withArgs(exitRequestsRandom.length, maxRequestsPerBatch); - }); + const tx = oracle.submitExitRequestsData(exitRequestRandom); + const timestamp = await oracle.getTime(); - it("Current limit should be equal to 0", async () => { - const data = await oracle.getExitRequestLimitFullInfo(); + for (let i = 0; i < maxRequestsPerBatch; i++) { + const request = exitRequestsRandom[i]; + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); + } - expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); - expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); - expect(data.frameDuration).to.equal(FRAME_DURATION); - expect(data.prevExitRequestsLimit).to.equal(0); - expect(data.currentExitRequestsLimit).to.equal(0); + const history = await oracle.getExitRequestsDeliveryHistory(exitRequestHashRandom); + + expect(history.length).to.be.equal(1); + expect(history[0].lastDeliveredExitDataIndex).to.be.equal(maxRequestsPerBatch - 1); + + const data2 = await oracle.getExitRequestLimitFullInfo(); + + expect(data2.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); + expect(data2.currentExitRequestsLimit).to.equal(1); }); it("Should set maxExitRequestsLimit equal to 0 and return as currentExitRequestsLimit type(uint256).max", async () => { diff --git a/test/integration/validators-exit-bus-multiple-delivery.ts b/test/integration/validators-exit-bus-multiple-delivery.ts new file mode 100644 index 0000000000..22fe0a3a88 --- /dev/null +++ b/test/integration/validators-exit-bus-multiple-delivery.ts @@ -0,0 +1,266 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { ValidatorsExitBusOracle } from "typechain-types"; + +import { advanceChainTime, de0x, ether, numberToHex } from "lib"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { Snapshot } from "test/suite"; + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + +describe("ValidatorsExitBus integration", () => { + let ctx: ProtocolContext; + let snapshot: string; + + let veb: ValidatorsExitBusOracle; + let hashReporter: HardhatEthersSigner; + let resumer: HardhatEthersSigner; + let agent: HardhatEthersSigner; + let limitManager: HardhatEthersSigner; + + const requests = [ + { + moduleId: 1, + nodeOpId: 1, + valIndex: 1, + valPubkey: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + { + moduleId: 2, + nodeOpId: 2, + valIndex: 2, + valPubkey: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + }, + { + moduleId: 3, + nodeOpId: 3, + valIndex: 3, + valPubkey: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }, + { + moduleId: 4, + nodeOpId: 4, + valIndex: 4, + valPubkey: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + }, + { + moduleId: 5, + nodeOpId: 5, + valIndex: 5, + valPubkey: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + }, + { + moduleId: 6, + nodeOpId: 6, + valIndex: 6, + valPubkey: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + moduleId: 7, + nodeOpId: 7, + valIndex: 7, + valPubkey: "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + }, + { + moduleId: 8, + nodeOpId: 8, + valIndex: 8, + valPubkey: "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + }, + { + moduleId: 9, + nodeOpId: 9, + valIndex: 9, + valPubkey: "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + }, + { + moduleId: 10, + nodeOpId: 10, + valIndex: 10, + valPubkey: "0x444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444", + }, + ]; + + const exitRequests = { + dataFormat: 1, + data: + "0x" + + requests + .map(({ moduleId, nodeOpId, valIndex, valPubkey }) => { + return encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey }); + }) + .join(""), + }; + + before(async () => { + ctx = await getProtocolContext(); + veb = ctx.contracts.validatorsExitBusOracle; + + [hashReporter, resumer, limitManager] = await ethers.getSigners(); + + agent = await ctx.getSigner("agent", ether("1")); + + // Grant role to submit exit hash + const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); + await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); + + const manageLimitRole = await veb.EXIT_REPORT_LIMIT_ROLE(); + await veb.connect(agent).grantRole(manageLimitRole, limitManager); + + if (await veb.isPaused()) { + const resumeRole = await veb.RESUME_ROLE(); + await veb.connect(agent).grantRole(resumeRole, resumer); + await veb.connect(resumer).resume(); + + expect(veb.isPaused()).to.be.false; + } + }); + + beforeEach(async () => (snapshot = await Snapshot.take())); + afterEach(async () => await Snapshot.restore(snapshot)); + + it("should submit hash and submit data in 4 deliveries", async () => { + // --- Setup exit limit --- + const maxLimit = 3; + const exitsPerFrame = 1; + const frameDurationSeconds = 48; + await veb.connect(limitManager).setExitRequestLimit(maxLimit, exitsPerFrame, frameDurationSeconds); + + // --- Prepare data --- + const exitRequestsHash: string = hashExitRequest(exitRequests); + + await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) + .to.emit(veb, "RequestsHashSubmitted") + .withArgs(exitRequestsHash); + + // --- 1st delivery: deliver maxLimit (3) requests --- + const tx1 = await veb.submitExitRequestsData(exitRequests); + const receipt1 = await tx1.wait(); + const block1 = await ethers.provider.getBlock(receipt1!.blockNumber); + const block1Timestamp = block1!.timestamp; + + // Validate logs & event count + const logs1 = receipt1!.logs.filter( + (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, + ); + expect(logs1.length).to.equal(maxLimit); + + for (let i = 0; i < maxLimit; i++) { + const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs1[i].data, logs1[i].topics); + console.log(decoded); + const expected = requests[i]; + expect(decoded[0]).to.equal(expected.moduleId); + expect(decoded[1]).to.equal(expected.nodeOpId); + expect(decoded[2]).to.equal(expected.valIndex); + expect(decoded[3]).to.equal(expected.valPubkey); + expect(decoded[4]).to.equal(block1Timestamp); + } + + // Validate delivery history + const deliveryHistory1 = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(deliveryHistory1.length).to.equal(1); + expect(deliveryHistory1[0].lastDeliveredExitDataIndex).to.equal(maxLimit - 1); + + // --- 2nd delivery: only 1 request can be processed after 48 seconds --- + await advanceChainTime(BigInt(frameDurationSeconds)); + + const tx2 = await veb.submitExitRequestsData(exitRequests); + const receipt2 = await tx2.wait(); + const block2 = await ethers.provider.getBlock(receipt2!.blockNumber); + const block2Timestamp = block2!.timestamp; + + const logs2 = receipt2!.logs.filter( + (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, + ); + expect(logs2.length).to.equal(1); + + const decoded2 = veb.interface.decodeEventLog("ValidatorExitRequest", logs2[0].data, logs2[0].topics); + const expected2 = requests[maxLimit]; + expect(decoded2[0]).to.equal(expected2.moduleId); + expect(decoded2[1]).to.equal(expected2.nodeOpId); + expect(decoded2[2]).to.equal(expected2.valIndex); + expect(decoded2[3]).to.equal(expected2.valPubkey); + expect(decoded2[4]).to.equal(block2Timestamp); + + const deliveryHistory2 = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(deliveryHistory2.length).to.equal(2); + expect(deliveryHistory2[1].lastDeliveredExitDataIndex).to.equal(maxLimit); + + // --- 3rd delivery: deliver remaining 6 requests after waiting (6 * 48) seconds --- + let remainingRequestsCount = requests.length - (maxLimit + 1); // 10 - 4 = 6 + await advanceChainTime(BigInt(frameDurationSeconds * remainingRequestsCount)); + + const tx3 = await veb.submitExitRequestsData(exitRequests); + const receipt3 = await tx3.wait(); + + const logs3 = receipt3!.logs.filter( + (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, + ); + + expect(logs3.length).to.equal(maxLimit); + + for (let i = 0; i < logs3.length; i++) { + const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs3[i].data, logs3[i].topics); + const expected = requests[4 + i]; + expect(decoded[0]).to.equal(expected.moduleId); + expect(decoded[1]).to.equal(expected.nodeOpId); + expect(decoded[2]).to.equal(expected.valIndex); + expect(decoded[3]).to.equal(expected.valPubkey); + } + + // --- 4th delivery: final 3 requests, but no need to increase time --- + + const currentLimit = (await veb.getExitRequestLimitFullInfo()).currentExitRequestsLimit; + expect(currentLimit).to.be.equal(0); + + remainingRequestsCount = requests.length - (maxLimit * 2 + 1); // 3 + + await advanceChainTime(BigInt(frameDurationSeconds * remainingRequestsCount)); + + const tx4 = await veb.submitExitRequestsData(exitRequests); + const receipt4 = await tx4.wait(); + const logs4 = receipt4!.logs.filter( + (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, + ); + expect(logs4.length).to.equal(maxLimit); + + for (let i = 0; i < logs4.length; i++) { + const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs4[i].data, logs4[i].topics); + const expected = requests[7 + i]; + expect(decoded[0]).to.equal(expected.moduleId); + expect(decoded[1]).to.equal(expected.nodeOpId); + expect(decoded[2]).to.equal(expected.valIndex); + expect(decoded[3]).to.equal(expected.valPubkey); + } + + // --- Validate total logs delivered = 10 requests --- + const totalDelivered = logs1.length + logs2.length + logs3.length + logs4.length; + expect(totalDelivered).to.equal(requests.length); + + // --- Validate delivery history entries: 4 deliveries --- + const finalHistory = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(finalHistory.length).to.equal(4); + expect(finalHistory[3].lastDeliveredExitDataIndex).to.equal(requests.length - 1); + }); +}); diff --git a/test/integration/validators-exit-bus-single-delivery.ts b/test/integration/validators-exit-bus-single-delivery.ts new file mode 100644 index 0000000000..82803ea947 --- /dev/null +++ b/test/integration/validators-exit-bus-single-delivery.ts @@ -0,0 +1,96 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { ValidatorsExitBusOracle } from "typechain-types"; + +import { de0x, ether, numberToHex } from "lib"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { Snapshot } from "test/suite"; + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + +describe("ValidatorsExitBus integration", () => { + let ctx: ProtocolContext; + let snapshot: string; + + let veb: ValidatorsExitBusOracle; + let hashReporter: HardhatEthersSigner; + let resumer: HardhatEthersSigner; + let agent: HardhatEthersSigner; + + const moduleId = 1; + const nodeOpId = 2; + const valIndex = 3; + const pubkey = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + const exitRequestPacked = "0x" + encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey: pubkey }); + + before(async () => { + ctx = await getProtocolContext(); + veb = ctx.contracts.validatorsExitBusOracle; + + [hashReporter, resumer] = await ethers.getSigners(); + + agent = await ctx.getSigner("agent", ether("1")); + + // Grant role to submit exit hash + const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); + await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); + + if (await veb.isPaused()) { + const resumeRole = await veb.RESUME_ROLE(); + await veb.connect(agent).grantRole(resumeRole, resumer); + await veb.connect(resumer).resume(); + + expect(veb.isPaused()).to.be.false; + } + }); + + beforeEach(async () => (snapshot = await Snapshot.take())); + afterEach(async () => await Snapshot.restore(snapshot)); + + it("should submit hash and data, updating delivery history", async () => { + const dataFormat = 1; + + const exitRequest = { dataFormat, data: exitRequestPacked }; + + const exitRequestsHash: string = hashExitRequest(exitRequest); + + await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) + .to.emit(veb, "RequestsHashSubmitted") + .withArgs(exitRequestsHash); + + const tx = await veb.submitExitRequestsData(exitRequest); + const receipt = await tx.wait(); + const block = await receipt?.getBlock(); + const blockTimestamp = block!.timestamp; + + await expect(tx) + .to.emit(veb, "ValidatorExitRequest") + .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); + + const deliveryHistory = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(deliveryHistory.length).to.equal(1); + expect(deliveryHistory[0].lastDeliveredExitDataIndex).to.equal(0); + }); +}); From cec0458367302bb811edca249b5b89eeca48643a Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 27 May 2025 11:40:31 +0200 Subject: [PATCH 179/405] fix: update agent reference in triggerable withdrawals deployment --- scripts/triggerable-withdrawals/tw-deploy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 372016f592..471e8e6055 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -38,6 +38,8 @@ async function main() { const state = readNetworkState(); persistNetworkState(state); + const agent = state["app:aragon-agent"].proxy.address; + log(`Using agent: ${agent}`); // Read contracts addresses from config const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); @@ -118,7 +120,7 @@ async function main() { Sk.triggerableWithdrawalsGateway, "TriggerableWithdrawalsGateway", deployer, - [deployer, locator.address, 13000, 1, 48], + [agent, locator.address, 13000, 1, 48], ); log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); log.emptyLine(); From 9a3956f9fa5f2b93338a568a6e2475cc2c125ffc Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 27 May 2025 16:51:14 +0400 Subject: [PATCH 180/405] fix: naming & removed extra checks & IValidatorsExitBus --- .../0.8.9/interfaces/IValidatorsExitBus.sol | 55 ------------------- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 47 ++++++++-------- .../oracle/validator-exit-bus.helpers.test.ts | 18 ------ 3 files changed, 22 insertions(+), 98 deletions(-) delete mode 100644 contracts/0.8.9/interfaces/IValidatorsExitBus.sol diff --git a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol b/contracts/0.8.9/interfaces/IValidatorsExitBus.sol deleted file mode 100644 index 7d8c038da2..0000000000 --- a/contracts/0.8.9/interfaces/IValidatorsExitBus.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.9; - -interface IValidatorsExitBus { - struct ExitRequestsData { - bytes data; - uint256 dataFormat; - } - - struct DeliveryHistory { - // index in array of requests - uint32 lastDeliveredExitDataIndex; - uint32 timestamp; - } - - function submitExitRequestsHash(bytes32 exitReportHash) external; - - function submitExitRequestsData(ExitRequestsData calldata request) external; - - function triggerExits( - ExitRequestsData calldata exitsData, - uint256[] calldata exitDataIndexes, - address refundRecipient - ) external payable; - - function setExitRequestLimit(uint256 maxExitRequests, uint256 exitsPerFrame, uint256 frameDuration) external; - - function getExitRequestLimitFullInfo() - external - view - returns ( - uint256 maxExitRequestsLimit, - uint256 exitsPerFrame, - uint256 frameDuration, - uint256 prevExitRequestsLimit, - uint256 currentExitRequestsLimit - ); - - function getExitRequestsDeliveryHistory( - bytes32 exitRequestsHash - ) external view returns (DeliveryHistory[] memory history); - - function unpackExitRequest( - bytes calldata exitRequests, - uint256 dataFormat, - uint256 index - ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); - - function resume() external; - - function pauseFor(uint256 _duration) external; - - function pauseUntil(uint256 _pauseUntilInclusive) external; -} diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 9be2b9e1f9..67471162e5 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -8,7 +8,6 @@ import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; import {Versioned} from "../utils/Versioned.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; import {PausableUntil} from "../utils/PausableUntil.sol"; -import {IValidatorsExitBus} from "../interfaces/IValidatorsExitBus.sol"; interface ITriggerableWithdrawalsGateway { struct ValidatorData { @@ -28,9 +27,8 @@ interface ITriggerableWithdrawalsGateway { * @title ValidatorsExitBus * @notice An on-chain contract that serves as the central infrastructure for managing validator exit requests. * It stores report hashes, emits exit events, and maintains data and tools that enables anyone to prove a validator was requested to exit. - * Unlike VEBO, it supports exit reports from a wide range of entities. */ -contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, PausableUntil, Versioned { +contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; @@ -139,19 +137,39 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa */ event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); + struct ExitRequestsData { + bytes data; + uint256 dataFormat; + } + struct ValidatorData { uint256 nodeOpId; uint256 moduleId; uint256 valIndex; bytes pubkey; } + + // RequestStatus stores the last delivered index of the request, timestamp of delivery, and deliveryHistory length (number of deliveries). + // If a request is fully delivered in one step (as with oracle requests, which can't be delivered partially), + // only RequestStatus is used for efficiency. + // If a request is delivered in parts (e.g., due to limit constraints), + // DeliveryHistory[] stores full delivery records in addition to RequestStatus. + // If deliveryHistoryLength == 1, delivery info is read from RequestStatus; otherwise, from DeliveryHistory[]. + // Both mappings use the same key (exitRequestsHash). + struct RequestStatus { uint32 contractVersion; uint32 deliveryHistoryLength; - uint32 lastDeliveredExitDataIndex; // index of validator, maximum 600 for example + uint32 lastDeliveredExitDataIndex; // index of validator in request uint32 lastDeliveredExitDataTimestamp; } + struct DeliveryHistory { + // index in array of requests + uint32 lastDeliveredExitDataIndex; + uint32 timestamp; + } + /// @notice An ACL role granting the permission to submit a hash of the exit requests data bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); /// @notice An ACL role granting the permission to set maximum exit request limit and the frame limit restoring values @@ -199,14 +217,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa // Storage slot for mapping(bytes32 => DeliveryHistory[]), keyed by exitRequestsHash bytes32 internal constant DELIVERY_HISTORY_POSITION = keccak256("lido.ValidatorsExitBus.deliveryHistory"); - // RequestStatus stores the last delivered index of the request, timestamp of delivery, and deliveryHistory length (number of deliveries). - // If a request is fully delivered in one step (as with oracle requests, which can't be delivered partially), - // only RequestStatus is used for efficiency. - // If a request is delivered in parts (e.g., due to limit constraints), - // DeliveryHistory[] stores full delivery records in addition to RequestStatus. - // If deliveryHistoryLength == 1, delivery info is read from RequestStatus; otherwise, from DeliveryHistory[]. - // Both mappings use the same key (exitRequestsHash). - uint256 public constant EXIT_TYPE = 2; /// @dev Ensures the contract’s ETH balance is unchanged. @@ -294,9 +304,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa newLastDeliveredIndex, _getTimestamp() ); - - // TODO: is this check extra? - _validateDeliveryState(exitRequestsHash, requestStatus); } /** @@ -443,8 +450,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa revert ExitHashNotSubmitted(); } - _validateDeliveryState(exitRequestsHash, storedRequest); - if (storedRequest.deliveryHistoryLength == 0) { DeliveryHistory[] memory deliveryHistory; return deliveryHistory; @@ -569,8 +574,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa } function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { - require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_EXITS_PER_FRAME"); - uint256 timestamp = _getTimestamp(); EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( @@ -653,12 +656,6 @@ contract ValidatorsExitBus is IValidatorsExitBus, AccessControlEnumerable, Pausa ); } - function _validateDeliveryState(bytes32 hash, RequestStatus storage status) internal view { - if (status.deliveryHistoryLength > 1) { - require(_storageDeliveryHistory()[hash].length == status.deliveryHistoryLength, "DeliveryHistoryMismatch"); - } - } - /// Methods for reading data from tightly packed validator exit requests /// Format DATA_FORMAT_LIST = 1; diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 3ee091707c..df0c10d363 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -219,24 +219,6 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { expect(secondDelivery.lastDeliveredExitDataIndex).to.equal(1); expect(secondDelivery.timestamp).to.equal(timestamp + 1n); }); - - it("reverts if deliveryHistoryLength > 1 but actual history array is smaller", async () => { - const hash = keccak256("0xdead"); - const contractVersion = 42; - - await oracle.storeNewHashRequestStatus( - hash, - contractVersion, - 2, // deliveryHistoryLength = 2 - 5, - 123456, - ); - - // Only add 1 entry (mismatch) - await oracle.storeDeliveryEntry(hash, 1, 123456); - - await expect(oracle.getExitRequestsDeliveryHistory(hash)).to.be.revertedWith("DeliveryHistoryMismatch"); - }); }); context("_updateRequestStatus", () => { From 78283f20c22a7bee5f65465f11e2ee2fe216e3b6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 27 May 2025 16:44:11 +0200 Subject: [PATCH 181/405] refactor: remove deprecated stuck penalty events and constants from NodeOperatorsRegistry --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index b029bec12a..3f9ee9375b 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -52,13 +52,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { event TotalSigningKeysCountChanged(uint256 indexed nodeOperatorId, uint256 totalValidatorsCount); event NonceChanged(uint256 nonce); - event StuckPenaltyDelayChanged(uint256 stuckPenaltyDelay); - event StuckPenaltyStateChanged( - uint256 indexed nodeOperatorId, - uint256 stuckValidatorsCount, - uint256 refundedValidatorsCount, - uint256 stuckPenaltyEndTimestamp - ); event TargetValidatorsCountChanged(uint256 indexed nodeOperatorId, uint256 targetValidatorsCount, uint256 targetLimitMode); event NodeOperatorPenalized(address indexed recipientAddress, uint256 sharesPenalizedAmount); event NodeOperatorPenaltyCleared(uint256 indexed nodeOperatorId); @@ -124,22 +117,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @dev actual operators's number of keys which could be deposited uint8 internal constant MAX_VALIDATORS_COUNT_OFFSET = 2; - // StuckPenaltyStats - /// @dev stuck keys count from oracle report - /// @dev [DEPRECATED] - uint8 internal constant STUCK_VALIDATORS_COUNT_OFFSET = 0; - /// @dev refunded keys count from dao - /// @dev [DEPRECATED] - uint8 internal constant REFUNDED_VALIDATORS_COUNT_OFFSET = 1; - /// @dev extra penalty time after stuck keys resolved (refunded and/or exited) - /// @notice field is also used as flag for "half-cleaned" penalty status - /// Operator is PENALIZED if `STUCK_VALIDATORS_COUNT > REFUNDED_VALIDATORS_COUNT` or - /// `STUCK_VALIDATORS_COUNT <= REFUNDED_VALIDATORS_COUNT && STUCK_PENALTY_END_TIMESTAMP <= refund timestamp + STUCK_PENALTY_DELAY` - /// When operator refund all stuck validators and time has pass STUCK_PENALTY_DELAY, but STUCK_PENALTY_END_TIMESTAMP not zeroed, - /// then Operator can receive rewards but can't get new deposits until the new Oracle report or `clearNodeOperatorPenalty` is called. - /// @dev [DEPRECATED] - uint8 internal constant STUCK_PENALTY_END_TIMESTAMP_OFFSET = 2; - // Summary SigningKeysStats uint8 internal constant SUMMARY_MAX_VALIDATORS_COUNT_OFFSET = 0; /// @dev Number of keys of all operators which were in the EXITED state for all time @@ -176,9 +153,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // bytes32 internal constant TYPE_POSITION = keccak256("lido.NodeOperatorsRegistry.type"); bytes32 internal constant TYPE_POSITION = 0xbacf4236659a602d72c631ba0b0d67ec320aaf523f3ae3590d7faee4f42351d0; - // bytes32 internal constant STUCK_PENALTY_DELAY_POSITION = keccak256("lido.NodeOperatorsRegistry.stuckPenaltyDelay"); - bytes32 internal constant STUCK_PENALTY_DELAY_POSITION = 0x8e3a1f3826a82c1116044b334cae49f3c3d12c3866a1c4b18af461e12e58a18e; - // bytes32 internal constant REWARD_DISTRIBUTION_STATE = keccak256("lido.NodeOperatorsRegistry.rewardDistributionState"); bytes32 internal constant REWARD_DISTRIBUTION_STATE = 0x4ddbb0dcdc5f7692e494c15a7fca1f9eb65f31da0b5ce1c3381f6a1a1fd579b6; @@ -516,7 +490,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 validatorsCount; uint256 _nodeOperatorIdsOffset; uint256 _exitedValidatorsCountsOffset; - /// @dev see comments for `updateStuckValidatorsCount` + assembly { _nodeOperatorIdsOffset := add(calldataload(4), 36) // arg1 calldata offset + 4 (signature len) + 32 (length slot) _exitedValidatorsCountsOffset := add(calldataload(36), 36) // arg2 calldata offset + 4 (signature len) + 32 (length slot)) @@ -581,7 +555,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _onlyExistedNodeOperator(_nodeOperatorId); _auth(STAKING_ROUTER_ROLE); - // _updateStuckValidatorsCount(_nodeOperatorId, _stuckValidatorsCount); // removed _updateExitedValidatorsCount(_nodeOperatorId, _exitedValidatorsCount, true /* _allowDecrease */ ); _increaseValidatorsKeysNonce(); } @@ -1407,7 +1380,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { (address[] memory recipients, uint256[] memory shares,) = getRewardsDistribution(sharesToDistribute); - 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; @@ -1415,9 +1387,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { 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) { From a5a58c1ee25b32ae68a6e65f9a43af4de3d04c98 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 17:01:39 +0200 Subject: [PATCH 182/405] feat: sanity checks for exit requests delivery history --- .../0.8.25/ValidatorExitDelayVerifier.sol | 19 +++++++ .../0.8.25/validatorExitDelayVerifier.test.ts | 56 ++++++++++++------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 7fb117afed..92cfe391c2 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -104,6 +104,8 @@ contract ValidatorExitDelayVerifier { uint256 eligibleExitRequestTimestamp ); error KeyWasNotUnpacked(uint256 keyIndex); + error NonMonotonicDeliveryHistory(uint256 index); + error EmptyDeliveryHistory(); /** * @dev The previous and current forks can be essentially the same. @@ -373,6 +375,23 @@ contract ValidatorExitDelayVerifier { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); DeliveryHistory[] memory history = vebo.getExitRequestsDeliveryHistory(exitRequestsHash); + if (history.length == 0) { + revert EmptyDeliveryHistory(); + } + + // Sanity check, delivery history is strictly monotonically increasing. + if (history.length > 1) { + for (uint256 i = 1; i < history.length; i++) { + // strictly increasing on both keys index and timestamps + if ( + history[i].lastDeliveredKeyIndex <= history[i - 1].lastDeliveredKeyIndex || + history[i].timestamp <= history[i - 1].timestamp + ) { + revert NonMonotonicDeliveryHistory(i); + } + } + } + return history; } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index b4a08b6fb2..7502bec10b 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -516,24 +516,11 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); }); - it("reverts with 'KeyWasNotUnpacked' if exit request index is not in delivery history", async () => { - const nodeOpId = 2; + it("reverts with 'EmptyDeliveryHistory' if exit request index is not in delivery history", async () => { const exitRequests: ExitRequest[] = [ { moduleId: 1, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - { - moduleId: 2, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - { - moduleId: 3, - nodeOpId, + nodeOpId: 1, valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, }, @@ -541,8 +528,9 @@ describe("ValidatorExitDelayVerifier.sol", () => { const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - const unpackedExitRequestIndex = 2; + const unpackedExitRequestIndex = 0; // Report not unpacked. await vebo.setExitRequests(encodedExitRequestsHash, [], exitRequests); @@ -554,9 +542,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); - - const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( @@ -565,7 +551,37 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); + }); + + it("reverts with 'KeyWasNotUnpacked' if exit request index is not in delivery history", async () => { + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId: 1, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + { + moduleId: 2, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + { + moduleId: 3, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + const unpackedExitRequestIndex = 2; // Report not fully unpacked. await vebo.setExitRequests(encodedExitRequestsHash, [{ timestamp: 0n, lastDeliveredKeyIndex: 1n }], exitRequests); From dbce5006aafb9f008792986a02a11c6b8788aa41 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 17:07:04 +0200 Subject: [PATCH 183/405] feat: improve exitRequestIndex comment --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 92cfe391c2..39d588930e 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -16,7 +16,7 @@ struct ExitRequestData { } struct ValidatorWitness { - // The index of an exit request in the VEBO exit requests data + // The index of an exit request in the VEB exit requests data uint32 exitRequestIndex; // -------------------- Validator details ------------------- bytes32 withdrawalCredentials; From bb9afd58173f0155fae1d30bff83ae7cafaa0397 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 17:22:00 +0200 Subject: [PATCH 184/405] feat: improve ValidatorExitDelayVerifier description --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 39d588930e..a01be19665 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -43,8 +43,9 @@ struct HistoricalHeaderWitness { /** * @title ValidatorExitDelayVerifier - * @notice Verifies validator proofs to ensure they are unexited after an exit request. - * Allows permissionless report the status of validators which are assumed to have exited but have not. + * @notice Allows permissionless reporting of exit delays for validators that have been requested to exit + * via the Validator Exit Bus. + * * @dev Uses EIP-4788 to confirm the correctness of a given beacon block root. */ contract ValidatorExitDelayVerifier { @@ -156,8 +157,9 @@ contract ValidatorExitDelayVerifier { // ------------------------- External Functions ------------------------- /** - * @notice Verifies that provided validators are still active (not exited) at the given beacon block. - * If they are unexpectedly still active, it reports them back to the Staking Router. + * @notice Verifies that the provided validators were not requested to exit on the CL after a VEB exit request. + * Reports exit delays to the Staking Router. + * @dev Ensures that `exitEpoch` is equal to `FAR_FUTURE_EPOCH` at the given beacon block. * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited. @@ -203,8 +205,10 @@ contract ValidatorExitDelayVerifier { } /** - * @notice Verifies historical blocks (via historical_summaries) and checks that certain validators - * are still active at that old block. If they're still active, it reports them to Staking Router. + * @notice Verifies that the provided validators were not requested to exit on the CL after a VEB exit request. + * Reports exit delays to the Staking Router. + * @dev Ensures that `exitEpoch` is equal to `FAR_FUTURE_EPOCH` at the given beacon block. + * @dev Verifies historical blocks (via historical_summaries). * @dev The oldBlock.header must have slot >= FIRST_SUPPORTED_SLOT. * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. From 7dd0781afa24b79400ba02a2791b2108db8a607f Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 17:42:59 +0200 Subject: [PATCH 185/405] feat: fix naming in ValidatorExitDelayVerifier contract --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index a01be19665..f1de858c7b 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -171,16 +171,16 @@ contract ValidatorExitDelayVerifier { ) external { _verifyBeaconBlockRoot(beaconBlock); - IValidatorsExitBus vebo = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); + IValidatorsExitBus veb = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(veb, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { ValidatorWitness calldata witness = validatorWitnesses[i]; - (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = vebo.unpackExitRequest( + (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = veb.unpackExitRequest( exitRequests.data, exitRequests.dataFormat, witness.exitRequestIndex @@ -224,16 +224,16 @@ contract ValidatorExitDelayVerifier { _verifyBeaconBlockRoot(beaconBlock); _verifyHistoricalBeaconBlockRoot(beaconBlock, oldBlock); - IValidatorsExitBus vebo = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); + IValidatorsExitBus veb = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(vebo, exitRequests); + DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(veb, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { ValidatorWitness calldata witness = validatorWitnesses[i]; - (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = vebo.unpackExitRequest( + (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) = veb.unpackExitRequest( exitRequests.data, exitRequests.dataFormat, witness.exitRequestIndex @@ -373,11 +373,11 @@ contract ValidatorExitDelayVerifier { } function _getExitRequestDeliveryHistory( - IValidatorsExitBus vebo, + IValidatorsExitBus veb, ExitRequestData calldata exitRequests ) internal view returns (DeliveryHistory[] memory) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); - DeliveryHistory[] memory history = vebo.getExitRequestsDeliveryHistory(exitRequestsHash); + DeliveryHistory[] memory history = veb.getExitRequestsDeliveryHistory(exitRequestsHash); if (history.length == 0) { revert EmptyDeliveryHistory(); From 752d54cb60322d742f42d2a81f3c57b2bf2c725e Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 17:46:12 +0200 Subject: [PATCH 186/405] feat: fix description in ValidatorExitDelayVerifier contract methods --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index f1de858c7b..98f484e477 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -334,7 +334,7 @@ contract ValidatorExitDelayVerifier { /** * @dev Determines how many seconds have passed since a validator was first eligible - * to exit after ValidatorsExitBusOracle exit request. + * to exit after VEB exit request. * @return uint256 The elapsed seconds since the earliest eligible exit request time. */ function _getSecondsSinceExitRequestEligible( From 72c209a608b5809fccb039588b0ad9da2edcd177 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 27 May 2025 19:15:28 +0400 Subject: [PATCH 187/405] fix: naming frameDuration -> frameDurationInSec & TW_EXIT_LIMIT_MANAGER_ROLE --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 38 ++++---- contracts/0.8.9/lib/ExitLimitUtils.sol | 18 ++-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 28 +++--- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 12 +-- .../steps/0090-deploy-non-aragon-contracts.ts | 4 +- .../0120-initialize-non-aragon-contracts.ts | 4 +- .../contracts/ExitLimitUtils__Harness.sol | 10 +- ...TriggerableWithdrawalsGateway__Harness.sol | 4 +- test/0.8.9/lib/exitLimitUtils.test.ts | 92 +++++++++---------- ...idator-exit-bus-oracle.finalize_v2.test.ts | 2 +- ...-bus-oracle.submitExitRequestsData.test.ts | 8 +- ...awalGateway.triggerFullWithdrawals.test.ts | 14 +-- test/deploy/validatorExitBusOracle.ts | 6 +- .../validators-exit-bus-multiple-delivery.ts | 10 +- 14 files changed, 124 insertions(+), 126 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 0437e7e8fa..711dc06249 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -56,13 +56,13 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { */ error FeeRefundFailed(); /** - * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. - * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @notice Emitted when limits configs are set. + * @param maxExitRequestsLimit The maximum number of exit requests. * @param exitsPerFrame The number of exits that can be restored per frame. - * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ - event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); + event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); /** * @notice Thrown when remaining exit requests limit is not enough to cover sender requests * @param requestsCount Amount of requests that were sent for processing @@ -78,7 +78,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { } bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); - bytes32 public constant TW_EXIT_REPORT_LIMIT_ROLE = keccak256("TW_EXIT_REPORT_LIMIT_ROLE"); + bytes32 public constant TW_EXIT_LIMIT_MANAGER_ROLE = keccak256("TW_EXIT_LIMIT_MANAGER_ROLE"); bytes32 public constant TWR_LIMIT_POSITION = keccak256("lido.TriggerableWithdrawalsGateway.maxExitRequestLimit"); @@ -101,13 +101,13 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { address lidoLocator, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration + uint256 frameDurationInSec ) { if (admin == address(0)) revert AdminCannotBeZero(); LOCATOR = ILidoLocator(lidoLocator); _setupRole(DEFAULT_ADMIN_ROLE, admin); - _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } /** @@ -156,22 +156,22 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { /** * @notice Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. - * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @param maxExitRequestsLimit The maximum number of exit requests. * @param exitsPerFrame The number of exits that can be restored per frame. - * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ - function setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) + function setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec) external - onlyRole(TW_EXIT_REPORT_LIMIT_ROLE) + onlyRole(TW_EXIT_LIMIT_MANAGER_ROLE) { - _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } /** * @notice Returns information about current limits data * @return maxExitRequestsLimit Maximum exit requests limit * @return exitsPerFrame The number of exits that can be restored per frame. - * @return frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @return frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. * @return prevExitRequestsLimit Limit left after previous requests * @return currentExitRequestsLimit Current exit requests limit */ @@ -181,7 +181,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { returns ( uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration, + uint256 frameDurationInSec, uint256 prevExitRequestsLimit, uint256 currentExitRequestsLimit ) @@ -189,7 +189,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { ExitRequestLimitData memory exitRequestLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); maxExitRequestsLimit = exitRequestLimitData.maxExitRequestsLimit; exitsPerFrame = exitRequestLimitData.exitsPerFrame; - frameDuration = exitRequestLimitData.frameDuration; + frameDurationInSec = exitRequestLimitData.frameDurationInSec; prevExitRequestsLimit = exitRequestLimitData.prevExitRequestsLimit; currentExitRequestsLimit = exitRequestLimitData.isExitLimitSet() @@ -241,20 +241,18 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { return block.timestamp; // solhint-disable-line not-rely-on-time } - function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) + function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec) internal { - require(maxExitRequestsLimit >= exitsPerFrame, "TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); - uint256 timestamp = _getTimestamp(); TWR_LIMIT_POSITION.setStorageExitRequestLimit( TWR_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( - maxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp + maxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp ) ); - emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } function _checkExitRequestLimit(uint256 requestsCount) internal { diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index 5ae982cd3e..6ac01f201c 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -6,7 +6,7 @@ struct ExitRequestLimitData { uint32 maxExitRequestsLimit; // Maximum limit uint32 prevExitRequestsLimit; // Limit left after previous requests uint32 prevTimestamp; // Timestamp of the last update - uint32 frameDuration; // Seconds that should pass to restore part of exits + uint32 frameDurationInSec; // Seconds that should pass to restore part of exits uint32 exitsPerFrame; // Restored exits per frame } @@ -38,11 +38,11 @@ library ExitLimitUtils { ) internal pure returns (uint256 currentLimit) { uint256 secondsPassed = timestamp - _data.prevTimestamp; - if (secondsPassed < _data.frameDuration || _data.exitsPerFrame == 0) { + if (secondsPassed < _data.frameDurationInSec || _data.exitsPerFrame == 0) { return _data.prevExitRequestsLimit; } - uint256 framesPassed = secondsPassed / _data.frameDuration; + uint256 framesPassed = secondsPassed / _data.frameDurationInSec; uint256 restoredLimit = framesPassed * _data.exitsPerFrame; uint256 newLimit = _data.prevExitRequestsLimit + restoredLimit; @@ -61,8 +61,8 @@ library ExitLimitUtils { require(_data.maxExitRequestsLimit >= newExitRequestLimit, "LIMIT_EXCEEDED"); uint256 secondsPassed = timestamp - _data.prevTimestamp; - uint256 framesPassed = secondsPassed / _data.frameDuration; - uint32 passedTime = uint32(framesPassed) * _data.frameDuration; + uint256 framesPassed = secondsPassed / _data.frameDurationInSec; + uint32 passedTime = uint32(framesPassed) * _data.frameDurationInSec; _data.prevExitRequestsLimit = uint32(newExitRequestLimit); _data.prevTimestamp += passedTime; @@ -74,18 +74,18 @@ library ExitLimitUtils { ExitRequestLimitData memory _data, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration, + uint256 frameDurationInSec, uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { // TODO: do we allow maxExitRequests be equal to zero? // require(maxExitRequests != 0, "ZERO_MAX_LIMIT");; require(maxExitRequestsLimit <= type(uint32).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); - require(frameDuration <= type(uint32).max, "TOO_LARGE_FRAME_DURATION"); + require(frameDurationInSec <= type(uint32).max, "TOO_LARGE_FRAME_DURATION"); require(exitsPerFrame <= maxExitRequestsLimit, "TOO_LARGE_EXITS_PER_FRAME"); - require(frameDuration != 0, "ZERO_FRAME_DURATION"); + require(frameDurationInSec != 0, "ZERO_FRAME_DURATION"); _data.exitsPerFrame = uint32(exitsPerFrame); - _data.frameDuration = uint32(frameDuration); + _data.frameDurationInSec = uint32(frameDurationInSec); if ( // new maxExitRequestsLimit is smaller than prev remaining limit diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d5021c4589..fbf1414e4a 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -132,12 +132,12 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned uint256 timestamp ); /** - * @notice Emitted when maximum exit request limit and the frame during which a portion of the limit can be restored set. - * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @notice Emitted when limits configs are set. + * @param maxExitRequestsLimit The maximum number of exit requests. * @param exitsPerFrame The number of exits that can be restored per frame. - * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ - event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration); + event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); struct ExitRequestsData { bytes data; @@ -381,23 +381,23 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned /** * @notice Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. - * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDuration) + * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDurationInSec) * @param exitsPerFrame The number of exits that can be restored per frame. - * @param frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ function setExitRequestLimit( uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration + uint256 frameDurationInSec ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { - _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } /** * @notice Returns information about current limits data * @return maxExitRequestsLimit Maximum exit requests limit * @return exitsPerFrame The number of exits that can be restored per frame. - * @return frameDuration The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. + * @return frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. * @return prevExitRequestsLimit Limit left after previous requests * @return currentExitRequestsLimit Current exit requests limit */ @@ -407,7 +407,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned returns ( uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration, + uint256 frameDurationInSec, uint256 prevExitRequestsLimit, uint256 currentExitRequestsLimit ) @@ -415,7 +415,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); maxExitRequestsLimit = exitRequestLimitData.maxExitRequestsLimit; exitsPerFrame = exitRequestLimitData.exitsPerFrame; - frameDuration = exitRequestLimitData.frameDuration; + frameDurationInSec = exitRequestLimitData.frameDurationInSec; prevExitRequestsLimit = exitRequestLimitData.prevExitRequestsLimit; currentExitRequestsLimit = exitRequestLimitData.isExitLimitSet() @@ -574,19 +574,19 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned return MAX_VALIDATORS_PER_BATCH_POSITION.getStorageUint256(); } - function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDuration) internal { + function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec) internal { uint256 timestamp = _getTimestamp(); EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( maxExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, timestamp ) ); - emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDuration); + emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } function _consumeLimit(uint256 requestsCount, function(uint256, uint256) internal pure returns(uint256) applyLimit) internal returns (uint256 requestsLimitedCount) { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 403fdc1319..046b026e67 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -73,7 +73,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { uint256 maxValidatorsPerBatch, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration + uint256 frameDurationInSec ) external { if (admin == address(0)) revert AdminCannotBeZero(); _setupRole(DEFAULT_ADMIN_ROLE, admin); @@ -81,7 +81,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { _pauseFor(PAUSE_INFINITELY); _initialize(consensusContract, consensusVersion, lastProcessingRefSlot); - _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDuration); + _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } /** @@ -93,20 +93,20 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { uint256 maxValidatorsPerBatch, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration + uint256 frameDurationInSec ) external { - _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDuration); + _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } function _initialize_v2( uint256 maxValidatorsPerBatch, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration + uint256 frameDurationInSec ) internal { _updateContractVersion(2); _setMaxRequestsPerBatch(maxValidatorsPerBatch); - _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDuration); + _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } /// diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 57cb846847..88872a96f3 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -224,13 +224,13 @@ export async function main() { // Deploy Triggerable Withdrawals Gateway const maxExitRequestsLimit = 13000; const exitsPerFrame = 1; - const frameDuration = 48; + const frameDurationInSec = 48; const triggerableWithdrawalsGateway = await deployWithoutProxy( Sk.triggerableWithdrawalsGateway, "TriggerableWithdrawalsGateway", deployer, - [admin, locator.address, maxExitRequestsLimit, exitsPerFrame, frameDuration], + [admin, locator.address, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec], ); // Update LidoLocator with valid implementation diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index f4b10394a7..4a489939c3 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -100,7 +100,7 @@ export async function main() { const maxValidatorsPerBatch = 600; const maxExitRequestsLimit = 13000; const exitsPerFrame = 1; - const frameDuration = 48; + const frameDurationInSec = 48; await makeTx( validatorsExitBusOracle, "initialize", @@ -112,7 +112,7 @@ export async function main() { maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, ], { from: deployer }, ); diff --git a/test/0.8.9/contracts/ExitLimitUtils__Harness.sol b/test/0.8.9/contracts/ExitLimitUtils__Harness.sol index e44313e633..cacfa3f78f 100644 --- a/test/0.8.9/contracts/ExitLimitUtils__Harness.sol +++ b/test/0.8.9/contracts/ExitLimitUtils__Harness.sol @@ -28,12 +28,12 @@ contract ExitLimitUtils__Harness { uint32 maxExitRequestsLimit, uint32 prevExitRequestsLimit, uint32 exitsPerFrame, - uint32 frameDuration, + uint32 frameDurationInSec, uint32 timestamp ) external { state.maxExitRequestsLimit = maxExitRequestsLimit; state.exitsPerFrame = exitsPerFrame; - state.frameDuration = frameDuration; + state.frameDurationInSec = frameDurationInSec; state.prevExitRequestsLimit = prevExitRequestsLimit; state.prevTimestamp = timestamp; } @@ -44,7 +44,7 @@ contract ExitLimitUtils__Harness { state.maxExitRequestsLimit, state.prevExitRequestsLimit, state.prevTimestamp, - state.frameDuration, + state.frameDurationInSec, state.exitsPerFrame ); } @@ -63,10 +63,10 @@ contract ExitLimitUtils__Harness { function setExitLimits( uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration, + uint256 frameDurationInSec, uint256 timestamp ) external view returns (ExitRequestLimitData memory) { - return state.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + return state.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); } function isExitLimitSet() external view returns (bool) { diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol index 3a7440872e..051bc9f80a 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__Harness.sol @@ -10,8 +10,8 @@ contract TriggerableWithdrawalsGateway__Harness is TriggerableWithdrawalsGateway address lidoLocator, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, - uint256 frameDuration - ) TriggerableWithdrawalsGateway(admin, lidoLocator, maxExitRequestsLimit, exitsPerFrame, frameDuration) {} + uint256 frameDurationInSec + ) TriggerableWithdrawalsGateway(admin, lidoLocator, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec) {} function getTimestamp() external view returns (uint256) { return _time; diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts index 11ca1f9ba4..cc3b0ad0f5 100644 --- a/test/0.8.9/lib/exitLimitUtils.test.ts +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -7,7 +7,7 @@ interface ExitRequestLimitData { maxExitRequestsLimit: bigint; prevExitRequestsLimit: bigint; prevTimestamp: bigint; - frameDuration: bigint; + frameDurationInSec: bigint; exitsPerFrame: bigint; } @@ -28,7 +28,7 @@ describe("ExitLimitUtils.sol", () => { maxExitRequestsLimit: 0n, prevExitRequestsLimit: 0n, prevTimestamp: 0n, - frameDuration: 0n, + frameDurationInSec: 0n, exitsPerFrame: 0n, }; @@ -38,7 +38,7 @@ describe("ExitLimitUtils.sol", () => { expect(result.maxExitRequestsLimit).to.equal(0n); expect(result.prevExitRequestsLimit).to.equal(0n); expect(result.prevTimestamp).to.equal(0n); - expect(result.frameDuration).to.equal(0n); + expect(result.frameDurationInSec).to.equal(0n); expect(result.exitsPerFrame).to.equal(0n); }); @@ -49,7 +49,7 @@ describe("ExitLimitUtils.sol", () => { maxExitRequestsLimit: MAX_UINT32, prevExitRequestsLimit: MAX_UINT32, prevTimestamp: MAX_UINT32, - frameDuration: MAX_UINT32, + frameDurationInSec: MAX_UINT32, exitsPerFrame: MAX_UINT32, }; @@ -59,7 +59,7 @@ describe("ExitLimitUtils.sol", () => { expect(result.maxExitRequestsLimit).to.equal(MAX_UINT32); expect(result.prevExitRequestsLimit).to.equal(MAX_UINT32); expect(result.prevTimestamp).to.equal(MAX_UINT32); - expect(result.frameDuration).to.equal(MAX_UINT32); + expect(result.frameDurationInSec).to.equal(MAX_UINT32); expect(result.exitsPerFrame).to.equal(MAX_UINT32); }); @@ -67,14 +67,14 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 100n; const prevExitRequestsLimit = 9n; const prevTimestamp = 90n; - const frameDuration = 10n; + const frameDurationInSec = 10n; const exitsPerFrame = 1n; data = { maxExitRequestsLimit, prevExitRequestsLimit, prevTimestamp, - frameDuration, + frameDurationInSec, exitsPerFrame, }; @@ -84,7 +84,7 @@ describe("ExitLimitUtils.sol", () => { expect(result.maxExitRequestsLimit).to.equal(maxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(prevExitRequestsLimit); expect(result.prevTimestamp).to.equal(prevTimestamp); - expect(result.frameDuration).to.equal(frameDuration); + expect(result.frameDurationInSec).to.equal(frameDurationInSec); expect(result.exitsPerFrame).to.equal(exitsPerFrame); }); }); @@ -100,13 +100,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 10; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, timestamp, ); @@ -119,13 +119,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 10; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -138,17 +138,17 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 10; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); - const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + frameDuration); + const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + frameDurationInSec); expect(result).to.equal(prevExitRequestsLimit + 1); }); @@ -157,13 +157,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 20; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); const result = await exitLimit.calculateCurrentExitLimit(prevTimestamp + 40); @@ -175,13 +175,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 100; const prevExitRequestsLimit = 90; // remaining limit from prev usage const exitsPerFrame = 3; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -194,13 +194,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 100; const prevExitRequestsLimit = 7; // remaining limit from prev usage const exitsPerFrame = 0; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -213,13 +213,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 20; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -239,13 +239,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 10; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -258,13 +258,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 10; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -278,13 +278,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 10; const prevExitRequestsLimit = 5; // remaining limit from prev usage const exitsPerFrame = 1; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -298,13 +298,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 100; const prevExitRequestsLimit = 90; // remaining limit from prev usage const exitsPerFrame = 5; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -318,13 +318,13 @@ describe("ExitLimitUtils.sol", () => { const maxExitRequestsLimit = 50; const prevExitRequestsLimit = 25; // remaining limit from prev usage const exitsPerFrame = 2; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( maxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, prevTimestamp, ); @@ -343,13 +343,13 @@ describe("ExitLimitUtils.sol", () => { const timestamp = 1000; const maxExitRequestsLimit = 100; const exitsPerFrame = 2; - const frameDuration = 10; + const frameDurationInSec = 10; - const result = await exitLimit.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + const result = await exitLimit.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); expect(result.maxExitRequestsLimit).to.equal(maxExitRequestsLimit); expect(result.exitsPerFrame).to.equal(exitsPerFrame); - expect(result.frameDuration).to.equal(frameDuration); + expect(result.frameDurationInSec).to.equal(frameDurationInSec); expect(result.prevExitRequestsLimit).to.equal(maxExitRequestsLimit); expect(result.prevTimestamp).to.equal(timestamp); }); @@ -359,18 +359,18 @@ describe("ExitLimitUtils.sol", () => { const oldMaxExitRequestsLimit = 100; const prevExitRequestsLimit = 80; const exitsPerFrame = 2; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( oldMaxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, timestamp, ); const newMaxExitRequestsLimit = 50; - const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); @@ -382,19 +382,19 @@ describe("ExitLimitUtils.sol", () => { const oldMaxExitRequestsLimit = 100; const prevExitRequestsLimit = 80; const exitsPerFrame = 2; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( oldMaxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, timestamp, ); const newMaxExitRequestsLimit = 150; - const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(prevExitRequestsLimit); @@ -406,18 +406,18 @@ describe("ExitLimitUtils.sol", () => { const oldMaxExitRequestsLimit = 0; const prevExitRequestsLimit = 0; const exitsPerFrame = 2; - const frameDuration = 10; + const frameDurationInSec = 10; await exitLimit.harness_setState( oldMaxExitRequestsLimit, prevExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, timestamp, ); const newMaxExitRequestsLimit = 77; - const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDuration, timestamp); + const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); @@ -435,7 +435,7 @@ describe("ExitLimitUtils.sol", () => { await expect(exitLimit.setExitLimits(100, 101, 10, 1000)).to.be.revertedWith("TOO_LARGE_EXITS_PER_FRAME"); }); - it("should revert if frameDuration is too large", async () => { + it("should revert if frameDurationInSec is too large", async () => { const MAX_UINT32 = 2 ** 32; await expect(exitLimit.setExitLimits(100, 2, MAX_UINT32, 1000)).to.be.revertedWith("TOO_LARGE_FRAME_DURATION"); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts index 99b1681788..7b7db88feb 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts @@ -56,7 +56,7 @@ describe("ValidatorsExitBusOracle.sol:finalizeUpgrade_v2", () => { const exitRequestLimitData = await oracle.getExitRequestLimitFullInfo(); expect(exitRequestLimitData.maxExitRequestsLimit).to.equal(150); expect(exitRequestLimitData.exitsPerFrame).to.equal(1); - expect(exitRequestLimitData.frameDuration).to.equal(48); + expect(exitRequestLimitData.frameDurationInSec).to.equal(48); expect(await oracle.getMaxRequestsPerBatch()).to.equal(15); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 6a74a0c3a9..8ed3a603a3 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -398,7 +398,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); - expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.frameDurationInSec).to.equal(FRAME_DURATION); expect(data.prevExitRequestsLimit).to.equal(0); expect(data.currentExitRequestsLimit).to.equal(0); }); @@ -409,7 +409,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); - expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.frameDurationInSec).to.equal(FRAME_DURATION); expect(data.prevExitRequestsLimit).to.equal(0); expect(data.currentExitRequestsLimit).to.equal(2); }); @@ -503,7 +503,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.maxExitRequestsLimit).to.equal(0); expect(data.exitsPerFrame).to.equal(0); - expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.frameDurationInSec).to.equal(FRAME_DURATION); expect(data.prevExitRequestsLimit).to.equal(0); expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); }); @@ -537,7 +537,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.maxExitRequestsLimit).to.equal(0); expect(data.exitsPerFrame).to.equal(0); - expect(data.frameDuration).to.equal(FRAME_DURATION); + expect(data.frameDurationInSec).to.equal(FRAME_DURATION); expect(data.prevExitRequestsLimit).to.equal(0); // as time is mocked and we didnt change it since last consume, currentExitRequestsLimit was not increased expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 559100cb6f..a2f447d4b5 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -107,8 +107,8 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .withArgs(3, 1); }); - it("should not allow to set limit without role TW_EXIT_REPORT_LIMIT_ROLE", async () => { - const reportLimitRole = await triggerableWithdrawalsGateway.TW_EXIT_REPORT_LIMIT_ROLE(); + it("should not allow to set limit without role TW_EXIT_LIMIT_MANAGER_ROLE", async () => { + const reportLimitRole = await triggerableWithdrawalsGateway.TW_EXIT_LIMIT_MANAGER_ROLE(); await expect( triggerableWithdrawalsGateway.connect(stranger).setExitRequestLimit(4, 1, 48), @@ -116,7 +116,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { }); it("set limit", async () => { - const role = await triggerableWithdrawalsGateway.TW_EXIT_REPORT_LIMIT_ROLE(); + const role = await triggerableWithdrawalsGateway.TW_EXIT_LIMIT_MANAGER_ROLE(); await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); const exitLimitTx = await triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(4, 1, 48); @@ -148,7 +148,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { expect(data[0]).to.equal(4); // exitsPerFrame expect(data[1]).to.equal(1); - // frameDuration + // frameDurationInSec expect(data[2]).to.equal(48); // prevExitRequestsLimit // maxExitRequestsLimit (4) - exitRequests.length (3) @@ -181,7 +181,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { expect(data[0]).to.equal(4); // exitsPerFrame expect(data[1]).to.equal(1); - // frameDuration + // frameDurationInSec expect(data[2]).to.equal(48); // prevExitRequestsLimit // maxExitRequestsLimit (4) - exitRequests.length (3) @@ -315,7 +315,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { expect(data.maxExitRequestsLimit).to.equal(0); expect(data.exitsPerFrame).to.equal(0); - expect(data.frameDuration).to.equal(48); + expect(data.frameDurationInSec).to.equal(48); expect(data.prevExitRequestsLimit).to.equal(0); expect(data.currentExitRequestsLimit).to.equal(2n ** 256n - 1n); }); @@ -354,6 +354,6 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { it("Should not allow to set exitsPerFrame bigger than maxExitRequestsLimit", async () => { await expect( triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(0, 1, 48), - ).to.be.revertedWith("TOO_LARGE_TW_EXIT_REQUEST_LIMIT"); + ).to.be.revertedWith("TOO_LARGE_EXITS_PER_FRAME"); }); }); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index 14c73bfc7a..ab8a16fd25 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -100,7 +100,7 @@ interface VEBOConfig { maxRequestsPerBatch?: number; maxExitRequestsLimit?: number; exitsPerFrame?: number; - frameDuration?: number; + frameDurationInSec?: number; } export async function initVEBO({ @@ -114,7 +114,7 @@ export async function initVEBO({ maxRequestsPerBatch = 600, maxExitRequestsLimit = 13000, exitsPerFrame = 1, - frameDuration = 48, + frameDurationInSec = 48, }: VEBOConfig) { const initTx = await oracle.initialize( admin, @@ -124,7 +124,7 @@ export async function initVEBO({ maxRequestsPerBatch, maxExitRequestsLimit, exitsPerFrame, - frameDuration, + frameDurationInSec, ); await oracle.grantRole(await oracle.MANAGE_CONSENSUS_CONTRACT_ROLE(), admin); diff --git a/test/integration/validators-exit-bus-multiple-delivery.ts b/test/integration/validators-exit-bus-multiple-delivery.ts index 22fe0a3a88..d59f64801a 100644 --- a/test/integration/validators-exit-bus-multiple-delivery.ts +++ b/test/integration/validators-exit-bus-multiple-delivery.ts @@ -144,8 +144,8 @@ describe("ValidatorsExitBus integration", () => { // --- Setup exit limit --- const maxLimit = 3; const exitsPerFrame = 1; - const frameDurationSeconds = 48; - await veb.connect(limitManager).setExitRequestLimit(maxLimit, exitsPerFrame, frameDurationSeconds); + const frameDurationInSec = 48; + await veb.connect(limitManager).setExitRequestLimit(maxLimit, exitsPerFrame, frameDurationInSec); // --- Prepare data --- const exitRequestsHash: string = hashExitRequest(exitRequests); @@ -183,7 +183,7 @@ describe("ValidatorsExitBus integration", () => { expect(deliveryHistory1[0].lastDeliveredExitDataIndex).to.equal(maxLimit - 1); // --- 2nd delivery: only 1 request can be processed after 48 seconds --- - await advanceChainTime(BigInt(frameDurationSeconds)); + await advanceChainTime(BigInt(frameDurationInSec)); const tx2 = await veb.submitExitRequestsData(exitRequests); const receipt2 = await tx2.wait(); @@ -209,7 +209,7 @@ describe("ValidatorsExitBus integration", () => { // --- 3rd delivery: deliver remaining 6 requests after waiting (6 * 48) seconds --- let remainingRequestsCount = requests.length - (maxLimit + 1); // 10 - 4 = 6 - await advanceChainTime(BigInt(frameDurationSeconds * remainingRequestsCount)); + await advanceChainTime(BigInt(frameDurationInSec * remainingRequestsCount)); const tx3 = await veb.submitExitRequestsData(exitRequests); const receipt3 = await tx3.wait(); @@ -236,7 +236,7 @@ describe("ValidatorsExitBus integration", () => { remainingRequestsCount = requests.length - (maxLimit * 2 + 1); // 3 - await advanceChainTime(BigInt(frameDurationSeconds * remainingRequestsCount)); + await advanceChainTime(BigInt(frameDurationInSec * remainingRequestsCount)); const tx4 = await veb.submitExitRequestsData(exitRequests); const receipt4 = await tx4.wait(); From 0aaf1d1b0ce6737124feb1e3c14ecf3b3448d1ca Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 27 May 2025 20:05:09 +0400 Subject: [PATCH 188/405] fix: maxExitRequestsLimit desc --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index fbf1414e4a..d832870424 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -381,7 +381,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned /** * @notice Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. - * @param maxExitRequestsLimit The maximum number of exit requests. The period for which this value is valid can be calculated as: X = maxExitRequests / (exitsPerFrame * frameDurationInSec) + * @param maxExitRequestsLimit The maximum number of exit requests. * @param exitsPerFrame The number of exits that can be restored per frame. * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ From f623bc2eafdcaebcc141d206b10a48b9e58f84cc Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 18:21:50 +0200 Subject: [PATCH 189/405] feat: update naming in ValidatorExitDelayVerifier contract --- .../0.8.25/ValidatorExitDelayVerifier.sol | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 98f484e477..56d693ce50 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -100,7 +100,7 @@ contract ValidatorExitDelayVerifier { error UnsupportedSlot(uint64 slot); error InvalidPivotSlot(); error ZeroLidoLocatorAddress(); - error ExitRequestNotEligibleOnProvableBeaconBlock( + error ExitIstNotEligibleOnProvableBeaconBlock( uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); @@ -186,7 +186,7 @@ contract ValidatorExitDelayVerifier { witness.exitRequestIndex ); - uint256 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( + uint256 eligibleToExitInSec = _getSecondsSinceExitIsEligible( requestsDeliveryHistory, witness, proofSlotTimestamp @@ -194,13 +194,7 @@ contract ValidatorExitDelayVerifier { _verifyValidatorExitUnset(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); - stakingRouter.reportValidatorExitDelay( - moduleId, - nodeOpId, - proofSlotTimestamp, - pubkey, - secondsSinceEligibleExitRequest - ); + stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); } } @@ -239,7 +233,7 @@ contract ValidatorExitDelayVerifier { witness.exitRequestIndex ); - uint256 secondsSinceEligibleExitRequest = _getSecondsSinceExitRequestEligible( + uint256 eligibleToExitInSec = _getSecondsSinceExitIsEligible( requestsDeliveryHistory, witness, proofSlotTimestamp @@ -247,13 +241,7 @@ contract ValidatorExitDelayVerifier { _verifyValidatorExitUnset(oldBlock.header, witness, pubkey, valIndex); - stakingRouter.reportValidatorExitDelay( - moduleId, - nodeOpId, - proofSlotTimestamp, - pubkey, - secondsSinceEligibleExitRequest - ); + stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); } } @@ -337,7 +325,7 @@ contract ValidatorExitDelayVerifier { * to exit after VEB exit request. * @return uint256 The elapsed seconds since the earliest eligible exit request time. */ - function _getSecondsSinceExitRequestEligible( + function _getSecondsSinceExitIsEligible( DeliveryHistory[] memory history, ValidatorWitness calldata witness, uint256 referenceSlotTimestamp @@ -357,7 +345,7 @@ contract ValidatorExitDelayVerifier { : earliestPossibleVoluntaryExitTimestamp; if (referenceSlotTimestamp < eligibleExitRequestTimestamp) { - revert ExitRequestNotEligibleOnProvableBeaconBlock(referenceSlotTimestamp, eligibleExitRequestTimestamp); + revert ExitIstNotEligibleOnProvableBeaconBlock(referenceSlotTimestamp, eligibleExitRequestTimestamp); } return referenceSlotTimestamp - eligibleExitRequestTimestamp; From 6884674641a36e235df5508bba15509eb6f81393 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 27 May 2025 20:22:07 +0400 Subject: [PATCH 190/405] fix: EXIT_REPORT_LIMIT_ROLE -> EXIT_REQUEST_LIMIT_MANAGER_ROLE --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 ++-- ...validator-exit-bus-oracle.submitExitRequestsData.test.ts | 6 +++--- .../validator-exit-bus-oracle.submitReportData.test.ts | 2 +- .../oracle/validator-exit-bus-oracle.triggerExits.test.ts | 2 +- test/integration/validators-exit-bus-multiple-delivery.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d832870424..ba59e9a7c7 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -175,7 +175,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned /// @notice An ACL role granting the permission to submit a hash of the exit requests data bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); /// @notice An ACL role granting the permission to set maximum exit request limit and the frame limit restoring values - bytes32 public constant EXIT_REPORT_LIMIT_ROLE = keccak256("EXIT_REPORT_LIMIT_ROLE"); + bytes32 public constant EXIT_REQUEST_LIMIT_MANAGER_ROLE = keccak256("EXIT_REQUEST_LIMIT_MANAGER_ROLE"); /// @notice An ACL role granting the permission to pause accepting validator exit requests bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); /// @notice An ACL role granting the permission to resume accepting validator exit requests @@ -389,7 +389,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec - ) external onlyRole(EXIT_REPORT_LIMIT_ROLE) { + ) external onlyRole(EXIT_REQUEST_LIMIT_MANAGER_ROLE) { _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 8ed3a603a3..381b44efc2 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -290,7 +290,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { [admin, authorizedEntity, stranger] = await ethers.getSigners(); await deploy(); - const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await oracle.grantRole(reportLimitRole, authorizedEntity); await consensus.advanceTimeBy(24 * 60 * 60); @@ -322,7 +322,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const HASH_REQUEST_DELIVERED_BY_PARTS = hashExitRequest(REQUEST_DELIVERED_BY_PARTS); it("Should not allow to set limit without role", async () => { - const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await expect( oracle.connect(stranger).setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION), @@ -330,7 +330,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should not allow to set limit without role", async () => { - const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await expect( oracle.connect(stranger).setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION), diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index f7193c3c34..3c9aa9b51e 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -607,7 +607,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); it("Set exit limit", async () => { - const role = await oracle.EXIT_REPORT_LIMIT_ROLE(); + const role = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await oracle.grantRole(role, admin); const exitLimitTx = await oracle.connect(admin).setExitRequestLimit(7, 1, 48); await expect(exitLimitTx).to.emit(oracle, "ExitRequestsLimitSet").withArgs(7, 1, 48); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 60b67263dc..556e8d4696 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -322,7 +322,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { it("Should deliver part of requests", async () => { // set limit - const reportLimitRole = await oracle.EXIT_REPORT_LIMIT_ROLE(); + const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await oracle.grantRole(reportLimitRole, authorizedEntity); await oracle diff --git a/test/integration/validators-exit-bus-multiple-delivery.ts b/test/integration/validators-exit-bus-multiple-delivery.ts index d59f64801a..885b56f80f 100644 --- a/test/integration/validators-exit-bus-multiple-delivery.ts +++ b/test/integration/validators-exit-bus-multiple-delivery.ts @@ -125,7 +125,7 @@ describe("ValidatorsExitBus integration", () => { const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); - const manageLimitRole = await veb.EXIT_REPORT_LIMIT_ROLE(); + const manageLimitRole = await veb.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await veb.connect(agent).grantRole(manageLimitRole, limitManager); if (await veb.isPaused()) { From 6798435f59cbac51100c594216d9ac821877bf13 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 27 May 2025 20:32:00 +0400 Subject: [PATCH 191/405] fix: rename _checkExitRequestLimit --- contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 711dc06249..5c1ac11196 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -136,7 +136,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { uint256 requestsCount = validatorsData.length; if (requestsCount == 0) revert ZeroArgument("validatorsData"); - _checkExitRequestLimit(requestsCount); + _consumeExitRequestLimit(requestsCount); IWithdrawalVault withdrawalVault = IWithdrawalVault(LOCATOR.withdrawalVault()); uint256 fee = withdrawalVault.getWithdrawalRequestFee(); @@ -255,7 +255,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } - function _checkExitRequestLimit(uint256 requestsCount) internal { + function _consumeExitRequestLimit(uint256 requestsCount) internal { ExitRequestLimitData memory twrLimitData = TWR_LIMIT_POSITION.getStorageExitRequestLimit(); if (!twrLimitData.isExitLimitSet()) { return; From ecb92ff449d5f2e86b247791bd7dc834ae522026 Mon Sep 17 00:00:00 2001 From: KRogLA Date: Tue, 27 May 2025 19:14:39 +0200 Subject: [PATCH 192/405] fix: assert pubkey length on 7002 request --- contracts/0.8.9/WithdrawalVaultEIP7685.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7685.sol index ba224fe7bb..35e182f161 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7685.sol @@ -20,7 +20,6 @@ abstract contract WithdrawalVaultEIP7685 { error IncorrectFee(uint256 providedFee, uint256 requiredFee); error RequestAdditionFailed(bytes callData); - /** * @dev Submits EIP-7002 full or partial withdrawal requests for the specified public keys. * Each full withdrawal request instructs a validator to fully withdraw its stake and exit its duties as a validator. @@ -49,7 +48,7 @@ abstract contract WithdrawalVaultEIP7685 { _checkFee(requestsCount * fee); for (uint256 i = 0; i < requestsCount; ++i) { - _callAddWithdrawalRequest(fee, abi.encodePacked(pubkeys[i], amounts[i])); + _callAddWithdrawalRequest(pubkeys[i], amounts[i], fee); } } @@ -75,7 +74,11 @@ abstract contract WithdrawalVaultEIP7685 { return abi.decode(feeData, (uint256)); } - function _callAddWithdrawalRequest(uint256 fee, bytes memory request) internal { + // function _callAddWithdrawalRequest(uint256 fee, bytes memory request) internal { + function _callAddWithdrawalRequest(bytes calldata pubkey, uint64 amount, uint256 fee) internal { + assert(pubkey.length == 48); + + bytes memory request = abi.encodePacked(pubkey, amount); (bool success,) = WITHDRAWAL_REQUEST.call{value: fee}(request); if (!success) { revert RequestAdditionFailed(request); From a672db2b37637f31dc43f684c40d52d1704d4010 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 19:20:48 +0200 Subject: [PATCH 193/405] fix: rename error in exit delay verifier test --- test/0.8.25/validatorExitDelayVerifier.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 7502bec10b..dcc7822997 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -397,7 +397,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); - it("reverts with 'ExitRequestNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { + it("reverts with 'ExitIstNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { const intervalInSecondsAfterProofSlot = 1; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; @@ -429,7 +429,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitRequestNotEligibleOnProvableBeaconBlock"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIstNotEligibleOnProvableBeaconBlock"); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -440,7 +440,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitRequestNotEligibleOnProvableBeaconBlock"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIstNotEligibleOnProvableBeaconBlock"); }); it("reverts if the validator proof is incorrect", async () => { From 3dd89fca3a4ec9e3f85c3ac28ec29830cbce4b7c Mon Sep 17 00:00:00 2001 From: KRogLA Date: Tue, 27 May 2025 19:38:08 +0200 Subject: [PATCH 194/405] test: fix tests for 7002 request --- test/0.8.9/withdrawalVault/withdrawalVault.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 5c485f010c..5d623deddf 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -368,27 +368,27 @@ describe("WithdrawalVault.sol", () => { .withArgs(2n, 3n); }); - it.skip("Should revert if pubkey is not 48 bytes", async function () { + it("Should revert if pubkey is not 48 bytes", async function () { // Invalid pubkey (only 2 bytes) const invalidPubkeyHexString = ["0x1234"]; const fee = await getFee(); await expect( vault.connect(validatorsExitBus).addWithdrawalRequests(invalidPubkeyHexString, [1n], { value: fee }), - ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); + ).to.be.revertedWithPanic(1); // assertion }); - it.skip("Should revert if last pubkey not 48 bytes", async function () { + it("Should revert if last pubkey not 48 bytes", async function () { const validPubey = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"; const invalidPubkey = "1234"; - const pubkeysHexArray = [`0x${validPubey}`, `${invalidPubkey}`]; + const pubkeysHexArray = [`0x${validPubey}`, `0x${invalidPubkey}`]; - const fee = await getFee(); + const fee = (await getFee()) * 2n; // 2 requests await expect( vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, [1n, 2n], { value: fee }), - ).to.be.revertedWithCustomError(vault, "MalformedPubkeysArray"); + ).to.be.revertedWithPanic(1); // assertion }); it("Should revert if addition fails at the withdrawal request contract", async function () { From 47e5cc85642199ccac7f0bca5477f63fc704c5ab Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 19:39:23 +0200 Subject: [PATCH 195/405] fix: update integration test for exit delay verifier --- test/integration/report-validator-exit-delay.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 12ec804281..967cdb5087 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -306,7 +306,7 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "KeyWasNotUnpacked"); + ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "EmptyDeliveryHistory"); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -317,7 +317,7 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "KeyWasNotUnpacked"); + ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "EmptyDeliveryHistory"); }); it("Should revert when submitting validator exit delay with invalid beacon block root", async () => { From 13036234fabf123ef9739fba4b52b551d691951c Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 27 May 2025 20:48:51 +0200 Subject: [PATCH 196/405] feat: add exit verifier tests for NonMonotonicDeliveryHistory --- .../0.8.25/validatorExitDelayVerifier.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index dcc7822997..3f0e5528bf 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -554,6 +554,63 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); }); + it("reverts with 'NonMonotonicDeliveryHistory' if delivery history is not strictly increasing.", async () => { + const exitRequests: ExitRequest[] = [ + { + moduleId: 1, + nodeOpId: 1, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + const nonMonotonicDeliveryHistory = [ + [ + { timestamp: 2, lastDeliveredKeyIndex: 2n }, + { timestamp: 1, lastDeliveredKeyIndex: 1n }, + ], + [ + { timestamp: 1, lastDeliveredKeyIndex: 2n }, + { timestamp: 1, lastDeliveredKeyIndex: 1n }, + ], + [ + { timestamp: 1, lastDeliveredKeyIndex: 1n }, + { timestamp: 1, lastDeliveredKeyIndex: 1n }, + ], + ]; + + async function testNonMonotonicDeliveryHistory( + deliveryHistory: { timestamp: number; lastDeliveredKeyIndex: bigint }[], + ) { + await vebo.setExitRequests(encodedExitRequestsHash, deliveryHistory, exitRequests); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "NonMonotonicDeliveryHistory"); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "NonMonotonicDeliveryHistory"); + } + + for (const deliveryHistory of nonMonotonicDeliveryHistory) { + await testNonMonotonicDeliveryHistory(deliveryHistory); + } + }); + it("reverts with 'KeyWasNotUnpacked' if exit request index is not in delivery history", async () => { const nodeOpId = 2; const exitRequests: ExitRequest[] = [ From 34562777a8edd831ca3718839dbe681dcc86f157 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 11:24:31 +0200 Subject: [PATCH 197/405] refactor: update exit delay handling in NodeOperatorsRegistry to use packed storage --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 3f9ee9375b..46b782a1ad 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -156,12 +156,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // bytes32 internal constant REWARD_DISTRIBUTION_STATE = keccak256("lido.NodeOperatorsRegistry.rewardDistributionState"); bytes32 internal constant REWARD_DISTRIBUTION_STATE = 0x4ddbb0dcdc5f7692e494c15a7fca1f9eb65f31da0b5ce1c3381f6a1a1fd579b6; - // Threshold in seconds after which a delayed exit is penalized - // bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = keccak256("lido.NodeOperatorsRegistry.exitDelayThresholdSeconds"); - bytes32 internal constant EXIT_DELAY_THRESHOLD_SECONDS = 0x96656d3ece9cdbe3bd729ff6d7df8d0aeb457ff7c7c42372184ae30b10b37976; - // Cutoff timestamp used to protect validators from penalization after threshold changes - // bytes32 internal constant EXIT_PENALTY_CUTOFF_TIMESTAMP = keccak256("lido.NodeOperatorsRegistry.exitPenaltyCutoffTimestamp"); - bytes32 internal constant EXIT_PENALTY_CUTOFF_TIMESTAMP = 0x93f1d4cdf7a6d0aac32b989ca335f5ae5f4322e4361b8f67a199fdda105f821b; + // bytes32 internal constant EXIT_DELAY_STATS = keccak256("lido.NodeOperatorsRegistry.exitDelayStats"); + bytes32 internal constant EXIT_DELAY_STATS = 0x9fe52a88cbf7bfbe5e42abc45469ad27b2231a10bcbcd0a227c7ca0835cecbd8; + /// @dev Exit delay stats offsets in Packed64x4: + /// @dev The delay threshold in seconds after which a validator exit is considered late + uint8 internal constant EXIT_DELAY_THRESHOLD_OFFSET = 0; + /// @dev Timestamp before which validators reported as late will not result in penalties for their Node Operators. + uint8 internal constant EXIT_PENALTY_CUTOFF_TIMESTAMP_OFFSET = 1; // @@ -1064,18 +1065,17 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } function _exitDeadlineThreshold() internal view returns (uint256) { - return EXIT_DELAY_THRESHOLD_SECONDS.getStorageUint256(); + return Packed64x4.Packed(EXIT_DELAY_STATS.getStorageUint256()).get(EXIT_DELAY_THRESHOLD_OFFSET); } - /// @notice Returns the cutoff timestamp before which validators cannot be penalized for delayed exit. + /// @notice Returns the Timestamp before which validators reported as late will not result in penalties for their Node Operators.. /// @return uint256 The cutoff timestamp used when evaluating late exits. function exitPenaltyCutoffTimestamp() public view returns (uint256) { - return EXIT_PENALTY_CUTOFF_TIMESTAMP.getStorageUint256(); + return Packed64x4.Packed(EXIT_DELAY_STATS.getStorageUint256()).get(EXIT_PENALTY_CUTOFF_TIMESTAMP_OFFSET); } /// @notice Sets the validator exit deadline threshold and the reporting window for late exits. - /// @dev Updates the cutoff timestamp before which validators are protected from penalization. - /// Prevents penalizing validators whose exit eligibility began before the new policy took effect. + /// @dev Updates the cutoff timestamp before which a validator that was requested to exit cannot be reported as late. /// @param _threshold Number of seconds a validator has to exit after becoming eligible. /// @param _reportingWindow Additional number of seconds during which a late exit can still be reported. function setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) external { @@ -1086,11 +1086,13 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) internal { require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); - EXIT_DELAY_THRESHOLD_SECONDS.setStorageUint256(_threshold); - // Set the cutoff timestamp to the current time minus the threshold and reportingWindow period uint256 currentCutoffTimestamp = block.timestamp - _threshold - _reportingWindow; - EXIT_PENALTY_CUTOFF_TIMESTAMP.setStorageUint256(currentCutoffTimestamp); + + Packed64x4.Packed memory stats; + stats.set(EXIT_DELAY_THRESHOLD_OFFSET, _threshold); + stats.set(EXIT_PENALTY_CUTOFF_TIMESTAMP_OFFSET, currentCutoffTimestamp); + EXIT_DELAY_STATS.setStorageUint256(stats.v); emit ExitDeadlineThresholdChanged(_threshold, _reportingWindow); } From a3c03c0b0e480d54cf3dd4fc5c5df0838f3a9ac8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 28 May 2025 14:12:26 +0400 Subject: [PATCH 198/405] fix: ExitRequestsLimit -> ExitRequestsLimitExceeded --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 5 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 16 +-- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- scripts/scratch/steps/0130-grant-roles.ts | 9 +- ...-bus-oracle.submitExitRequestsData.test.ts | 2 +- ...r-exit-bus-oracle.submitReportData.test.ts | 2 +- ...awalGateway.triggerFullWithdrawals.test.ts | 2 +- .../validators-exit-bus-multiple-delivery.ts | 1 - .../validators-exit-bus-trigger-exits.ts | 102 ++++++++++++++++++ 9 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 test/integration/validators-exit-bus-trigger-exits.ts diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 5c1ac11196..20c4be19e0 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -68,8 +68,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param requestsCount Amount of requests that were sent for processing * @param remainingLimit Amount of requests that still can be processed at current day */ - - error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); struct ValidatorData { uint256 stakingModuleId; @@ -264,7 +263,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { uint256 limit = twrLimitData.calculateCurrentExitLimit(_getTimestamp()); if (limit < requestsCount) { - revert ExitRequestsLimit(requestsCount, limit); + revert ExitRequestsLimitExceeded(requestsCount, limit); } TWR_LIMIT_POSITION.setStorageExitRequestLimit( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index ba59e9a7c7..8db77673c4 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -101,12 +101,11 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned * @param requestsCount Amount of requests that were sent for processing * @param remainingLimit Amount of requests that still can be processed at current day */ - error ExitRequestsLimit(uint256 requestsCount, uint256 remainingLimit); + error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); /** * @notice Thrown when submitting was not started for request */ - error DeliveryWasNotStarted(); /// @dev Events @@ -605,7 +604,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned function _applyDeliverLimit(uint256 limit, uint256 count) internal pure returns (uint256 limitedCount) { if (limit == 0) { - revert ExitRequestsLimit(count, 0); + revert ExitRequestsLimitExceeded(count, 0); } return min(limit, count); } @@ -713,13 +712,16 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned } bytes calldata pubkey; + uint256 dataWithoutPubkey; + uint256 moduleId; + uint256 nodeOpId; + uint64 valIndex; assembly { pubkey.length := 48 } while (offset < offsetPastEnd) { - uint256 dataWithoutPubkey; assembly { // 16 most significant bytes are taken by module id, node op id, and val index dataWithoutPubkey := shr(128, calldataload(offset)) @@ -729,7 +731,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned offset := add(offset, 64) } - uint256 moduleId = uint24(dataWithoutPubkey >> (64 + 40)); + moduleId = uint24(dataWithoutPubkey >> (64 + 40)); if (moduleId == 0) { revert InvalidRequestsData(); @@ -742,8 +744,8 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned revert InvalidRequestsDataSortOrder(); } - uint64 valIndex = uint64(dataWithoutPubkey); - uint256 nodeOpId = uint40(dataWithoutPubkey >> 64); + valIndex = uint64(dataWithoutPubkey); + nodeOpId = uint40(dataWithoutPubkey >> 64); lastDataWithoutPubkey = dataWithoutPubkey; emit ValidatorExitRequest(moduleId, nodeOpId, valIndex, pubkey, timestamp); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 046b026e67..8e6d933c6d 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -292,7 +292,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { function _applyOracleLimit(uint256 limit, uint256 count) internal pure returns (uint256) { if (limit < count) { - revert ExitRequestsLimit(count, limit); + revert ExitRequestsLimitExceeded(count, limit); } return count; } diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index 5d1d7d9a29..b747f846c5 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -52,6 +52,13 @@ export async function main() { await makeTx(stakingRouter, "grantRole", [await stakingRouter.STAKING_MODULE_MANAGE_ROLE(), agentAddress], { from: deployer, }); + await makeTx( + stakingRouter, + "grantRole", + [await stakingRouter.REPORT_EXITED_VALIDATORS_ROLE(), triggerableWithdrawalsGatewayAddress], + { from: deployer }, + ); + // ValidatorsExitBusOracle if (gateSealAddress) { @@ -104,7 +111,7 @@ export async function main() { await makeTx( withdrawalVault, "grantRole", - [await withdrawalVault.ADD_WITHDRAWAL_REQUEST_ROLE(), validatorsExitBusOracleAddress], + [await withdrawalVault.ADD_WITHDRAWAL_REQUEST_ROLE(), triggerableWithdrawalsGatewayAddress], { from: deployer, }, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 381b44efc2..8f28b8ac05 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -389,7 +389,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { it("Should revert when limit exceeded for the frame", async () => { await expect(oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS)) - .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") + .to.be.revertedWithCustomError(oracle, "ExitRequestsLimitExceeded") .withArgs(2, 0); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 3c9aa9b51e..68f4c28fe8 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -664,7 +664,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { const { reportData } = await prepareReportAndSubmitHash(requests); await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "ExitRequestsLimit") + .to.be.revertedWithCustomError(oracle, "ExitRequestsLimitExceeded") .withArgs(4, 3); }); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index a2f447d4b5..6657dfa930 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -166,7 +166,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .connect(authorizedEntity) .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }), ) - .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ExitRequestsLimit") + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ExitRequestsLimitExceeded") .withArgs(3, 1); }); diff --git a/test/integration/validators-exit-bus-multiple-delivery.ts b/test/integration/validators-exit-bus-multiple-delivery.ts index 885b56f80f..c10133d19a 100644 --- a/test/integration/validators-exit-bus-multiple-delivery.ts +++ b/test/integration/validators-exit-bus-multiple-delivery.ts @@ -168,7 +168,6 @@ describe("ValidatorsExitBus integration", () => { for (let i = 0; i < maxLimit; i++) { const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs1[i].data, logs1[i].topics); - console.log(decoded); const expected = requests[i]; expect(decoded[0]).to.equal(expected.moduleId); expect(decoded[1]).to.equal(expected.nodeOpId); diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts new file mode 100644 index 0000000000..d4d061808c --- /dev/null +++ b/test/integration/validators-exit-bus-trigger-exits.ts @@ -0,0 +1,102 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { ValidatorsExitBusOracle, WithdrawalVault } from "typechain-types"; + +import { de0x, ether, numberToHex } from "lib"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { Snapshot } from "test/suite"; + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + +describe("ValidatorsExitBus integration", () => { + let ctx: ProtocolContext; + let snapshot: string; + + let veb: ValidatorsExitBusOracle; + let wv: WithdrawalVault; + let hashReporter: HardhatEthersSigner; + let resumer: HardhatEthersSigner; + let agent: HardhatEthersSigner; + let refundRecipient: HardhatEthersSigner; + + const moduleId = 1; + const nodeOpId = 2; + const valIndex = 3; + const pubkey = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + const exitRequestPacked = "0x" + encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey: pubkey }); + + before(async () => { + ctx = await getProtocolContext(); + veb = ctx.contracts.validatorsExitBusOracle; + wv = ctx.contracts.withdrawalVault; + + [hashReporter, resumer, refundRecipient] = await ethers.getSigners(); + + agent = await ctx.getSigner("agent", ether("1")); + + // Grant role to submit exit hash + const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); + await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); + + if (await veb.isPaused()) { + const resumeRole = await veb.RESUME_ROLE(); + await veb.connect(agent).grantRole(resumeRole, resumer); + await veb.connect(resumer).resume(); + + expect(veb.isPaused()).to.be.false; + } + }); + + beforeEach(async () => (snapshot = await Snapshot.take())); + afterEach(async () => await Snapshot.restore(snapshot)); + + it("should trigger exits", async () => { + const dataFormat = 1; + + const exitRequest = { dataFormat, data: exitRequestPacked }; + + const exitRequestsHash: string = hashExitRequest(exitRequest); + + await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) + .to.emit(veb, "RequestsHashSubmitted") + .withArgs(exitRequestsHash); + + const tx = await veb.submitExitRequestsData(exitRequest); + const receipt = await tx.wait(); + const block = await receipt?.getBlock(); + const blockTimestamp = block!.timestamp; + + await expect(tx) + .to.emit(veb, "ValidatorExitRequest") + .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); + + const deliveryHistory = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(deliveryHistory.length).to.equal(1); + expect(deliveryHistory[0].lastDeliveredExitDataIndex).to.equal(0); + + await expect(veb.triggerExits(exitRequest, [0], refundRecipient, {value: 10})) + .to.emit(wv, "WithdrawalRequestAdded"); + }); +}); From 7f34ec518e9ada03453c00bdc3b859432ea87218 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 12:39:57 +0200 Subject: [PATCH 199/405] refactor: rename roles for validator exit reporting in StakingRouter --- contracts/0.8.9/StakingRouter.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index f4a95ed57f..835dc86f05 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -132,7 +132,8 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version bytes32 public constant STAKING_MODULE_MANAGE_ROLE = keccak256("STAKING_MODULE_MANAGE_ROLE"); bytes32 public constant STAKING_MODULE_UNVETTING_ROLE = keccak256("STAKING_MODULE_UNVETTING_ROLE"); bytes32 public constant REPORT_EXITED_VALIDATORS_ROLE = keccak256("REPORT_EXITED_VALIDATORS_ROLE"); - bytes32 public constant REPORT_EXITED_VALIDATORS_STATUS_ROLE = keccak256("REPORT_EXITED_VALIDATORS_STATUS_ROLE"); + bytes32 public constant REPORT_VALIDATOR_EXITING_STATUS_ROLE = keccak256("REPORT_VALIDATOR_EXITING_STATUS_ROLE"); + bytes32 public constant REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE = keccak256("REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE"); bytes32 public constant UNSAFE_SET_EXITED_VALIDATORS_ROLE = keccak256("UNSAFE_SET_EXITED_VALIDATORS_ROLE"); bytes32 public constant REPORT_REWARDS_MINTED_ROLE = keccak256("REPORT_REWARDS_MINTED_ROLE"); @@ -1485,7 +1486,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _eligibleToExitInSec ) external - onlyRole(REPORT_EXITED_VALIDATORS_STATUS_ROLE) + onlyRole(REPORT_VALIDATOR_EXITING_STATUS_ROLE) { _getIStakingModuleById(_stakingModuleId).reportValidatorExitDelay( _nodeOperatorId, @@ -1511,7 +1512,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _exitType ) external - onlyRole(REPORT_EXITED_VALIDATORS_ROLE) + onlyRole(REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE) { _getIStakingModuleById(_stakingModuleId).onValidatorExitTriggered( _nodeOperatorId, From 2836dba3f94dd5f41422afd5a13dce3de9a6bb49 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 12:51:02 +0200 Subject: [PATCH 200/405] refactor: correct role name for reporting validator exiting status in staking router --- test/integration/report-validator-exit-delay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 12ec804281..337654aec8 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -45,7 +45,7 @@ describe("Report Validator Exit Delay", () => { await stakingRouter .connect(agentSigner) - .grantRole(await stakingRouter.REPORT_EXITED_VALIDATORS_STATUS_ROLE(), validatorExitDelayVerifier.address); + .grantRole(await stakingRouter.REPORT_VALIDATOR_EXITING_STATUS_ROLE(), validatorExitDelayVerifier.address); // Ensure that the validatorExitDelayVerifier contract and provided proof use same GI expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal( From 43f6b150b55b76b796e0840735aba1882b8563ac Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 13:02:08 +0200 Subject: [PATCH 201/405] refactor: initialize stats variable in NodeOperatorsRegistry to avoid uninitialized memory access --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 46b782a1ad..c8f4e7cf92 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1089,7 +1089,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Set the cutoff timestamp to the current time minus the threshold and reportingWindow period uint256 currentCutoffTimestamp = block.timestamp - _threshold - _reportingWindow; - Packed64x4.Packed memory stats; + Packed64x4.Packed memory stats = Packed64x4.Packed(0); stats.set(EXIT_DELAY_THRESHOLD_OFFSET, _threshold); stats.set(EXIT_PENALTY_CUTOFF_TIMESTAMP_OFFSET, currentCutoffTimestamp); EXIT_DELAY_STATS.setStorageUint256(stats.v); From 433c373f9aaa58c4360a62fdb5b561e423bd932a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 28 May 2025 15:05:36 +0400 Subject: [PATCH 202/405] fix: veb description --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 8db77673c4..1a88c3493d 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -25,7 +25,7 @@ interface ITriggerableWithdrawalsGateway { /** * @title ValidatorsExitBus - * @notice An on-chain contract that serves as the central infrastructure for managing validator exit requests. + * @notice Сontract that serves as the central infrastructure for managing validator exit requests. * It stores report hashes, emits exit events, and maintains data and tools that enables anyone to prove a validator was requested to exit. */ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned { From 3015a734ed4dd70cfbad5d18c3f68f13ec6a6a60 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 14:30:30 +0200 Subject: [PATCH 203/405] refactor: update validator exit logic descriptions to clarify node operator responsibilities --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 2 +- contracts/0.8.9/StakingRouter.sol | 2 +- contracts/0.8.9/interfaces/IStakingModule.sol | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c8f4e7cf92..614da99429 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1134,7 +1134,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { && _proofSlotTimestamp - _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(); } - /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. + /// @notice Handles tracking and penalization logic for a node operator who failed to exit their validator within the defined exit window. /// @dev This function is called by the StakingRouter to report the current exit-related status of a validator /// belonging to a specific node operator. It marks the validator as processed to avoid duplicate reports. /// @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered. diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 835dc86f05..3350a11c6b 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1469,7 +1469,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version return stakingModule.getStakingModuleSummary(); } - /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. + /// @notice Handles tracking and penalization logic for a node operator who failed to exit their validator within the defined exit window. /// @dev This function is called to report the current exit-related status of a validator belonging to a specific node operator. /// It accepts a validator's public key, associated with the duration (in seconds) it was eligible to exit but has not exited. /// This data could be used to trigger penalties for the node operator if the validator has been non-exiting for too long. diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index 1a6e4b3c8c..6e09b4c220 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -14,7 +14,7 @@ interface IStakingModule { /// @dev Event to be emitted when a signing key is removed from the StakingModule event SigningKeyRemoved(uint256 indexed nodeOperatorId, bytes pubkey); - /// @notice Handles tracking and penalization logic for a validator that remains active beyond its eligible exit window. + /// @notice Handles tracking and penalization logic for a node operator who failed to exit their validator within the defined exit window. /// @dev This function is called by the StakingRouter to report the current exit-related status of a validator /// belonging to a specific node operator. It accepts a validator's public key, associated /// with the duration (in seconds) it was eligible to exit but has not exited. From 1b366c11bb95d071db942a43caebee45255b4b93 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 14:59:25 +0200 Subject: [PATCH 204/405] refactor: add validation for exit penalty cutoff timestamp in NodeOperatorsRegistry --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 1 + test/0.4.24/nor/nor.exit.manager.test.ts | 37 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 614da99429..b465121c89 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1088,6 +1088,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Set the cutoff timestamp to the current time minus the threshold and reportingWindow period uint256 currentCutoffTimestamp = block.timestamp - _threshold - _reportingWindow; + require(exitPenaltyCutoffTimestamp() <= currentCutoffTimestamp, "INVALID_EXIT_PENALTY_CUTOFF_TIMESTAMP"); Packed64x4.Packed memory stats = Packed64x4.Packed(0); stats.set(EXIT_DELAY_THRESHOLD_OFFSET, _threshold); diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index e78d5a3410..a288463a68 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -246,33 +246,44 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { }); context("exitPenaltyCutoffTimestamp", () => { - const threshold = ONE_DAY; const reportingWindow = 3600n; // 1 hour let cutoff: bigint; beforeEach(async () => { - // Set threshold and grace period via contract method - const tx = await nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(threshold, reportingWindow); + await deployer.provider.send("hardhat_mine", [`0x${(BigInt(await deployer.provider.getBlockNumber()) + 3000n).toString(16)}`, 12000]); + + const tx = await nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(exitDeadlineThreshold, reportingWindow); // Fetch actual cutoff timestamp from the contract cutoff = BigInt(await nor.exitPenaltyCutoffTimestamp()); // Get the block timestamp of the transaction const block = await deployer.provider.getBlock(tx.blockNumber!); - const expectedCutoff = BigInt(block!.timestamp) - threshold - reportingWindow; + const expectedCutoff = BigInt(block!.timestamp) - exitDeadlineThreshold - reportingWindow; // Ensure cutoff was set correctly expect(cutoff).to.equal(expectedCutoff); }); + it("reverts oldCutoffTimestamp <= currentCutoffTimestamp", async () => { + await expect( + nor + .connect(nodeOperatorsManager) + .setExitDeadlineThreshold( + eligibleToExitInSec, + eligibleToExitInSec + 100_000n, + ), + ).to.be.revertedWith("INVALID_EXIT_PENALTY_CUTOFF_TIMESTAMP"); + }); + it("returns false when _proofSlotTimestamp < cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff + threshold - 1n, + cutoff + exitDeadlineThreshold - 1n, testPublicKey, - threshold, + exitDeadlineThreshold, ); expect(result).to.be.false; }); @@ -280,9 +291,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns true when _proofSlotTimestamp == cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff + threshold, + cutoff + exitDeadlineThreshold, testPublicKey, - threshold, + exitDeadlineThreshold, ); expect(result).to.be.true; }); @@ -290,9 +301,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("returns true when _proofSlotTimestamp > cutoff", async () => { const result = await nor.isValidatorExitDelayPenaltyApplicable( firstNodeOperatorId, - cutoff + threshold + 1n, + cutoff + exitDeadlineThreshold + 1n, testPublicKey, - threshold, + exitDeadlineThreshold, ); expect(result).to.be.true; }); @@ -303,7 +314,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .connect(stakingRouter) .reportValidatorExitDelay( firstNodeOperatorId, - cutoff + threshold - 1n, + cutoff + exitDeadlineThreshold - 1n, testPublicKey, eligibleToExitInSec, ), @@ -316,13 +327,13 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .connect(stakingRouter) .reportValidatorExitDelay( firstNodeOperatorId, - cutoff + threshold, + cutoff + exitDeadlineThreshold, testPublicKey, eligibleToExitInSec, ), ) .to.emit(nor, "ValidatorExitStatusUpdated") - .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff + threshold); + .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff + exitDeadlineThreshold); }); }); From 486049440c2e7ce6f6b723feaa3dc144e0d40bdf Mon Sep 17 00:00:00 2001 From: F4ever Date: Wed, 28 May 2025 15:25:03 +0200 Subject: [PATCH 205/405] test: fix extra data delivery tests --- .../oracle/accountingOracle.happyPath.test.ts | 10 + ...untingOracle.submitReportExtraData.test.ts | 193 +++++++----------- 2 files changed, 87 insertions(+), 116 deletions(-) diff --git a/test/0.8.9/oracle/accountingOracle.happyPath.test.ts b/test/0.8.9/oracle/accountingOracle.happyPath.test.ts index 0be6542177..65e857052d 100644 --- a/test/0.8.9/oracle/accountingOracle.happyPath.test.ts +++ b/test/0.8.9/oracle/accountingOracle.happyPath.test.ts @@ -349,6 +349,11 @@ describe("AccountingOracle.sol:happyPath", () => { expect(call2.keysCounts).to.equal("0x" + [2].map((i) => numberToHex(i, 16)).join("")); }); + it("Staking router was told that exited keys updating is finished", async () => { + const totalFinishedCalls = await mockStakingRouter.totalCalls_onValidatorsCountsByNodeOperatorReportingFinished(); + expect(totalFinishedCalls).to.equal(1); + }); + it(`extra data for the same reference slot cannot be re-submitted`, async () => { await expect(oracle.connect(member1).submitReportExtraDataList(extraDataList)).to.be.revertedWithCustomError( oracle, @@ -437,6 +442,11 @@ describe("AccountingOracle.sol:happyPath", () => { expect(totalReportCalls).to.equal(2); }); + it("Staking router was told that exited keys updating is finished", async () => { + const totalFinishedCalls = await mockStakingRouter.totalCalls_onValidatorsCountsByNodeOperatorReportingFinished(); + expect(totalFinishedCalls).to.equal(2); + }); + it(`extra data for the same reference slot cannot be re-submitted`, async () => { await expect(oracle.connect(member1).submitReportExtraDataEmpty()).to.be.revertedWithCustomError( oracle, diff --git a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts index 42f16bbe28..48e5294889 100644 --- a/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts +++ b/test/0.8.9/oracle/accountingOracle.submitReportExtraData.test.ts @@ -40,14 +40,11 @@ import { deployAndConfigureAccountingOracle } from "test/deploy"; import { Snapshot } from "test/suite"; const getDefaultExtraData = (): ExtraDataType => ({ - // stuckKeys: [ - // { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - // { moduleId: 2, nodeOpIds: [0], keysCounts: [2] }, - // { moduleId: 3, nodeOpIds: [2], keysCounts: [3] }, - // ], exitedKeys: [ - { moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }, - { moduleId: 3, nodeOpIds: [1], keysCounts: [2] }, + { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, + { moduleId: 2, nodeOpIds: [0, 1, 2, 3, 4], keysCounts: [0, 1, 2, 3, 4] }, + { moduleId: 3, nodeOpIds: [1, 2], keysCounts: [2, 3] }, + { moduleId: 4, nodeOpIds: [0], keysCounts: [2] }, ], }); @@ -181,6 +178,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { reportFields, config, }: ConstructOracleReportWithDefaultValuesProps) { + await consensus.advanceTimeToNextFrameStart(); const data = await constructOracleReportWithDefaultValuesForCurrentRefSlot({ extraData, reportFields, @@ -247,8 +245,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { .withArgs(0, EXTRA_DATA_TYPE_STUCK_VALIDATORS); }); }); - // TODO: Rewrite tests to account for the fact that the stuck item type is no longer supported - context.skip("submitReportExtraDataList", () => { + context("submitReportExtraDataList", () => { beforeEach(takeSnapshot); afterEach(rollback); @@ -258,7 +255,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { expect(extraDataChunks.length).to.be.equal(1); await oracleMemberSubmitReportData(report); const tx = await oracleMemberSubmitExtraData(extraDataChunks[0]); - await expect(tx).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, 5, 5); + await expect(tx).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, 4, 4); }); it("submit extra data report within two transaction", async () => { @@ -276,23 +273,23 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { (BigInt(itemType) << 240n) | (BigInt(moduleId) << 64n) | BigInt(firstNodeOpId); const stateBeforeProcessingStart = await oracle.getExtraDataProcessingState(); - expect(stateBeforeProcessingStart.itemsCount).to.be.equal(5); + expect(stateBeforeProcessingStart.itemsCount).to.be.equal(4); expect(stateBeforeProcessingStart.itemsProcessed).to.be.equal(0); expect(stateBeforeProcessingStart.submitted).to.be.equal(false); expect(stateBeforeProcessingStart.lastSortingKey).to.be.equal(0); expect(stateBeforeProcessingStart.dataHash).to.be.equal(extraDataChunkHashes[0]); const tx1 = await oracleMemberSubmitExtraData(extraDataChunks[0]); - await expect(tx1).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, 3, 5); + await expect(tx1).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, 3, 4); const state1 = await oracle.getExtraDataProcessingState(); - expect(state1.itemsCount).to.be.equal(5); + expect(state1.itemsCount).to.be.equal(4); expect(state1.itemsProcessed).to.be.equal(3); expect(state1.submitted).to.be.equal(false); expect(state1.lastSortingKey).to.be.equal( calcSortingKey( - EXTRA_DATA_TYPE_STUCK_VALIDATORS, + EXTRA_DATA_TYPE_EXITED_VALIDATORS, defaultExtraData.exitedKeys[2].moduleId, - defaultExtraData.exitedKeys[2].nodeOpIds[0], + defaultExtraData.exitedKeys[2].nodeOpIds[1], // defaultExtraData.stuckKeys[2].moduleId, // defaultExtraData.stuckKeys[2].nodeOpIds[0], @@ -301,16 +298,16 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { expect(state1.dataHash).to.be.equal(extraDataChunkHashes[1]); const tx2 = await oracleMemberSubmitExtraData(extraDataChunks[1]); - await expect(tx2).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, 5, 5); + await expect(tx2).to.emit(oracle, "ExtraDataSubmitted").withArgs(report.refSlot, 4, 4); const state2 = await oracle.getExtraDataProcessingState(); - expect(state2.itemsCount).to.be.equal(5); - expect(state2.itemsProcessed).to.be.equal(5); + expect(state2.itemsCount).to.be.equal(4); + expect(state2.itemsProcessed).to.be.equal(4); expect(state2.submitted).to.be.equal(true); expect(state2.lastSortingKey).to.be.equal( calcSortingKey( EXTRA_DATA_TYPE_EXITED_VALIDATORS, - defaultExtraData.exitedKeys[1].moduleId, - defaultExtraData.exitedKeys[1].nodeOpIds[0], + defaultExtraData.exitedKeys[3].moduleId, + defaultExtraData.exitedKeys[3].nodeOpIds[0], ), ); expect(state2.dataHash).to.be.equal(extraDataChunkHashes[1]); @@ -318,19 +315,15 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("submit extra data with multiple items per module splitted on many transactions", async () => { const validExtraDataForLargeReport = { - stuckKeys: [ - { moduleId: 1, nodeOpIds: [0, 1, 2], keysCounts: [1, 2, 3] }, - { moduleId: 1, nodeOpIds: [3, 4, 5], keysCounts: [4, 5, 6] }, - { moduleId: 2, nodeOpIds: [0], keysCounts: [1] }, - ], exitedKeys: [ - { moduleId: 1, nodeOpIds: [0, 1, 2, 3], keysCounts: [1, 2, 3, 4] }, - { moduleId: 1, nodeOpIds: [4, 5], keysCounts: [4, 5] }, - { moduleId: 2, nodeOpIds: [0], keysCounts: [1] }, + { moduleId: 1, nodeOpIds: [0, 1, 2], keysCounts: [1, 2, 3] }, + { moduleId: 2, nodeOpIds: [0, 1, 2], keysCounts: [4, 5, 4] }, + { moduleId: 2, nodeOpIds: [4, 5], keysCounts: [4, 5] }, + { moduleId: 3, nodeOpIds: [0], keysCounts: [1] }, ], }; - // Submit report with extra data splitted on many transactions + // Submit report with extra data split on many transactions // It does not matter on how many transactions we will split extra data for (let i = 1; i <= 6; i++) { await consensus.advanceTimeToNextFrameStart(); @@ -416,6 +409,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { context("checks ref slot", () => { it("reverts with CannotSubmitExtraDataBeforeMainData in attempt of try to pass extra data ahead of submitReportData", async () => { + await consensus.advanceTimeToNextFrameStart(); const { report, reportHash, extraDataChunks } = await constructOracleReportWithDefaultValuesForCurrentRefSlot( {}, ); @@ -427,6 +421,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { }); it("pass successfully ", async () => { + await consensus.advanceTimeToNextFrameStart(); const { report, reportHash, extraDataChunks } = await constructOracleReportWithDefaultValuesForCurrentRefSlot( {}, ); @@ -445,7 +440,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { const { reportFields, extraDataHash } = await submitReportHash(); await oracle.connect(member1).submitReportData(reportFields, oracleVersion); const incorrectExtraData = getDefaultExtraData(); - // ++incorrectExtraData.stuckKeys[0].nodeOpIds[0]; + ++incorrectExtraData.exitedKeys[0].nodeOpIds[0]; const incorrectExtraDataItems = encodeExtraDataItems(incorrectExtraData); const incorrectExtraDataList = packExtraDataList(incorrectExtraDataItems); const incorrectExtraDataHash = calcExtraDataListHash(incorrectExtraDataList); @@ -467,6 +462,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { const incorrectExtraData = getDefaultExtraData(); ++incorrectExtraData.exitedKeys[0].nodeOpIds[0]; + ++incorrectExtraData.exitedKeys[3].nodeOpIds[0]; const { extraDataChunks: incorrectExtraDataChunks, extraDataChunkHashes: incorrectExtraDataChunkHashes } = await constructOracleReportWithDefaultValuesForCurrentRefSlot({ @@ -577,7 +573,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { }); expect(extraDataChunks.length).to.be.equal(2); - const wrongItemsCount = 4; + const wrongItemsCount = 5; const reportWithWrongItemsCount = { ...report, extraDataItemsCount: wrongItemsCount }; const hashOfReportWithWrongItemsCount = calcReportDataHash(getReportDataItems(reportWithWrongItemsCount)); @@ -586,7 +582,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { await oracleMemberSubmitExtraData(extraDataChunks[0]); await expect(oracleMemberSubmitExtraData(extraDataChunks[1])) .to.be.revertedWithCustomError(oracle, "UnexpectedExtraDataItemsCount") - .withArgs(reportWithWrongItemsCount.extraDataItemsCount, 5); + .withArgs(reportWithWrongItemsCount.extraDataItemsCount, 4); }); }); @@ -686,20 +682,20 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { .withArgs(invalidItemIndex); } - it("should revert if stuckKeys processed after exitedKeys", async () => { - const invalidExtraDataItemsArray = [ - { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 2, nodeOpIds: [0], keysCounts: [1] }, - { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - ]; - - await extraDataSubmitShouldRevertWithSortOrderError(invalidExtraDataItemsArray, 2); - }); + // it("should revert if stuckKeys processed after exitedKeys", async () => { + // const invalidExtraDataItemsArray = [ + // { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, + // { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 2, nodeOpIds: [0], keysCounts: [1] }, + // { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, + // ]; + // + // await extraDataSubmitShouldRevertWithSortOrderError(invalidExtraDataItemsArray, 2); + // }); it("should revert if modules items not sorted by module id", async () => { const invalidStuckKeysItemsOrder = [ - { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 2, nodeOpIds: [0], keysCounts: [1] }, - { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, + { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 2, nodeOpIds: [0], keysCounts: [1] }, + { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, ]; await extraDataSubmitShouldRevertWithSortOrderError(invalidStuckKeysItemsOrder, 1); @@ -718,7 +714,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { return [{ type, moduleId: 1, nodeOpIds, keysCounts: Array(nodeOpIds.length).fill(1) }]; } - await extraDataSubmitShouldRevertWithSortOrderError(data(EXTRA_DATA_TYPE_STUCK_VALIDATORS), 0); + // await extraDataSubmitShouldRevertWithSortOrderError(data(EXTRA_DATA_TYPE_STUCK_VALIDATORS), 0); await extraDataSubmitShouldRevertWithSortOrderError(data(EXTRA_DATA_TYPE_EXITED_VALIDATORS), 0); } @@ -730,12 +726,12 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { }); it("should revert if module node operators not sorted within multiple items", async () => { - const invalidStuckKeysItemsOrder = [ - { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [1, 2, 3], keysCounts: [1, 2, 3] }, - { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [3, 4, 5], keysCounts: [1, 2, 3] }, - ]; - - await extraDataSubmitShouldRevertWithSortOrderError(invalidStuckKeysItemsOrder, 1); + // const invalidStuckKeysItemsOrder = [ + // { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [1, 2, 3], keysCounts: [1, 2, 3] }, + // { type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, moduleId: 1, nodeOpIds: [3, 4, 5], keysCounts: [1, 2, 3] }, + // ]; + // + // await extraDataSubmitShouldRevertWithSortOrderError(invalidStuckKeysItemsOrder, 1); const invalidExitedKeysItemsOrder = [ { type: EXTRA_DATA_TYPE_EXITED_VALIDATORS, moduleId: 1, nodeOpIds: [1, 2, 3], keysCounts: [1, 2, 3] }, @@ -747,14 +743,12 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("should revert if second transaction extra data not sorted", async () => { const invalidExtraData = { - stuckKeys: [ - { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - { moduleId: 2, nodeOpIds: [3], keysCounts: [2] }, - // Items for second transaction. + exitedKeys: [ + { moduleId: 1, nodeOpIds: [1, 2], keysCounts: [1, 3] }, + { moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }, // Break report data sorting order, nodeOpId 3 already processed. - { moduleId: 2, nodeOpIds: [3], keysCounts: [4] }, + { moduleId: 2, nodeOpIds: [2], keysCounts: [3] }, ], - exitedKeys: [{ moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }], }; const { extraDataChunks } = await submitOracleReport({ @@ -774,12 +768,12 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { // contextual helper to prepeare wrong indexed data const getExtraWithCustomLastIndex = (itemsCount: number, lastIndexCustom: number) => { const dummyArr = Array.from(Array(itemsCount)); - const stuckKeys = dummyArr.map((_, i) => ({ moduleId: i + 1, nodeOpIds: [0], keysCounts: [i + 1] })); - const extraData = { stuckKeys, exitedKeys: [] }; + const exitedKeys = dummyArr.map((_, i) => ({ moduleId: i + 1, nodeOpIds: [0], keysCounts: [i + 1] })); + const extraData = { exitedKeys }; const extraDataItems: string[] = []; - const type = EXTRA_DATA_TYPE_STUCK_VALIDATORS; + const type = EXTRA_DATA_TYPE_EXITED_VALIDATORS; dummyArr.forEach((_, i) => { - const item = extraData.stuckKeys[i]; + const item = extraData.exitedKeys[i]; const index = i < itemsCount - 1 ? i : lastIndexCustom; extraDataItems.push(encodeExtraDataItem(index, type, item.moduleId, item.nodeOpIds, item.keysCounts)); }); @@ -845,10 +839,9 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { // contextual helper to prepeare wrong typed data const getExtraWithCustomType = (typeCustom: bigint) => { const extraData = { - stuckKeys: [{ moduleId: 1, nodeOpIds: [1], keysCounts: [2] }], - exitedKeys: [], + exitedKeys: [{ moduleId: 1, nodeOpIds: [1], keysCounts: [2] }], }; - const item = extraData.stuckKeys[0]; + const item = extraData.exitedKeys[0]; const extraDataItems = []; extraDataItems.push(encodeExtraDataItem(0, typeCustom, item.moduleId, item.nodeOpIds, item.keysCounts)); return { @@ -879,13 +872,14 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { .withArgs(wrongTypedIndex, typeCustom); }); - it("succeeds if `1` was passed", async () => { - const { extraDataItems } = getExtraWithCustomType(1n); + it("if type `1` was passed", async () => { + const { extraDataItems, wrongTypedIndex, typeCustom } = getExtraWithCustomType(1n); await consensus.advanceTimeToNextFrameStart(); const { reportFields, extraDataList } = await submitReportHash({ extraData: extraDataItems }); await oracle.connect(member1).submitReportData(reportFields, oracleVersion); - const tx = await oracle.connect(member1).submitReportExtraDataList(extraDataList); - await expect(tx).to.emit(oracle, "ExtraDataSubmitted").withArgs(reportFields.refSlot, anyValue, anyValue); + await expect(oracle.connect(member1).submitReportExtraDataList(extraDataList)) + .to.be.revertedWithCustomError(oracle, "DeprecatedExtraDataType") + .withArgs(wrongTypedIndex, typeCustom); }); it("succeeds if `2` was passed", async () => { @@ -902,10 +896,9 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("by reverting TooManyNodeOpsPerExtraDataItem if there was too much node operators", async () => { const problematicItemIdx = 0; const extraData = { - stuckKeys: [{ moduleId: 1, nodeOpIds: [1, 2], keysCounts: [2, 3] }], - exitedKeys: [], + exitedKeys: [{ moduleId: 1, nodeOpIds: [1, 2], keysCounts: [2, 3] }], }; - const problematicItemsCount = extraData.stuckKeys[problematicItemIdx].nodeOpIds.length; + const problematicItemsCount = extraData.exitedKeys[problematicItemIdx].nodeOpIds.length; await consensus.advanceTimeToNextFrameStart(); const { reportFields, extraDataList } = await submitReportHash({ extraData }); await oracle.connect(member1).submitReportData(reportFields, oracleVersion); @@ -922,10 +915,9 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("should not revert in case when items count exactly equals limit", async () => { const problematicItemIdx = 0; const extraData = { - stuckKeys: [{ moduleId: 1, nodeOpIds: [1, 2], keysCounts: [2, 3] }], - exitedKeys: [], + exitedKeys: [{ moduleId: 1, nodeOpIds: [1, 2], keysCounts: [2, 3] }], }; - const problematicItemsCount = extraData.stuckKeys[problematicItemIdx].nodeOpIds.length; + const problematicItemsCount = extraData.exitedKeys[problematicItemIdx].nodeOpIds.length; await consensus.advanceTimeToNextFrameStart(); const { reportFields, extraDataList } = await submitReportHash({ extraData }); await oracle.connect(member1).submitReportData(reportFields, oracleVersion); @@ -945,7 +937,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { config: { maxItemsPerChunk }, }); - expect(itemsCount).to.be.equal(5); + expect(itemsCount).to.be.equal(4); expect(extraDataChunks.length).to.be.equal(2); await oracleMemberSubmitReportData(report); @@ -972,11 +964,10 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("reverts if some item not long enough to contain all necessary data — early cut", async () => { const invalidItemIndex = 1; const extraData = { - stuckKeys: [ + exitedKeys: [ { moduleId: 1, nodeOpIds: [1], keysCounts: [2] }, { moduleId: 2, nodeOpIds: [1], keysCounts: [2] }, ], - exitedKeys: [], }; const extraDataItems = encodeExtraDataItems(extraData); // Cutting item to provoke error on early stage @@ -994,11 +985,10 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("reverts if some item not long enough to contain all necessary data — late cut", async () => { const invalidItemIndex = 1; const extraData = { - stuckKeys: [ + exitedKeys: [ { moduleId: 1, nodeOpIds: [1], keysCounts: [2] }, { moduleId: 2, nodeOpIds: [1, 2, 3, 4], keysCounts: [2] }, ], - exitedKeys: [], }; const extraDataItems = encodeExtraDataItems(extraData); // Providing long items and cutting them from end to provoke error on late stage @@ -1016,11 +1006,10 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { it("moduleId cannot be zero", async () => { const invalidItemIndex = 1; const extraData = { - stuckKeys: [ + exitedKeys: [ { moduleId: 1, nodeOpIds: [1], keysCounts: [2] }, { moduleId: 0, nodeOpIds: [1], keysCounts: [2] }, ], - exitedKeys: [], }; await consensus.advanceTimeToNextFrameStart(); const { reportFields, extraDataList } = await submitReportHash({ extraData }); @@ -1035,11 +1024,10 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { // Empty nodeOpIds list should provoke check fail // in `_processExtraDataItem` function, 812 line in AccountingOracle, second condition const extraData = { - stuckKeys: [ + exitedKeys: [ { moduleId: 1, nodeOpIds: [], keysCounts: [2] }, { moduleId: 2, nodeOpIds: [1], keysCounts: [2] }, ], - exitedKeys: [], }; await consensus.advanceTimeToNextFrameStart(); const { reportFields, extraDataList } = await submitReportHash({ extraData }); @@ -1077,29 +1065,6 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { }); context("delivers the data to staking router", () => { - it("calls reportStakingModuleStuckValidatorsCountByNodeOperator on StakingRouter", async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportFields, reportInput, extraDataList } = await submitReportHash(); - await oracle.connect(member1).submitReportData(reportFields, oracleVersion); - - await oracle.connect(member1).submitReportExtraDataList(extraDataList); - - const callsCount = await stakingRouter.totalCalls_reportStuckKeysByNodeOperator(); - - const extraDataValue = reportInput.extraDataValue as ExtraDataType; - // expect(callsCount).to.equal(extraDataValue.stuckKeys.length); - expect(callsCount).to.equal(extraDataValue.exitedKeys.length); - - for (let i = 0; i < callsCount; i++) { - const call = await stakingRouter.calls_reportStuckKeysByNodeOperator(i); - // const item = extraDataValue.stuckKeys[i]; - const item = extraDataValue.exitedKeys[i]; - expect(call.stakingModuleId).to.equal(item.moduleId); - expect(call.nodeOperatorIds).to.equal("0x" + item.nodeOpIds.map((id) => numberToHex(id, 8)).join("")); - expect(call.keysCounts).to.equal("0x" + item.keysCounts.map((count) => numberToHex(count, 16)).join("")); - } - }); - it("calls reportStakingModuleExitedValidatorsCountByNodeOperator on StakingRouter", async () => { await consensus.advanceTimeToNextFrameStart(); const { reportFields, reportInput, extraDataList } = await submitReportHash(); @@ -1141,6 +1106,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { }); it("reverts if main data has not been processed yet", async () => { + await constructOracleReportForCurrentFrameAndSubmitReportHash({}); await consensus.advanceTimeToNextFrameStart(); const report1 = await constructOracleReportWithSingeExtraDataTransactionForCurrentRefSlot(); @@ -1195,7 +1161,7 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { expect(stateAfter.itemsProcessed).to.equal(extraDataItemsCount); // TODO: figure out how to build this value and test it properly expect(stateAfter.lastSortingKey).to.equal( - "3533694129556768659166595001485837031654967793751237971583444623713894401", + "3533694129556768659166595001485837031654967793751237990030188697423446016", ); expect(stateAfter.dataHash).to.equal(extraDataHash); }); @@ -1204,12 +1170,8 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { await consensus.advanceTimeToNextFrameStart(); const extraDataDay1 = { - stuckKeys: [ - { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, - { moduleId: 2, nodeOpIds: [0], keysCounts: [2] }, - { moduleId: 3, nodeOpIds: [2], keysCounts: [3] }, - ], exitedKeys: [ + { moduleId: 1, nodeOpIds: [0], keysCounts: [1] }, { moduleId: 2, nodeOpIds: [1, 2], keysCounts: [1, 3] }, { moduleId: 3, nodeOpIds: [1], keysCounts: [2] }, ], @@ -1218,12 +1180,12 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { const { report: reportDay1, extraDataChunks: extraDataChunksDay1 } = await constructOracleReportWithDefaultValuesForCurrentRefSlot({ extraData: extraDataDay1, - config: { maxItemsPerChunk: 4 }, + config: { maxItemsPerChunk: 2 }, }); expect(extraDataChunksDay1.length).to.be.equal(2); - const validExtraDataItemsCount = 5; + const validExtraDataItemsCount = 3; const invalidExtraDataItemsCount = 7; const reportDay1WithInvalidItemsCount = { ...reportDay1, extraDataItemsCount: invalidExtraDataItemsCount }; @@ -1242,13 +1204,12 @@ describe("AccountingOracle.sol:submitReportExtraData", () => { await consensus.advanceTimeToNextFrameStart(); const extraDataDay2 = JSON.parse(JSON.stringify(extraDataDay1)); - extraDataDay2.stuckKeys[0].keysCounts = [2]; - extraDataDay2.exitedKeys[0].keysCounts = [1, 4]; + extraDataDay2.exitedKeys[2].keysCounts = [4]; const { report: reportDay2, extraDataChunks: extraDataChunksDay2 } = await constructOracleReportForCurrentFrameAndSubmitReportHash({ extraData: extraDataDay2, - config: { maxItemsPerChunk: 4 }, + config: { maxItemsPerChunk: 2 }, }); await oracleMemberSubmitReportData(reportDay2); From 9f8e619ee438cfdeb45214ee70bf42d0eeadb836 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 16:14:55 +0200 Subject: [PATCH 206/405] fix: add AO deploy to TW deploy script --- scripts/deploy-tw.sh | 2 +- scripts/triggerable-withdrawals/tw-deploy.ts | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/deploy-tw.sh b/scripts/deploy-tw.sh index eabc206625..3a86a9f7f8 100755 --- a/scripts/deploy-tw.sh +++ b/scripts/deploy-tw.sh @@ -18,5 +18,5 @@ export NETWORK_STATE_FILE="deployed-hoodi.json" # Need this to get sure the last transactions are mined -yarn hardhat --network $NETWORK run scripts/triggerable-withdrawals/tw-deploy.ts +npx hardhat --network $NETWORK run scripts/triggerable-withdrawals/tw-deploy.ts diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 372016f592..dbece9a86f 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -38,6 +38,10 @@ async function main() { const state = readNetworkState(); persistNetworkState(state); + const chainSpec = state[Sk.chainSpec]; + + log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); + // Read contracts addresses from config const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); @@ -123,9 +127,22 @@ async function main() { log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); log.emptyLine(); + const accountingOracle = await deployImplementation( + Sk.accountingOracle, + "AccountingOracle", + deployer, + [ + locator.address, + await locator.lido(), + await locator.legacyOracle(), + Number(chainSpec.secondsPerSlot), + Number(chainSpec.genesisTime), + ], + ); + // fetch contract addresses that will not changed const locatorConfig = [ - await locator.accountingOracle(), + accountingOracle.address, await locator.depositSecurityModule(), await locator.elRewardsVault(), await locator.legacyOracle(), @@ -148,6 +165,7 @@ async function main() { log(`Configuration for voting script:`); log(` LIDO_LOCATOR_IMPL = "${lidoLocator.address}" +ACCOUNTING_ORACLE = "${accountingOracle.address}" VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}" WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" From 7642192a69ab5e8e1af671e05da48b05b4bc9b4e Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 16:20:53 +0200 Subject: [PATCH 207/405] refactor: remove unused IBurner interface import from NodeOperatorsRegistry --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index b465121c89..0c9d1deb2e 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -11,7 +11,6 @@ import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStora import {Math256} from "../../common/lib/Math256.sol"; import {MinFirstAllocationStrategy} from "../../common/lib/MinFirstAllocationStrategy.sol"; import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; -import {IBurner} from "../../common/interfaces/IBurner.sol"; import {SigningKeys} from "../lib/SigningKeys.sol"; import {Packed64x4} from "../lib/Packed64x4.sol"; import {Versioned} from "../utils/Versioned.sol"; From 9ad1ec20a61f64fac417c968fdd775974050f3bd Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 16:27:29 +0200 Subject: [PATCH 208/405] refactor: remove penalization events from NodeOperatorsRegistry --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 0c9d1deb2e..846598e67c 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -52,8 +52,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { event NonceChanged(uint256 nonce); event TargetValidatorsCountChanged(uint256 indexed nodeOperatorId, uint256 targetValidatorsCount, uint256 targetLimitMode); - event NodeOperatorPenalized(address indexed recipientAddress, uint256 sharesPenalizedAmount); - event NodeOperatorPenaltyCleared(uint256 indexed nodeOperatorId); event ValidatorExitStatusUpdated( uint256 indexed nodeOperatorId, From 471949fa48c7b7cd0aeceb6b1a855761440104b2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 16:54:00 +0200 Subject: [PATCH 209/405] refactor: remove deprecated reportStakingModuleStuckValidatorsCountByNodeOperator function --- contracts/0.8.9/oracle/AccountingOracle.sol | 22 +++++++------------ ...StakingRouter__MockForAccountingOracle.sol | 10 --------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index d26c53ecb2..03c5b7d02b 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -68,12 +68,6 @@ interface IStakingRouter { bytes calldata exitedValidatorsCounts ) external; - function reportStakingModuleStuckValidatorsCountByNodeOperator( - uint256 stakingModuleId, - bytes calldata nodeOperatorIds, - bytes calldata stuckValidatorsCounts - ) external; - function onValidatorsCountsByNodeOperatorReportingFinished() external; } @@ -822,17 +816,17 @@ contract AccountingOracle is BaseOracle { revert DeprecatedExtraDataType(index, itemType); } - if (itemType == EXTRA_DATA_TYPE_EXITED_VALIDATORS) { - uint256 nodeOpsProcessed = _processExtraDataItem(data, iter); - - if (nodeOpsProcessed > maxNodeOperatorsPerItem) { - maxNodeOperatorsPerItem = nodeOpsProcessed; - maxNodeOperatorItemIndex = index; - } - } else { + if (itemType != EXTRA_DATA_TYPE_EXITED_VALIDATORS) { revert UnsupportedExtraDataType(index, itemType); } + uint256 nodeOpsProcessed = _processExtraDataItem(data, iter); + + if (nodeOpsProcessed > maxNodeOperatorsPerItem) { + maxNodeOperatorsPerItem = nodeOpsProcessed; + maxNodeOperatorItemIndex = index; + } + assert(iter.dataOffset > dataOffset); dataOffset = iter.dataOffset; unchecked { diff --git a/test/0.8.9/contracts/StakingRouter__MockForAccountingOracle.sol b/test/0.8.9/contracts/StakingRouter__MockForAccountingOracle.sol index b2233b6f5b..8f61c1482f 100644 --- a/test/0.8.9/contracts/StakingRouter__MockForAccountingOracle.sol +++ b/test/0.8.9/contracts/StakingRouter__MockForAccountingOracle.sol @@ -72,16 +72,6 @@ contract StakingRouter__MockForAccountingOracle is IStakingRouter { ); } - function reportStakingModuleStuckValidatorsCountByNodeOperator( - uint256 stakingModuleId, - bytes calldata nodeOperatorIds, - bytes calldata stuckKeysCounts - ) external { - calls_reportStuckKeysByNodeOperator.push( - ReportKeysByNodeOperatorCallData(stakingModuleId, nodeOperatorIds, stuckKeysCounts) - ); - } - function onValidatorsCountsByNodeOperatorReportingFinished() external { ++totalCalls_onValidatorsCountsByNodeOperatorReportingFinished; } From 3751fa881dbbecdd8fec65504b84c89a70db6e3c Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 28 May 2025 17:01:59 +0200 Subject: [PATCH 210/405] fix: SR role for TWG --- scripts/scratch/steps/0130-grant-roles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index b747f846c5..9d2f0bb4e3 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -55,7 +55,7 @@ export async function main() { await makeTx( stakingRouter, "grantRole", - [await stakingRouter.REPORT_EXITED_VALIDATORS_ROLE(), triggerableWithdrawalsGatewayAddress], + [await stakingRouter.REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE(), triggerableWithdrawalsGatewayAddress], { from: deployer }, ); From 65a7b1d5ff8dfbadfbf71e84d39b7cea839d2b28 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 28 May 2025 19:53:32 +0400 Subject: [PATCH 211/405] fix: maxBatchSize != 0 --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 6 ++++-- ...or-exit-bus-oracle.submitExitRequestsData.test.ts | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 1a88c3493d..820c074956 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -28,7 +28,7 @@ interface ITriggerableWithdrawalsGateway { * @notice Сontract that serves as the central infrastructure for managing validator exit requests. * It stores report hashes, emits exit events, and maintains data and tools that enables anyone to prove a validator was requested to exit. */ -contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned { +abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned { using UnstructuredStorage for bytes32; using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; @@ -294,7 +294,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned // if totalItemsCount == requestsToDeliver, we deliver it via one attempt and don't need to store deliveryHistory array // can store in RequestStatus via lastDeliveredExitDataIndex and lastDeliveredExitDataTimestamp fields - bool deliverFullyAtOnce = totalItemsCount == requestsToDeliver && requestStatus.deliveryHistoryLength == 0; + bool deliverFullyAtOnce = totalItemsCount == requestsToDeliver; if (!deliverFullyAtOnce) { _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); } @@ -566,6 +566,8 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned } function _setMaxRequestsPerBatch(uint256 value) internal { + require(value > 0, "MAX_BATCH_SIZE_ZERO"); + MAX_VALIDATORS_PER_BATCH_POSITION.setStorageUint256(value); } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 8f28b8ac05..9705b5935f 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -443,14 +443,20 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); + it("Should not allow to set new maximum requests per batch value eq to 0", async () => { + const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); + await oracle.grantRole(role, authorizedEntity); + + await expect( + oracle.connect(authorizedEntity).setMaxRequestsPerBatch(0), + ).to.be.revertedWith('MAX_BATCH_SIZE_ZERO'); + }); + it("Should limit request by MAX_VALIDATORS_PER_BATCH if it is smaller than available vebo limit", async () => { await consensus.advanceTimeBy(MAX_EXIT_REQUESTS_LIMIT * 4 * 12); const data = await oracle.getExitRequestLimitFullInfo(); expect(data.currentExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); - const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); - await oracle.grantRole(role, authorizedEntity); - const maxRequestsPerBatch = 4; await oracle.connect(authorizedEntity).setMaxRequestsPerBatch(maxRequestsPerBatch); From a6d79f7a75884b3cc817a7306a1401fd3e74445f Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 11:35:16 +0200 Subject: [PATCH 212/405] feat: remove redundant history length check in exit delay verifier contract --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 56d693ce50..35bc747e7b 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -372,15 +372,12 @@ contract ValidatorExitDelayVerifier { } // Sanity check, delivery history is strictly monotonically increasing. - if (history.length > 1) { - for (uint256 i = 1; i < history.length; i++) { - // strictly increasing on both keys index and timestamps - if ( - history[i].lastDeliveredKeyIndex <= history[i - 1].lastDeliveredKeyIndex || - history[i].timestamp <= history[i - 1].timestamp - ) { - revert NonMonotonicDeliveryHistory(i); - } + for (uint256 i = 1; i < history.length; i++) { + if ( + history[i].lastDeliveredKeyIndex <= history[i - 1].lastDeliveredKeyIndex || + history[i].timestamp <= history[i - 1].timestamp + ) { + revert NonMonotonicDeliveryHistory(i); } } From b21441d9b711ac4931b02b0ca9b5c0600508b013 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 11:54:17 +0200 Subject: [PATCH 213/405] feat: improve comments in exit delay verifier contract --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 35bc747e7b..7f638d9bab 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.25; @@ -160,9 +160,9 @@ contract ValidatorExitDelayVerifier { * @notice Verifies that the provided validators were not requested to exit on the CL after a VEB exit request. * Reports exit delays to the Staking Router. * @dev Ensures that `exitEpoch` is equal to `FAR_FUTURE_EPOCH` at the given beacon block. - * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited. + * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. */ function verifyValidatorExitDelay( ProvableBeaconBlockHeader calldata beaconBlock, @@ -204,10 +204,10 @@ contract ValidatorExitDelayVerifier { * @dev Ensures that `exitEpoch` is equal to `FAR_FUTURE_EPOCH` at the given beacon block. * @dev Verifies historical blocks (via historical_summaries). * @dev The oldBlock.header must have slot >= FIRST_SUPPORTED_SLOT. - * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. * @param beaconBlock The block header and EIP-4788 timestamp to prove the block root is known. * @param oldBlock Historical block header witness data and its proof. * @param validatorWitnesses Array of validator proofs to confirm they are not yet exited in oldBlock.header. + * @param exitRequests The concatenated VEBO exit requests, each 64 bytes in length. */ function verifyHistoricalValidatorExitDelay( ProvableBeaconBlockHeader calldata beaconBlock, From 9748ebbcc07270099b5c47204b3bc4a99e92ff45 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 29 May 2025 12:36:39 +0200 Subject: [PATCH 214/405] fix: update allowance for NOR to 0 after removing stuck keys logic --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 6 +++--- test/0.4.24/nor/nor.initialize.upgrade.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 846598e67c..db842ce48d 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -238,9 +238,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { TYPE_POSITION.setStorageBytes32(_type); _setContractVersion(2); - // set unlimited allowance for burner from staking router - // to burn stuck keys penalized shares - IStETH(getLocator().lido()).approve(getLocator().burner(), ~uint256(0)); emit LocatorContractSet(_locator); emit StakingModuleTypeSet(_type); @@ -272,6 +269,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function finalizeUpgrade_v4(uint256 _exitDeadlineThresholdInSeconds) external { require(hasInitialized(), "CONTRACT_NOT_INITIALIZED"); _checkContractVersion(3); + + // Set allowance to 0 since the stuck keys logic has been removed and burning shares is no longer needed + IStETH(getLocator().lido()).approve(getLocator().burner(), 0); _initialize_v4(_exitDeadlineThresholdInSeconds); } diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 8422bd1271..232738f367 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -121,7 +121,7 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { expect(await nor.getLocator()).to.equal(await locator.getAddress()); expect(await nor.getInitializationBlock()).to.equal(latestBlock + 1n); - expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(MaxUint256); + expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(0); expect(await nor.getContractVersion()).to.equal(4); expect(await nor.getType()).to.equal(moduleType); }); From c476e27fff1f0d3bce477ad157755c60193867e8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 15:49:34 +0400 Subject: [PATCH 215/405] fix: module notification wrap in try/catch --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 48 ++++++++++++++----- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 11 ++++- ...-bus-oracle.submitExitRequestsData.test.ts | 12 +++++ 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 20c4be19e0..826feae6ae 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -61,8 +61,15 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param exitsPerFrame The number of exits that can be restored per frame. * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ - event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); + /** + * @notice Emitted when notifying a staking module about a validator exit fails. + * @param stakingModuleId The ID of the staking module that failed to process the exit notification. + * @param nodeOperatorId The ID of the node operator for the validator. + * @param pubkey The public key of the validator for which the exit notification failed. + */ + event StakingModuleExitNotificationFailed(uint256 stakingModuleId, uint256 nodeOperatorId, bytes pubkey); + /** * @notice Thrown when remaining exit requests limit is not enough to cover sender requests * @param requestsCount Amount of requests that were sent for processing @@ -159,10 +166,11 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { * @param exitsPerFrame The number of exits that can be restored per frame. * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ - function setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec) - external - onlyRole(TW_EXIT_LIMIT_MANAGER_ROLE) - { + function setExitRequestLimit( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDurationInSec + ) external onlyRole(TW_EXIT_LIMIT_MANAGER_ROLE) { _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } @@ -216,9 +224,18 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { ValidatorData calldata data; for (uint256 i = 0; i < validatorsData.length; ++i) { data = validatorsData[i]; - stakingRouter.onValidatorExitTriggered( - data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalRequestPaidFee, exitType - ); + + try + stakingRouter.onValidatorExitTriggered( + data.stakingModuleId, + data.nodeOperatorId, + data.pubkey, + withdrawalRequestPaidFee, + exitType + ) + {} catch { + emit StakingModuleExitNotificationFailed(data.stakingModuleId, data.nodeOperatorId, data.pubkey); + } } } @@ -229,7 +246,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { recipient = msg.sender; } - (bool success,) = recipient.call{value: refund}(""); + (bool success, ) = recipient.call{value: refund}(""); if (!success) { revert FeeRefundFailed(); } @@ -240,14 +257,19 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { return block.timestamp; // solhint-disable-line not-rely-on-time } - function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec) - internal - { + function _setExitRequestLimit( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDurationInSec + ) internal { uint256 timestamp = _getTimestamp(); TWR_LIMIT_POSITION.setStorageExitRequestLimit( TWR_LIMIT_POSITION.getStorageExitRequestLimit().setExitLimits( - maxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp + maxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp ) ); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 1a88c3493d..d2138ebaaa 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -112,7 +112,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned /** * @notice Emitted when an entity with the SUBMIT_REPORT_HASH_ROLE role submits a hash of the exit requests data. - * @param exitRequestsHash - keccak256 hash of the encoded validators list + * @param exitRequestsHash keccak256 hash of the encoded validators list */ event RequestsHashSubmitted(bytes32 exitRequestsHash); /** @@ -137,6 +137,13 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); + /** + * @notice Emitted when exit requests were delivered + * @param exitRequestsHash keccak256 hash of the encoded validators list + * @param startIndex Start index of request in submitted data that will be delivered + * @param endIndex End index of request in submitted data that will be delivered + */ + event ExitDataProcessing(bytes32 exitRequestsHash, uint256 startIndex, uint256 endIndex); struct ExitRequestsData { bytes data; @@ -305,6 +312,8 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned newLastDeliveredIndex, _getTimestamp() ); + + emit ExitDataProcessing(exitRequestsHash, startIndex, newLastDeliveredIndex); } /** diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 8f28b8ac05..d51ec8fc59 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -372,6 +372,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .to.emit(oracle, "ValidatorExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } + + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(exitRequestHash, 0, 1); }); it("Should deliver part of request equal to remaining limit", async () => { @@ -385,6 +387,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .to.emit(oracle, "ValidatorExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } + + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(HASH_REQUEST_DELIVERED_BY_PARTS, 0, 2); }); it("Should revert when limit exceeded for the frame", async () => { @@ -424,6 +428,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .to.emit(oracle, "ValidatorExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } + + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(HASH_REQUEST_DELIVERED_BY_PARTS, 3, 4); }); it("Should revert when no new requests to deliver", async () => { @@ -483,6 +489,10 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } + await expect(tx) + .to.emit(oracle, "ExitDataProcessing") + .withArgs(exitRequestHashRandom, 0, maxRequestsPerBatch - 1); + const history = await oracle.getExitRequestsDeliveryHistory(exitRequestHashRandom); expect(history.length).to.be.equal(1); @@ -533,6 +543,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(exitRequestRandomHash, 0, 1); + const data = await oracle.getExitRequestLimitFullInfo(); expect(data.maxExitRequestsLimit).to.equal(0); From b0090c166cd8c7693927f9f9be3e4cbba842c2e8 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 16:11:59 +0400 Subject: [PATCH 216/405] fix: desc --- contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 826feae6ae..1a7431d83e 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -64,11 +64,11 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); /** * @notice Emitted when notifying a staking module about a validator exit fails. - * @param stakingModuleId The ID of the staking module that failed to process the exit notification. - * @param nodeOperatorId The ID of the node operator for the validator. - * @param pubkey The public key of the validator for which the exit notification failed. + * @param stakingModuleId Id of staking module. + * @param nodeOperatorId Id of node operator. + * @param validatorPubkey Public key of validator. */ - event StakingModuleExitNotificationFailed(uint256 stakingModuleId, uint256 nodeOperatorId, bytes pubkey); + event StakingModuleExitNotificationFailed(uint256 stakingModuleId, uint256 nodeOperatorId, bytes validatorPubkey); /** * @notice Thrown when remaining exit requests limit is not enough to cover sender requests From 13c5e4156c45f98e396905486c5bc3827b8e1428 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 29 May 2025 15:12:09 +0200 Subject: [PATCH 217/405] fix: locator deploy --- scripts/triggerable-withdrawals/tw-deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index d618c5a890..30f95af3e3 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -144,7 +144,7 @@ async function main() { // fetch contract addresses that will not changed const locatorConfig = [ - accountingOracle.address, + await locator.accountingOracle(), await locator.depositSecurityModule(), await locator.elRewardsVault(), await locator.legacyOracle(), From 03f2c981d2e757e2e32509378279b7995bb93c6b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 15:34:16 +0200 Subject: [PATCH 218/405] feat: make delivery history atomic --- .../0.8.25/ValidatorExitDelayVerifier.sol | 36 +--- .../0.8.25/interfaces/IValidatorsExitBus.sol | 4 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 175 +++++------------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 49 ++--- .../ValidatorsExitBusOracle_Mock.sol | 13 +- .../contracts/ValidatorsExitBus__Harness.sol | 35 +--- 6 files changed, 79 insertions(+), 233 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 7f638d9bab..7e1f1d81ea 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -174,7 +174,7 @@ contract ValidatorExitDelayVerifier { IValidatorsExitBus veb = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(veb, exitRequests); + uint256 deliveredTimestamp = _getExitRequestDeliveryHistory(veb, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { @@ -187,7 +187,7 @@ contract ValidatorExitDelayVerifier { ); uint256 eligibleToExitInSec = _getSecondsSinceExitIsEligible( - requestsDeliveryHistory, + deliveredTimestamp, witness, proofSlotTimestamp ); @@ -221,7 +221,7 @@ contract ValidatorExitDelayVerifier { IValidatorsExitBus veb = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - DeliveryHistory[] memory requestsDeliveryHistory = _getExitRequestDeliveryHistory(veb, exitRequests); + uint256 deliveredTimestamp = _getExitRequestDeliveryHistory(veb, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { @@ -234,7 +234,7 @@ contract ValidatorExitDelayVerifier { ); uint256 eligibleToExitInSec = _getSecondsSinceExitIsEligible( - requestsDeliveryHistory, + deliveredTimestamp, witness, proofSlotTimestamp ); @@ -326,12 +326,10 @@ contract ValidatorExitDelayVerifier { * @return uint256 The elapsed seconds since the earliest eligible exit request time. */ function _getSecondsSinceExitIsEligible( - DeliveryHistory[] memory history, + uint256 deliveredTimestamp, ValidatorWitness calldata witness, uint256 referenceSlotTimestamp ) internal view returns (uint256) { - uint256 validatorExitRequestTimestamp = _getExitRequestTimestamp(history, witness.exitRequestIndex); - // The earliest a validator can voluntarily exit is after the Shard Committee Period // subsequent to its activation epoch. uint256 earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + @@ -340,8 +338,8 @@ contract ValidatorExitDelayVerifier { // The actual eligible timestamp is the max between the exit request submission time // and the earliest possible voluntary exit time. - uint256 eligibleExitRequestTimestamp = validatorExitRequestTimestamp > earliestPossibleVoluntaryExitTimestamp - ? validatorExitRequestTimestamp + uint256 eligibleExitRequestTimestamp = deliveredTimestamp > earliestPossibleVoluntaryExitTimestamp + ? deliveredTimestamp : earliestPossibleVoluntaryExitTimestamp; if (referenceSlotTimestamp < eligibleExitRequestTimestamp) { @@ -363,25 +361,9 @@ contract ValidatorExitDelayVerifier { function _getExitRequestDeliveryHistory( IValidatorsExitBus veb, ExitRequestData calldata exitRequests - ) internal view returns (DeliveryHistory[] memory) { + ) internal view returns (uint256 timestamp) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); - DeliveryHistory[] memory history = veb.getExitRequestsDeliveryHistory(exitRequestsHash); - - if (history.length == 0) { - revert EmptyDeliveryHistory(); - } - - // Sanity check, delivery history is strictly monotonically increasing. - for (uint256 i = 1; i < history.length; i++) { - if ( - history[i].lastDeliveredKeyIndex <= history[i - 1].lastDeliveredKeyIndex || - history[i].timestamp <= history[i - 1].timestamp - ) { - revert NonMonotonicDeliveryHistory(i); - } - } - - return history; + return veb.getExitRequestsDeliveryHistory(exitRequestsHash); } function _getExitRequestTimestamp( diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol index b9df6196d3..2d526b42c9 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -10,9 +10,7 @@ struct DeliveryHistory { uint256 timestamp; } interface IValidatorsExitBus { - function getExitRequestsDeliveryHistory( - bytes32 exitRequestsHash - ) external view returns (DeliveryHistory[] memory history); + function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (uint256 timestamp); function unpackExitRequest( bytes calldata exitRequests, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d56387484c..7921361779 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -61,11 +61,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V */ error InvalidRequestsDataSortOrder(); - /** - * @notice Thrown when pubkeys of invalid length are provided - */ - error InvalidPubkeysArray(); - /** * Thrown when there are attempt to send exit events for request that was not submitted earlier by trusted entities */ @@ -81,11 +76,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V */ error RequestsAlreadyDelivered(); - /** - * @notice Thrown when any of the provided `exitDataIndexes` refers to a validator that was not yet delivered (i.e., exit request not emitted) - */ - error ExitDataWasNotDelivered(uint256 exitDataIndex, uint256 lastDeliveredExitDataIndex); - /** * @notice Thrown when index of request in submitted data for triggerable withdrawal is out of range */ @@ -106,7 +96,12 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /** * @notice Thrown when submitting was not started for request */ - error DeliveryWasNotStarted(); + error RequestsNotDelivered(); + + /** + * @notice Thrown when exit requests in report exceed the maximum allowed number of requests per batch. + */ + error ToManyExitRequestsInReport(uint256 totalItemsCount, uint256 maxRequestsPerBatch); /// @dev Events @@ -140,10 +135,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /** * @notice Emitted when exit requests were delivered * @param exitRequestsHash keccak256 hash of the encoded validators list - * @param startIndex Start index of request in submitted data that will be delivered - * @param endIndex End index of request in submitted data that will be delivered */ - event ExitDataProcessing(bytes32 exitRequestsHash, uint256 startIndex, uint256 endIndex); + event ExitDataProcessing(bytes32 exitRequestsHash); struct ExitRequestsData { bytes data; @@ -157,25 +150,11 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V bytes pubkey; } - // RequestStatus stores the last delivered index of the request, timestamp of delivery, and deliveryHistory length (number of deliveries). - // If a request is fully delivered in one step (as with oracle requests, which can't be delivered partially), - // only RequestStatus is used for efficiency. - // If a request is delivered in parts (e.g., due to limit constraints), - // DeliveryHistory[] stores full delivery records in addition to RequestStatus. - // If deliveryHistoryLength == 1, delivery info is read from RequestStatus; otherwise, from DeliveryHistory[]. - // Both mappings use the same key (exitRequestsHash). - + // RequestStatus stores timestamp of delivery, and contract version. + // Mappings use exitRequestsHash as key struct RequestStatus { uint32 contractVersion; - uint32 deliveryHistoryLength; - uint32 lastDeliveredExitDataIndex; // index of validator in request - uint32 lastDeliveredExitDataTimestamp; - } - - struct DeliveryHistory { - // index in array of requests - uint32 lastDeliveredExitDataIndex; - uint32 timestamp; + uint32 deliveredExitDataTimestamp; } /// @notice An ACL role granting the permission to submit a hash of the exit requests data @@ -222,8 +201,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V // Storage slot for mapping(bytes32 => RequestStatus), keyed by exitRequestsHash bytes32 internal constant REQUEST_STATUS_POSITION = keccak256("lido.ValidatorsExitBus.requestStatus"); - // Storage slot for mapping(bytes32 => DeliveryHistory[]), keyed by exitRequestsHash - bytes32 internal constant DELIVERY_HISTORY_POSITION = keccak256("lido.ValidatorsExitBus.deliveryHistory"); uint256 public constant EXIT_TYPE = 2; @@ -252,10 +229,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V */ function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); - _storeNewHashRequestStatus( - exitRequestsHash, - RequestStatus(uint32(contractVersion), 0, type(uint32).max, type(uint32).max) - ); + _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), 0)); } /** @@ -279,41 +253,24 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestsHash]; _checkExitSubmitted(requestStatus); + _checkNotDelivered(requestStatus); _checkExitRequestData(request.data, request.dataFormat); _checkContractVersion(requestStatus.contractVersion); uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; - uint32 lastDeliveredIndex = requestStatus.lastDeliveredExitDataIndex; - - uint256 startIndex = requestStatus.deliveryHistoryLength == 0 ? 0 : lastDeliveredIndex + 1; - uint256 undeliveredItemsCount = totalItemsCount - startIndex; + uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); - if (undeliveredItemsCount == 0) { - revert RequestsAlreadyDelivered(); + if (totalItemsCount > maxRequestsPerBatch) { + revert ToManyExitRequestsInReport(totalItemsCount, maxRequestsPerBatch); } - // take min between undeliveredItemsCount and maxBatchSize - uint256 requestsToDeliver = _consumeLimit(_applyMaxBatchSize(undeliveredItemsCount), _applyDeliverLimit); + uint256 requestsToDeliver = _consumeLimit(totalItemsCount, _applyDeliverLimit); - _processExitRequestsList(request.data, startIndex, requestsToDeliver); - - uint256 newLastDeliveredIndex = startIndex + requestsToDeliver - 1; - - // if totalItemsCount == requestsToDeliver, we deliver it via one attempt and don't need to store deliveryHistory array - // can store in RequestStatus via lastDeliveredExitDataIndex and lastDeliveredExitDataTimestamp fields - bool deliverFullyAtOnce = totalItemsCount == requestsToDeliver; - if (!deliverFullyAtOnce) { - _storeDeliveryEntry(exitRequestsHash, newLastDeliveredIndex, _getTimestamp()); - } + _processExitRequestsList(request.data, 0, requestsToDeliver); - _updateRequestStatus( - requestStatus, - requestStatus.deliveryHistoryLength + 1, - newLastDeliveredIndex, - _getTimestamp() - ); + _updateRequestStatus(requestStatus, _getTimestamp()); - emit ExitDataProcessing(exitRequestsHash, startIndex, newLastDeliveredIndex); + emit ExitDataProcessing(exitRequestsHash); } /** @@ -346,7 +303,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V ]; _checkExitSubmitted(requestStatus); - _checkDeliveryStarted(requestStatus); + _checkDelivered(requestStatus); _checkExitRequestData(exitsData.data, exitsData.dataFormat); _checkContractVersion(requestStatus.contractVersion); @@ -361,10 +318,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V revert ExitDataIndexOutOfRange(exitDataIndexes[i], requestsCount); } - if (exitDataIndexes[i] > requestStatus.lastDeliveredExitDataIndex) { - revert ExitDataWasNotDelivered(exitDataIndexes[i], requestStatus.lastDeliveredExitDataIndex); - } - if (i > 0 && exitDataIndexes[i] <= lastExitDataIndex) { revert InvalidExitDataIndexSortOrder(); } @@ -455,7 +408,9 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @dev Reverts if: * - exitRequestsHash was not submited */ - function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (DeliveryHistory[] memory) { + function getExitRequestsDeliveryHistory( + bytes32 exitRequestsHash + ) external view returns (uint256 deliveryDateTimestamp) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; @@ -463,22 +418,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V revert ExitHashNotSubmitted(); } - if (storedRequest.deliveryHistoryLength == 0) { - DeliveryHistory[] memory deliveryHistory; - return deliveryHistory; - } - - if (storedRequest.deliveryHistoryLength == 1) { - DeliveryHistory[] memory deliveryHistory = new DeliveryHistory[](1); - deliveryHistory[0] = DeliveryHistory( - storedRequest.lastDeliveredExitDataIndex, - storedRequest.lastDeliveredExitDataTimestamp - ); - return deliveryHistory; - } - - mapping(bytes32 => DeliveryHistory[]) storage deliveryHistoryMap = _storageDeliveryHistory(); - return deliveryHistoryMap[exitRequestsHash]; + return storedRequest.deliveredExitDataTimestamp; } /** @@ -553,20 +493,21 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } } - function _applyMaxBatchSize(uint256 requestsCount) internal view returns (uint256) { - uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); - return min(requestsCount, maxRequestsPerBatch); - } - function _checkExitSubmitted(RequestStatus storage requestStatus) internal view { if (requestStatus.contractVersion == 0) { revert ExitHashNotSubmitted(); } } - function _checkDeliveryStarted(RequestStatus storage status) internal view { - if (status.deliveryHistoryLength == 0) { - revert DeliveryWasNotStarted(); + function _checkNotDelivered(RequestStatus storage status) internal view { + if (status.deliveredExitDataTimestamp != 0) { + revert RequestsAlreadyDelivered(); + } + } + + function _checkDelivered(RequestStatus storage status) internal view { + if (status.deliveredExitDataTimestamp == 0) { + revert RequestsNotDelivered(); } } @@ -584,7 +525,11 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V return MAX_VALIDATORS_PER_BATCH_POSITION.getStorageUint256(); } - function _setExitRequestLimit(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec) internal { + function _setExitRequestLimit( + uint256 maxExitRequestsLimit, + uint256 exitsPerFrame, + uint256 frameDurationInSec + ) internal { uint256 timestamp = _getTimestamp(); EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( @@ -599,7 +544,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } - function _consumeLimit(uint256 requestsCount, function(uint256, uint256) internal pure returns(uint256) applyLimit) internal returns (uint256 requestsLimitedCount) { + function _consumeLimit( + uint256 requestsCount, + function(uint256, uint256) internal pure returns (uint256) applyLimit + ) internal returns (uint256 requestsLimitedCount) { ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (!exitRequestLimitData.isExitLimitSet()) { return requestsCount; @@ -633,34 +581,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V emit RequestsHashSubmitted(exitRequestsHash); } - function _updateRequestStatus( - RequestStatus storage requestStatus, - uint256 deliveryHistoryLength, - uint256 lastDeliveredExitDataIndex, - uint256 lastDeliveredExitDataTimestamp - ) internal { - require(deliveryHistoryLength <= type(uint32).max, "DELIVERY_HISTORY_LENGTH_OVERFLOW"); - require(lastDeliveredExitDataIndex <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW"); - require(lastDeliveredExitDataTimestamp <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); - - requestStatus.deliveryHistoryLength = uint32(deliveryHistoryLength); - requestStatus.lastDeliveredExitDataIndex = uint32(lastDeliveredExitDataIndex); - requestStatus.lastDeliveredExitDataTimestamp = uint32(lastDeliveredExitDataTimestamp); - } - - function _storeDeliveryEntry( - bytes32 exitRequestsHash, - uint256 lastDeliveredExitDataIndex, - uint256 lastDeliveredExitDataTimestamp - ) internal { - require(lastDeliveredExitDataIndex <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW"); - require(lastDeliveredExitDataTimestamp <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); + function _updateRequestStatus(RequestStatus storage requestStatus, uint256 deliveredExitDataTimestamp) internal { + require(deliveredExitDataTimestamp <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); - mapping(bytes32 => DeliveryHistory[]) storage deliveryHistoryMap = _storageDeliveryHistory(); - DeliveryHistory[] storage deliveryHistory = deliveryHistoryMap[exitRequestsHash]; - deliveryHistory.push( - DeliveryHistory(uint32(lastDeliveredExitDataIndex), uint32(lastDeliveredExitDataTimestamp)) - ); + requestStatus.deliveredExitDataTimestamp = uint32(deliveredExitDataTimestamp); } /// Methods for reading data from tightly packed validator exit requests @@ -726,7 +650,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V uint256 dataWithoutPubkey; uint256 moduleId; uint256 nodeOpId; - uint64 valIndex; + uint64 valIndex; assembly { pubkey.length := 48 @@ -775,11 +699,4 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V r.slot := position } } - - function _storageDeliveryHistory() internal pure returns (mapping(bytes32 => DeliveryHistory[]) storage r) { - bytes32 position = DELIVERY_HISTORY_POSITION; - assembly { - r.slot := position - } - } } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 8e6d933c6d..d227dc13fb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -2,36 +2,30 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; -import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; +import {SafeCast} from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; -import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; +import {UnstructuredStorage} from "../lib/UnstructuredStorage.sol"; -import { BaseOracle } from "./BaseOracle.sol"; -import { ValidatorsExitBus } from "./ValidatorsExitBus.sol"; +import {BaseOracle} from "./BaseOracle.sol"; +import {ValidatorsExitBus} from "./ValidatorsExitBus.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; interface IOracleReportSanityChecker { function checkExitBusOracleReport(uint256 _exitRequestsCount) external view; } - contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { using UnstructuredStorage for bytes32; using SafeCast for uint256; using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; - error AdminCannotBeZero(); error SenderNotAllowed(); error UnexpectedRequestsDataLength(); error ArgumentOutOfBounds(); - event WarnDataIncompleteProcessing( - uint256 indexed refSlot, - uint256 requestsProcessed, - uint256 requestsCount - ); + event WarnDataIncompleteProcessing(uint256 indexed refSlot, uint256 requestsProcessed, uint256 requestsCount); struct DataProcessingState { uint64 refSlot; @@ -121,13 +115,11 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// @dev Version of the oracle consensus rules. Current version expected /// by the oracle can be obtained by calling getConsensusVersion(). uint256 consensusVersion; - /// @dev Reference slot for which the report was calculated. If the slot /// contains a block, the state being reported should include all state /// changes resulting from that block. The epoch containing the slot /// should be finalized prior to calculating the report. uint256 refSlot; - /// /// Requests data /// @@ -135,11 +127,9 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// @dev Total number of validator exit requests in this report. Must not be greater /// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport. uint256 requestsCount; - /// @dev Format of the validator exit requests data. Currently, only the /// DATA_FORMAT_LIST=1 is supported. uint256 dataFormat; - /// @dev Validator exit requests data. Can differ based on the data format, /// see the constant defining a specific data format below for more info. bytes data; @@ -161,9 +151,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// provided by the hash consensus contract. /// - The provided data doesn't meet safety checks. /// - function submitReportData(ReportData calldata data, uint256 contractVersion) - external whenResumed - { + function submitReportData(ReportData calldata data, uint256 contractVersion) external whenResumed { _checkMsgSenderIsAllowedToSubmitData(); _checkContractVersion(contractVersion); bytes32 dataHash = keccak256(abi.encode(data.data, data.dataFormat)); @@ -172,7 +160,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { _checkConsensusData(data.refSlot, data.consensusVersion, reportDataHash); _startProcessing(); _handleConsensusReportData(data); - _storeOracleExitRequestHash(dataHash, data.requestsCount, contractVersion); + _storeOracleExitRequestHash(dataHash, contractVersion); } /// @notice Returns the total number of validator exit requests ever processed @@ -240,11 +228,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { ) internal override { DataProcessingState memory state = _storageDataProcessingState().value; if (state.refSlot == prevProcessingRefSlot && state.requestsProcessed < state.requestsCount) { - emit WarnDataIncompleteProcessing( - prevProcessingRefSlot, - state.requestsProcessed, - state.requestsCount - ); + emit WarnDataIncompleteProcessing(prevProcessingRefSlot, state.requestsProcessed, state.requestsCount); } } @@ -297,13 +281,8 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { return count; } - function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 requestsCount, uint256 contractVersion) - internal - { - if (requestsCount == 0) { - return; - } - _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); + function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 contractVersion) internal { + _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), uint32(_getTime()))); } /// @@ -314,10 +293,10 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { DataProcessingState value; } - function _storageDataProcessingState() internal pure returns ( - StorageDataProcessingState storage r - ) { + function _storageDataProcessingState() internal pure returns (StorageDataProcessingState storage r) { bytes32 position = DATA_PROCESSING_STATE_POSITION; - assembly { r.slot := position } + assembly { + r.slot := position + } } } diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 0bde4ffb5e..e149258d83 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -12,20 +12,17 @@ struct MockExitRequestData { contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { bytes32 _hash; - DeliveryHistory[] private _deliveryHistory; + uint256 private _deliveryTimestamp; MockExitRequestData[] private _data; function setExitRequests( bytes32 exitRequestsHash, - DeliveryHistory[] calldata deliveryHistory, + uint256 deliveryTimestamp, MockExitRequestData[] calldata data ) external { _hash = exitRequestsHash; - delete _deliveryHistory; - for (uint256 i = 0; i < deliveryHistory.length; i++) { - _deliveryHistory.push(deliveryHistory[i]); - } + _deliveryTimestamp = deliveryTimestamp; delete _data; for (uint256 i = 0; i < data.length; i++) { @@ -33,9 +30,9 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { } } - function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (DeliveryHistory[] memory) { + function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (uint256 timestamp) { require(exitRequestsHash == _hash, "Mock error, Invalid exitRequestsHash"); - return _deliveryHistory; + return _deliveryTimestamp; } function unpackExitRequest( diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 0e07d4f4c6..a69557b0c7 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -42,44 +42,17 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { return _storageDataProcessingState().value; } - function storeNewHashRequestStatus( - bytes32 exitRequestHash, - uint8 contractVersion, - uint32 deliveryHistoryLength, - uint32 lastDeliveredExitDataIndex, - uint32 timestamp - ) external { - _storeNewHashRequestStatus( - exitRequestHash, - RequestStatus(contractVersion, deliveryHistoryLength, lastDeliveredExitDataIndex, timestamp) - ); - } - - function storeDeliveryEntry( - bytes32 exitRequestsHash, - uint256 lastDeliveredExitDataIndex, - uint256 lastDeliveredExitDataTimestamp - ) external { - _storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); + function storeNewHashRequestStatus(bytes32 exitRequestHash, uint8 contractVersion, uint32 timestamp) external { + _storeNewHashRequestStatus(exitRequestHash, RequestStatus(contractVersion, timestamp)); } function setContractVersion(uint256 version) external { CONTRACT_VERSION_POSITION.setStorageUint256(version); } - function updateRequestStatus( - bytes32 exitRequestHash, - uint256 deliveryHistoryLength, - uint256 lastDeliveredExitDataIndex, - uint256 lastDeliveredExitDataTimestamp - ) external { + function updateRequestStatus(bytes32 exitRequestHash, uint256 deliveredExitDataTimestamp) external { RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestHash]; - _updateRequestStatus( - requestStatus, - deliveryHistoryLength, - lastDeliveredExitDataIndex, - lastDeliveredExitDataTimestamp - ); + _updateRequestStatus(requestStatus, deliveredExitDataTimestamp); } function getRequestStatus(bytes32 exitRequestHash) external view returns (RequestStatus memory requestStatus) { From ff2562d8386a0b475fcc170ba0fecd9dc53599e5 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 17:15:01 +0200 Subject: [PATCH 219/405] feat: remove unused code and obsolete tests for exit delay verifier --- .../0.8.25/ValidatorExitDelayVerifier.sol | 17 +- .../0.8.25/interfaces/IValidatorsExitBus.sol | 5 - .../ValidatorsExitBusOracle_Mock.sol | 63 +------ .../0.8.25/validatorExitDelayVerifier.test.ts | 176 +----------------- 4 files changed, 8 insertions(+), 253 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 7e1f1d81ea..939232900f 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -7,7 +7,7 @@ import {BeaconBlockHeader, Validator} from "./lib/BeaconTypes.sol"; import {GIndex} from "./lib/GIndex.sol"; import {SSZ} from "./lib/SSZ.sol"; import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; -import {IValidatorsExitBus, DeliveryHistory} from "./interfaces/IValidatorsExitBus.sol"; +import {IValidatorsExitBus} from "./interfaces/IValidatorsExitBus.sol"; import {IStakingRouter} from "./interfaces/IStakingRouter.sol"; struct ExitRequestData { @@ -104,9 +104,7 @@ contract ValidatorExitDelayVerifier { uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); - error KeyWasNotUnpacked(uint256 keyIndex); error NonMonotonicDeliveryHistory(uint256 index); - error EmptyDeliveryHistory(); /** * @dev The previous and current forks can be essentially the same. @@ -366,19 +364,6 @@ contract ValidatorExitDelayVerifier { return veb.getExitRequestsDeliveryHistory(exitRequestsHash); } - function _getExitRequestTimestamp( - DeliveryHistory[] memory deliveryHistory, - uint256 index - ) internal pure returns (uint256 validatorExitRequestTimestamp) { - for (uint256 i = 0; i < deliveryHistory.length; i++) { - if (deliveryHistory[i].lastDeliveredKeyIndex >= index) { - return deliveryHistory[i].timestamp; - } - } - - revert KeyWasNotUnpacked(index); - } - function _slotToTimestamp(uint64 slot) internal view returns (uint256) { return GENESIS_TIME + slot * SECONDS_PER_SLOT; } diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol index 2d526b42c9..81c264a8ee 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -4,11 +4,6 @@ // See contracts/COMPILERS.md pragma solidity 0.8.25; -struct DeliveryHistory { - // index in array of requests - uint256 lastDeliveredKeyIndex; - uint256 timestamp; -} interface IValidatorsExitBus { function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (uint256 timestamp); diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index e149258d83..3c67b17c42 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IValidatorsExitBus, DeliveryHistory} from "contracts/0.8.25/interfaces/IValidatorsExitBus.sol"; +import {IValidatorsExitBus} from "contracts/0.8.25/interfaces/IValidatorsExitBus.sol"; struct MockExitRequestData { bytes pubkey; @@ -11,7 +11,7 @@ struct MockExitRequestData { } contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { - bytes32 _hash; + bytes32 private _hash; uint256 private _deliveryTimestamp; MockExitRequestData[] private _data; @@ -46,62 +46,3 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { return (data.pubkey, data.nodeOpId, data.moduleId, data.valIndex); } } - -library ExitRequests { - uint256 internal constant PACKED_REQUEST_LENGTH = 64; - uint256 internal constant PUBLIC_KEY_LENGTH = 48; - - error ExitRequestIndexOutOfRange(uint256 exitRequestIndex); - - function unpackExitRequest( - bytes calldata exitRequests, - uint256 exitRequestIndex - ) internal pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex) { - if (exitRequestIndex >= count(exitRequests)) { - revert ExitRequestIndexOutOfRange(exitRequestIndex); - } - - uint256 itemOffset; - uint256 dataWithoutPubkey; - - assembly { - // Compute the start of this packed request (item) - itemOffset := add(exitRequests.offset, mul(PACKED_REQUEST_LENGTH, exitRequestIndex)) - - // Load the first 16 bytes which contain moduleId (24 bits), - // nodeOpId (40 bits), and valIndex (64 bits). - dataWithoutPubkey := shr(128, calldataload(itemOffset)) - } - - // dataWithoutPubkey format (128 bits total): - // MSB <-------------------- 128 bits --------------------> LSB - // | 128 bits: zeros | 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex | - - valIndex = uint64(dataWithoutPubkey); - nodeOpId = uint40(dataWithoutPubkey >> 64); - moduleId = uint24(dataWithoutPubkey >> (64 + 40)); - - // Allocate a new bytes array in memory for the pubkey - pubkey = new bytes(PUBLIC_KEY_LENGTH); - - assembly { - // Starting offset in calldata for the pubkey part - let pubkeyCalldataOffset := add(itemOffset, 16) - - // Memory location of the 'pubkey' bytes array data - let pubkeyMemPtr := add(pubkey, 32) - - // Copy the 48 bytes of the pubkey from calldata into memory - calldatacopy(pubkeyMemPtr, pubkeyCalldataOffset, PUBLIC_KEY_LENGTH) - } - - return (pubkey, nodeOpId, moduleId, valIndex); - } - - /** - * @dev Counts how many exit requests are packed in the given calldata array. - */ - function count(bytes calldata exitRequests) internal pure returns (uint256) { - return exitRequests.length / PACKED_REQUEST_LENGTH; - } -} diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 3f0e5528bf..4e6ad4fcf0 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -186,11 +186,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]; const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - await vebo.setExitRequests( - encodedExitRequestsHash, - [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - exitRequests, - ); + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { const receipt = await tx.wait(); @@ -256,11 +252,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]; const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - await vebo.setExitRequests( - encodedExitRequestsHash, - [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - exitRequests, - ); + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -415,11 +407,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]; const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - await vebo.setExitRequests( - encodedExitRequestsHash, - [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - exitRequests, - ); + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -460,11 +448,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]; const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - await vebo.setExitRequests( - encodedExitRequestsHash, - [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - exitRequests, - ); + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -516,152 +500,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); }); - it("reverts with 'EmptyDeliveryHistory' if exit request index is not in delivery history", async () => { - const exitRequests: ExitRequest[] = [ - { - moduleId: 1, - nodeOpId: 1, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - - const unpackedExitRequestIndex = 0; - - // Report not unpacked. - await vebo.setExitRequests(encodedExitRequestsHash, [], exitRequests); - expect((await vebo.getExitRequestsDeliveryHistory(encodedExitRequestsHash)).length).to.equal(0); - - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); - - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); - }); - - it("reverts with 'NonMonotonicDeliveryHistory' if delivery history is not strictly increasing.", async () => { - const exitRequests: ExitRequest[] = [ - { - moduleId: 1, - nodeOpId: 1, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - - const nonMonotonicDeliveryHistory = [ - [ - { timestamp: 2, lastDeliveredKeyIndex: 2n }, - { timestamp: 1, lastDeliveredKeyIndex: 1n }, - ], - [ - { timestamp: 1, lastDeliveredKeyIndex: 2n }, - { timestamp: 1, lastDeliveredKeyIndex: 1n }, - ], - [ - { timestamp: 1, lastDeliveredKeyIndex: 1n }, - { timestamp: 1, lastDeliveredKeyIndex: 1n }, - ], - ]; - - async function testNonMonotonicDeliveryHistory( - deliveryHistory: { timestamp: number; lastDeliveredKeyIndex: bigint }[], - ) { - await vebo.setExitRequests(encodedExitRequestsHash, deliveryHistory, exitRequests); - - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "NonMonotonicDeliveryHistory"); - - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "NonMonotonicDeliveryHistory"); - } - - for (const deliveryHistory of nonMonotonicDeliveryHistory) { - await testNonMonotonicDeliveryHistory(deliveryHistory); - } - }); - - it("reverts with 'KeyWasNotUnpacked' if exit request index is not in delivery history", async () => { - const nodeOpId = 2; - const exitRequests: ExitRequest[] = [ - { - moduleId: 1, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - { - moduleId: 2, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - { - moduleId: 3, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - - const unpackedExitRequestIndex = 2; - - // Report not fully unpacked. - await vebo.setExitRequests(encodedExitRequestsHash, [{ timestamp: 0n, lastDeliveredKeyIndex: 1n }], exitRequests); - expect((await vebo.getExitRequestsDeliveryHistory(encodedExitRequestsHash)).length).to.equal(1); - - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); - - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "KeyWasNotUnpacked"); - }); - it("reverts if the oldBlock proof is corrupted", async () => { const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -700,11 +538,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]; const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - await vebo.setExitRequests( - encodedExitRequestsHash, - [{ timestamp: veboExitRequestTimestamp, lastDeliveredKeyIndex: 1n }], - exitRequests, - ); + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); From a29a27b9fbd6cc4d7fbcfd343f9591beff4cdcf1 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 19:24:37 +0400 Subject: [PATCH 220/405] fix: rename error & try catch for notification --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 7 ++-- contracts/0.8.9/lib/ExitLimitUtils.sol | 2 -- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 12 +++---- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 7 +++- .../contracts/StakingRouter_MockForTWG.sol | 21 +++++++++++ ...ggerableWithdrawalsGateway__MockForVEB.sol | 4 +-- ...-bus-oracle.submitExitRequestsData.test.ts | 2 +- ...r-exit-bus-oracle.submitReportData.test.ts | 2 +- ...dator-exit-bus-oracle.triggerExits.test.ts | 2 +- ...awalGateway.triggerFullWithdrawals.test.ts | 36 +++++++++++++++++++ 10 files changed, 77 insertions(+), 18 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 1a7431d83e..edc1748a52 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -31,7 +31,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; - /// @dev Errors /** * @notice Thrown when an invalid zero value is passed * @param name Name of the argument that was zero @@ -136,7 +135,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { function triggerFullWithdrawals( ValidatorData[] calldata validatorsData, address refundRecipient, - uint8 exitType + uint256 exitType ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { if (msg.value == 0) revert ZeroArgument("msg.value"); uint256 requestsCount = validatorsData.length; @@ -218,7 +217,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { function _notifyStakingModules( ValidatorData[] calldata validatorsData, uint256 withdrawalRequestPaidFee, - uint8 exitType + uint256 exitType ) internal { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); ValidatorData calldata data; @@ -233,7 +232,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { withdrawalRequestPaidFee, exitType ) - {} catch { + {} catch { // (bytes memory lowLevelRevertData) emit StakingModuleExitNotificationFailed(data.stakingModuleId, data.nodeOperatorId, data.pubkey); } } diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index 6ac01f201c..dd42503617 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -77,8 +77,6 @@ library ExitLimitUtils { uint256 frameDurationInSec, uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { - // TODO: do we allow maxExitRequests be equal to zero? - // require(maxExitRequests != 0, "ZERO_MAX_LIMIT");; require(maxExitRequestsLimit <= type(uint32).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); require(frameDurationInSec <= type(uint32).max, "TOO_LARGE_FRAME_DURATION"); require(exitsPerFrame <= maxExitRequestsLimit, "TOO_LARGE_EXITS_PER_FRAME"); diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index d2138ebaaa..28f69894b6 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -19,7 +19,7 @@ interface ITriggerableWithdrawalsGateway { function triggerFullWithdrawals( ValidatorData[] calldata triggerableExitData, address refundRecipient, - uint8 exitType + uint256 exitType ) external payable; } @@ -33,7 +33,6 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; - /// @dev Errors /** * @notice Thrown when an invalid zero value is passed * @param name Name of the argument that was zero @@ -54,7 +53,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned /** * @notice Thrown when module id equal to zero */ - error InvalidRequestsData(); + error InvalidModuleId(); /** * @notice Thrown when data submitted for exit requests was not sorted in ascending order or contains duplicates @@ -335,6 +334,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned address refundRecipient ) external payable whenResumed preservesEthBalance { if (msg.value == 0) revert ZeroArgument("msg.value"); + if (exitDataIndexes.length == 0) revert ZeroArgument("exitDataIndexes"); // If the refund recipient is not set, use the sender as the refund recipient if (refundRecipient == address(0)) { @@ -373,7 +373,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned ValidatorData memory validatorData = _getValidatorData(exitsData.data, exitDataIndexes[i]); - if (validatorData.moduleId == 0) revert InvalidRequestsData(); + if (validatorData.moduleId == 0) revert InvalidModuleId(); triggerableExitData[i] = ITriggerableWithdrawalsGateway.ValidatorData( validatorData.moduleId, @@ -384,7 +384,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned ITriggerableWithdrawalsGateway(LOCATOR.triggerableWithdrawalsGateway()).triggerFullWithdrawals{ value: msg.value - }(triggerableExitData, refundRecipient, uint8(EXIT_TYPE)); + }(triggerableExitData, refundRecipient, EXIT_TYPE); } /** @@ -743,7 +743,7 @@ contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned moduleId = uint24(dataWithoutPubkey >> (64 + 40)); if (moduleId == 0) { - revert InvalidRequestsData(); + revert InvalidModuleId(); } // dataWithoutPubkey diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 8e6d933c6d..90edf9925a 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -303,7 +303,12 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (requestsCount == 0) { return; } - _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), 1, uint32(requestsCount - 1), uint32(_getTime()))); + _storeNewHashRequestStatus(exitRequestsHash, RequestStatus({ + contractVersion: uint32(contractVersion), + deliveryHistoryLength: 1, + lastDeliveredExitDataIndex: uint32(requestsCount - 1), + lastDeliveredExitDataTimestamp: uint32(_getTime()) + })); } /// diff --git a/test/0.8.9/contracts/StakingRouter_MockForTWG.sol b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol index 734ba2db36..6ca91bfafa 100644 --- a/test/0.8.9/contracts/StakingRouter_MockForTWG.sol +++ b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol @@ -1,6 +1,8 @@ pragma solidity 0.8.9; contract StakingRouter__MockForTWG { + error CustomRevertError(uint256 id, string reason); + event Mock__onValidatorExitTriggered( uint256 _stakingModuleId, uint256 _nodeOperatorId, @@ -9,6 +11,17 @@ contract StakingRouter__MockForTWG { uint256 exitType ); + bool private shouldRevert; + bool private shouldRevertWithCustomError; + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } + + function setShouldRevertWithCustomError(bool _shouldRevert) external { + shouldRevertWithCustomError = _shouldRevert; + } + function onValidatorExitTriggered( uint256 _stakingModuleId, uint256 _nodeOperatorId, @@ -16,6 +29,14 @@ contract StakingRouter__MockForTWG { uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external { + if (shouldRevert) { + revert("some reason"); + } + + if (shouldRevertWithCustomError) { + revert CustomRevertError(42, "custom fail"); + } + emit Mock__onValidatorExitTriggered( _stakingModuleId, _nodeOperatorId, diff --git a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol index e88edd162d..aac5007d8f 100644 --- a/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol +++ b/test/0.8.9/contracts/TriggerableWithdrawalsGateway__MockForVEB.sol @@ -1,7 +1,7 @@ pragma solidity 0.8.9; contract TriggerableWithdrawalsGateway__MockForVEB { - event Mock__triggerFullWithdrawalsTriggered(uint256 exitsCount, address refundRecipient, uint8 exitType); + event Mock__triggerFullWithdrawalsTriggered(uint256 exitsCount, address refundRecipient, uint256 exitType); struct ValidatorData { uint256 stakingModuleId; @@ -12,7 +12,7 @@ contract TriggerableWithdrawalsGateway__MockForVEB { function triggerFullWithdrawals( ValidatorData[] calldata triggerableExitData, address refundRecipient, - uint8 exitType + uint256 exitType ) external payable { emit Mock__triggerFullWithdrawalsTriggered(triggerableExitData.length, refundRecipient, exitType); } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index d51ec8fc59..022852d699 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -280,7 +280,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await expect(oracle.submitExitRequestsData(exitRequestData)).to.be.revertedWithCustomError( oracle, - "InvalidRequestsData", + "InvalidModuleId", ); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 68f4c28fe8..770bff2bd0 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -294,7 +294,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)).to.be.revertedWithCustomError( oracle, - "InvalidRequestsData", + "InvalidModuleId", ); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 556e8d4696..cd15f71e2a 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -393,7 +393,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { oracle.triggerExits(request, [0, 1, 2], ZERO_ADDRESS, { value: 4, }), - ).to.be.revertedWithCustomError(oracle, "InvalidRequestsData"); + ).to.be.revertedWithCustomError(oracle, "InvalidModuleId"); }); }); diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 6657dfa930..866f771168 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -356,4 +356,40 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(0, 1, 48), ).to.be.revertedWith("TOO_LARGE_EXITS_PER_FRAME"); }); + + it("should emit StakingModuleExitNotificationFailed if onValidatorExitTriggered reverts", async () => { + const requests = createValidatorDataList(exitRequests); + + await stakingRouter.setShouldRevert(true); + + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(triggerableWithdrawalsGateway, "StakingModuleExitNotificationFailed") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey); + } + + await stakingRouter.connect(admin).setShouldRevert(false); + }); + + it("should emit StakingModuleExitNotificationFailed with custom error revert reason", async () => { + const requests = createValidatorDataList(exitRequests); + + await stakingRouter.setShouldRevertWithCustomError(true); + + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); + + for (const request of exitRequests) { + await expect(tx) + .to.emit(triggerableWithdrawalsGateway, "StakingModuleExitNotificationFailed") + .withArgs(request.moduleId, request.nodeOpId, request.valPubkey); + } + + await stakingRouter.setShouldRevertWithCustomError(false); + }); }); From 1ca0d16596bfdff1c9cfb73bd8ed45ca73500c17 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 17:49:59 +0200 Subject: [PATCH 221/405] feat: fix integration test for exit delay verifier --- test/integration/report-validator-exit-delay.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index c24488ae87..72c6b98fdc 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -85,8 +85,8 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); - const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory[0].timestamp; + const deliveryTimestamp = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -152,8 +152,8 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); - const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory[0].timestamp; + const deliveryTimestamp = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -375,8 +375,8 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryHistory = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); - const eligibleToExitInSec = proofSlotTimestamp - deliveryHistory[0].timestamp; + const deliveryTimestamp = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); From 2a09d8e6de30b634a7a9abfbcf90baf268cdda4f Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 18:09:24 +0200 Subject: [PATCH 222/405] feat: restore empty delivery history check in exit delay verifier --- .../0.8.25/ValidatorExitDelayVerifier.sol | 16 +++++--- .../0.8.25/validatorExitDelayVerifier.test.ts | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 939232900f..815b4f3833 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -104,7 +104,7 @@ contract ValidatorExitDelayVerifier { uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); - error NonMonotonicDeliveryHistory(uint256 index); + error EmptyDeliveryHistory(); /** * @dev The previous and current forks can be essentially the same. @@ -172,7 +172,7 @@ contract ValidatorExitDelayVerifier { IValidatorsExitBus veb = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - uint256 deliveredTimestamp = _getExitRequestDeliveryHistory(veb, exitRequests); + uint256 deliveredTimestamp = _getExitRequestDeliveryTimestamp(veb, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(beaconBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { @@ -219,7 +219,7 @@ contract ValidatorExitDelayVerifier { IValidatorsExitBus veb = IValidatorsExitBus(LOCATOR.validatorsExitBusOracle()); IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - uint256 deliveredTimestamp = _getExitRequestDeliveryHistory(veb, exitRequests); + uint256 deliveredTimestamp = _getExitRequestDeliveryTimestamp(veb, exitRequests); uint256 proofSlotTimestamp = _slotToTimestamp(oldBlock.header.slot); for (uint256 i = 0; i < validatorWitnesses.length; i++) { @@ -356,12 +356,16 @@ contract ValidatorExitDelayVerifier { return stateSlot < PIVOT_SLOT ? GI_HISTORICAL_SUMMARIES_PREV : GI_HISTORICAL_SUMMARIES_CURR; } - function _getExitRequestDeliveryHistory( + function _getExitRequestDeliveryTimestamp( IValidatorsExitBus veb, ExitRequestData calldata exitRequests - ) internal view returns (uint256 timestamp) { + ) internal view returns (uint256 deliveryTimestamp) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); - return veb.getExitRequestsDeliveryHistory(exitRequestsHash); + deliveryTimestamp = veb.getExitRequestsDeliveryHistory(exitRequestsHash); + + if (deliveryTimestamp == 0) { + revert EmptyDeliveryHistory(); + } } function _slotToTimestamp(uint64 slot) internal view returns (uint256) { diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 4e6ad4fcf0..3928b58ac1 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -500,6 +500,44 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); }); + it("reverts with 'EmptyDeliveryHistory' if exit request index is not in delivery history", async () => { + const exitRequests: ExitRequest[] = [ + { + moduleId: 1, + nodeOpId: 1, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + const unpackedExitRequestIndex = 0; + + // Report not unpacked, deliveryTimestamp == 0 + await vebo.setExitRequests(encodedExitRequestsHash, 0, exitRequests); + expect(await vebo.getExitRequestsDeliveryHistory(encodedExitRequestsHash)).to.equal(0); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); + }); + it("reverts if the oldBlock proof is corrupted", async () => { const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); From c3fcb86af868fdb5d6a83506d80da70ee25bed0f Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 22:52:33 +0400 Subject: [PATCH 223/405] fix: veb limits consume & tests for atomic history --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 68 +++-- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 11 +- .../contracts/ValidatorsExitBus__Harness.sol | 2 +- ...-bus-oracle.submitExitRequestsData.test.ts | 106 +++---- ...dator-exit-bus-oracle.triggerExits.test.ts | 44 +-- .../oracle/validator-exit-bus.helpers.test.ts | 194 ++----------- .../validators-exit-bus-multiple-delivery.ts | 265 ------------------ 7 files changed, 115 insertions(+), 575 deletions(-) delete mode 100644 test/integration/validators-exit-bus-multiple-delivery.ts diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 7921361779..486de81a5f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -78,8 +78,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /** * @notice Thrown when index of request in submitted data for triggerable withdrawal is out of range + * @param exitDataIndex Index of request + * @param requestsCount Amount of requests that were sent for processing */ - error ExitDataIndexOutOfRange(uint256 exitDataIndex, uint256 totalItemsCount); + error ExitDataIndexOutOfRange(uint256 exitDataIndex, uint256 requestsCount); /** * @notice Thrown when array of indexes of requests in submitted data for triggerable withdrawal is not is not strictly increasing array @@ -100,8 +102,9 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /** * @notice Thrown when exit requests in report exceed the maximum allowed number of requests per batch. + * @param requestsCount Amount of requests that were sent for processing */ - error ToManyExitRequestsInReport(uint256 totalItemsCount, uint256 maxRequestsPerBatch); + error ToManyExitRequestsInReport(uint256 requestsCount, uint256 maxRequestsPerBatch); /// @dev Events @@ -151,7 +154,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } // RequestStatus stores timestamp of delivery, and contract version. - // Mappings use exitRequestsHash as key struct RequestStatus { uint32 contractVersion; uint32 deliveredExitDataTimestamp; @@ -229,7 +231,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V */ function submitExitRequestsHash(bytes32 exitRequestsHash) external whenResumed onlyRole(SUBMIT_REPORT_HASH_ROLE) { uint256 contractVersion = getContractVersion(); - _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), 0)); + _storeNewHashRequestStatus(exitRequestsHash, uint32(contractVersion), 0); } /** @@ -257,16 +259,16 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V _checkExitRequestData(request.data, request.dataFormat); _checkContractVersion(requestStatus.contractVersion); - uint256 totalItemsCount = request.data.length / PACKED_REQUEST_LENGTH; + uint256 requestsCount = request.data.length / PACKED_REQUEST_LENGTH; uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); - if (totalItemsCount > maxRequestsPerBatch) { - revert ToManyExitRequestsInReport(totalItemsCount, maxRequestsPerBatch); + if (requestsCount > maxRequestsPerBatch) { + revert ToManyExitRequestsInReport(requestsCount, maxRequestsPerBatch); } - uint256 requestsToDeliver = _consumeLimit(totalItemsCount, _applyDeliverLimit); + _consumeLimit(requestsCount); - _processExitRequestsList(request.data, 0, requestsToDeliver); + _processExitRequestsList(request.data, 0, requestsCount); _updateRequestStatus(requestStatus, _getTimestamp()); @@ -401,22 +403,23 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } /** - * @notice Returns delivery history and current status for specific exitRequestsData + * @notice Timestamp * * @param exitRequestsHash - The exit requests hash. * * @dev Reverts if: * - exitRequestsHash was not submited + * - Request was not delivered */ function getExitRequestsDeliveryHistory( + // TODO: rename bytes32 exitRequestsHash ) external view returns (uint256 deliveryDateTimestamp) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; - if (storedRequest.contractVersion == 0) { - revert ExitHashNotSubmitted(); - } + _checkExitSubmitted(storedRequest); + _checkDelivered(storedRequest); return storedRequest.deliveredExitDataTimestamp; } @@ -544,45 +547,44 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } - function _consumeLimit( - uint256 requestsCount, - function(uint256, uint256) internal pure returns (uint256) applyLimit - ) internal returns (uint256 requestsLimitedCount) { + function _consumeLimit(uint256 requestsCount) internal returns (uint256 requestsLimitedCount) { ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (!exitRequestLimitData.isExitLimitSet()) { return requestsCount; } uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); - requestsLimitedCount = applyLimit(limit, requestsCount); + + if (requestsCount > limit) { + revert ExitRequestsLimitExceeded(requestsCount, limit); + } EXIT_REQUEST_LIMIT_POSITION.setStorageExitRequestLimit( - exitRequestLimitData.updatePrevExitLimit(limit - requestsLimitedCount, _getTimestamp()) + exitRequestLimitData.updatePrevExitLimit(limit - requestsCount, _getTimestamp()) ); } - function _applyDeliverLimit(uint256 limit, uint256 count) internal pure returns (uint256 limitedCount) { - if (limit == 0) { - revert ExitRequestsLimitExceeded(count, 0); - } - return min(limit, count); - } - - function _storeNewHashRequestStatus(bytes32 exitRequestsHash, RequestStatus memory requestStatus) internal { + function _storeNewHashRequestStatus( + bytes32 exitRequestsHash, + uint32 contractVersion, + uint32 deliveredExitDataTimestamp + ) internal { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); - RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; - if (storedRequest.contractVersion != 0) { + if (requestStatusMap[exitRequestsHash].contractVersion != 0) { revert ExitHashAlreadySubmitted(); } - requestStatusMap[exitRequestsHash] = requestStatus; + requestStatusMap[exitRequestsHash] = RequestStatus({ + contractVersion: contractVersion, + deliveredExitDataTimestamp: deliveredExitDataTimestamp + }); emit RequestsHashSubmitted(exitRequestsHash); } function _updateRequestStatus(RequestStatus storage requestStatus, uint256 deliveredExitDataTimestamp) internal { - require(deliveredExitDataTimestamp <= type(uint32).max, "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); + require(deliveredExitDataTimestamp <= type(uint32).max, "DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); requestStatus.deliveredExitDataTimestamp = uint32(deliveredExitDataTimestamp); } @@ -687,10 +689,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } } - function min(uint256 a, uint256 b) internal pure returns (uint256) { - return a < b ? a : b; - } - /// Storage helpers function _storageRequestStatus() internal pure returns (mapping(bytes32 => RequestStatus) storage r) { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index d227dc13fb..f0a98325a0 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -255,7 +255,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()).checkExitBusOracleReport(data.requestsCount); // Check VEB common limit - _consumeLimit(data.requestsCount, _applyOracleLimit); + _consumeLimit(data.requestsCount); _processExitRequestsList(data.data, 0, data.requestsCount); _storageDataProcessingState().value = DataProcessingState({ @@ -274,15 +274,8 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { ); } - function _applyOracleLimit(uint256 limit, uint256 count) internal pure returns (uint256) { - if (limit < count) { - revert ExitRequestsLimitExceeded(count, limit); - } - return count; - } - function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 contractVersion) internal { - _storeNewHashRequestStatus(exitRequestsHash, RequestStatus(uint32(contractVersion), uint32(_getTime()))); + _storeNewHashRequestStatus(exitRequestsHash, uint32(contractVersion), uint32(_getTime())); } /// diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index a69557b0c7..9c14a7c1f3 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -43,7 +43,7 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { } function storeNewHashRequestStatus(bytes32 exitRequestHash, uint8 contractVersion, uint32 timestamp) external { - _storeNewHashRequestStatus(exitRequestHash, RequestStatus(contractVersion, timestamp)); + _storeNewHashRequestStatus(exitRequestHash, contractVersion, timestamp); } function setContractVersion(uint256 version) external { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index fed2a81d04..e25915f4b7 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -306,7 +306,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const FRAME_DURATION = 48; // Data for case when limit is not enough to process entire request - const VALIDATORS_DELIVERED_BY_PARTS: ExitRequest[] = [ + const VALIDATORS: ExitRequest[] = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, @@ -314,20 +314,12 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { { moduleId: 3, nodeOpId: 0, valIndex: 3, valPubkey: PUBKEYS[4] }, ]; - const REQUEST_DELIVERED_BY_PARTS = { + const REQUEST = { dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(VALIDATORS_DELIVERED_BY_PARTS), + data: encodeExitRequestsDataList(VALIDATORS), }; - const HASH_REQUEST_DELIVERED_BY_PARTS = hashExitRequest(REQUEST_DELIVERED_BY_PARTS); - - it("Should not allow to set limit without role", async () => { - const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); - - await expect( - oracle.connect(stranger).setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION), - ).to.be.revertedWithOZAccessControlError(await stranger.getAddress(), reportLimitRole); - }); + const HASH_REQUEST = hashExitRequest(REQUEST); it("Should not allow to set limit without role", async () => { const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); @@ -343,7 +335,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); - it("Should deliver request fully as it is below limit", async () => { + it("Should deliver request as it is below limit", async () => { const exitLimitTx = await oracle .connect(authorizedEntity) .setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION); @@ -373,28 +365,14 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } - await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(exitRequestHash, 0, 1); - }); - - it("Should deliver part of request equal to remaining limit", async () => { - await oracle.connect(authorizedEntity).submitExitRequestsHash(HASH_REQUEST_DELIVERED_BY_PARTS); - const emitTx = await oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS); - const timestamp = await oracle.getTime(); - - for (let i = 0; i < 3; i++) { - const request = VALIDATORS_DELIVERED_BY_PARTS[i]; - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); - } - - await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(HASH_REQUEST_DELIVERED_BY_PARTS, 0, 2); + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(exitRequestHash); }); - it("Should revert when limit exceeded for the frame", async () => { - await expect(oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS)) + it("Should not allow to deliver if limit doesnt cover full request", async () => { + await oracle.connect(authorizedEntity).submitExitRequestsHash(HASH_REQUEST); + await expect(oracle.submitExitRequestsData(REQUEST)) .to.be.revertedWithCustomError(oracle, "ExitRequestsLimitExceeded") - .withArgs(2, 0); + .withArgs(5, 3); }); it("Current limit should be equal to 0", async () => { @@ -403,8 +381,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); expect(data.frameDurationInSec).to.equal(FRAME_DURATION); - expect(data.prevExitRequestsLimit).to.equal(0); - expect(data.currentExitRequestsLimit).to.equal(0); + expect(data.prevExitRequestsLimit).to.equal(3); + expect(data.currentExitRequestsLimit).to.equal(3); }); it("Should current limit should be increased on 2 if 2*48 seconds passed", async () => { @@ -414,26 +392,26 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { expect(data.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); expect(data.exitsPerFrame).to.equal(EXITS_PER_FRAME); expect(data.frameDurationInSec).to.equal(FRAME_DURATION); - expect(data.prevExitRequestsLimit).to.equal(0); - expect(data.currentExitRequestsLimit).to.equal(2); + expect(data.prevExitRequestsLimit).to.equal(3); + expect(data.currentExitRequestsLimit).to.equal(5); }); - it("Should process remaining requests after 2 frames passes", async () => { - const emitTx = await oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS); + it("Should process requests after 2 frames passes", async () => { + const emitTx = await oracle.submitExitRequestsData(REQUEST); const timestamp = await oracle.getTime(); - for (let i = 3; i < 5; i++) { - const request = VALIDATORS_DELIVERED_BY_PARTS[i]; + for (let i = 0; i < 5; i++) { + const request = VALIDATORS[i]; await expect(emitTx) .to.emit(oracle, "ValidatorExitRequest") .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } - await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(HASH_REQUEST_DELIVERED_BY_PARTS, 3, 4); + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(HASH_REQUEST); }); - it("Should revert when no new requests to deliver", async () => { - await expect(oracle.submitExitRequestsData(REQUEST_DELIVERED_BY_PARTS)).to.be.revertedWithCustomError( + it("Should revert when request already delivered", async () => { + await expect(oracle.submitExitRequestsData(REQUEST)).to.be.revertedWithCustomError( oracle, "RequestsAlreadyDelivered", ); @@ -453,12 +431,18 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); await oracle.grantRole(role, authorizedEntity); - await expect( - oracle.connect(authorizedEntity).setMaxRequestsPerBatch(0), - ).to.be.revertedWith('MAX_BATCH_SIZE_ZERO'); + await expect(oracle.connect(authorizedEntity).setMaxRequestsPerBatch(0)).to.be.revertedWith( + "MAX_BATCH_SIZE_ZERO", + ); + }); + + it("Should not allow to ", async () => { + // if (totalItemsCount > maxRequestsPerBatch) { + // revert ToManyExitRequestsInReport(totalItemsCount, maxRequestsPerBatch); + // } }); - it("Should limit request by MAX_VALIDATORS_PER_BATCH if it is smaller than available vebo limit", async () => { + it("Should not allow to process request larger than MAX_VALIDATORS_PER_BATCH", async () => { await consensus.advanceTimeBy(MAX_EXIT_REQUESTS_LIMIT * 4 * 12); const data = await oracle.getExitRequestLimitFullInfo(); expect(data.currentExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); @@ -485,29 +469,9 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHashRandom); - const tx = oracle.submitExitRequestsData(exitRequestRandom); - const timestamp = await oracle.getTime(); - - for (let i = 0; i < maxRequestsPerBatch; i++) { - const request = exitRequestsRandom[i]; - await expect(tx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); - } - - await expect(tx) - .to.emit(oracle, "ExitDataProcessing") - .withArgs(exitRequestHashRandom, 0, maxRequestsPerBatch - 1); - - const history = await oracle.getExitRequestsDeliveryHistory(exitRequestHashRandom); - - expect(history.length).to.be.equal(1); - expect(history[0].lastDeliveredExitDataIndex).to.be.equal(maxRequestsPerBatch - 1); - - const data2 = await oracle.getExitRequestLimitFullInfo(); - - expect(data2.maxExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); - expect(data2.currentExitRequestsLimit).to.equal(1); + await expect(oracle.submitExitRequestsData(exitRequestRandom)) + .to.be.revertedWithCustomError(oracle, "ToManyExitRequestsInReport") + .withArgs(5, 4); }); it("Should set maxExitRequestsLimit equal to 0 and return as currentExitRequestsLimit type(uint256).max", async () => { @@ -549,7 +513,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { .withArgs(request.moduleId, request.nodeOpId, request.valIndex, request.valPubkey, timestamp); } - await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(exitRequestRandomHash, 0, 1); + await expect(emitTx).to.emit(oracle, "ExitDataProcessing").withArgs(exitRequestRandomHash); const data = await oracle.getExitRequestLimitFullInfo(); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 556e8d4696..a507516089 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -276,7 +276,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { // the only difference in this checks, is that it is possible to get DeliveryWasNotStarted error because of partial delivery describe("Submit via trustfull method", () => { - const MAX_EXIT_REQUESTS_LIMIT = 2; + const MAX_EXIT_REQUESTS_LIMIT = 3; const EXITS_PER_FRAME = 1; const FRAME_DURATION = 48; @@ -299,6 +299,17 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await deploy(); }); + it("should revert if request was not submitted", async () => { + await expect( + oracle.triggerExits( + { data: exitRequest.data, dataFormat: exitRequest.dataFormat }, + [0, 1, 2, 3], + ZERO_ADDRESS, + { value: 4 }, + ), + ).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); + }); + it("Should store exit hash for authorized entity", async () => { const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); @@ -317,10 +328,10 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { ZERO_ADDRESS, { value: 4 }, ), - ).to.be.revertedWithCustomError(oracle, "DeliveryWasNotStarted"); + ).to.be.revertedWithCustomError(oracle, "RequestsNotDelivered"); }); - it("Should deliver part of requests", async () => { + it("Should deliver request", async () => { // set limit const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await oracle.grantRole(reportLimitRole, authorizedEntity); @@ -351,16 +362,16 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { exitRequests[1].valPubkey, timestamp, ); - }); - it("should revert with error if requested index was not delivered yet", async () => { - await expect( - oracle.triggerExits({ data: exitRequest.data, dataFormat: exitRequest.dataFormat }, [0, 1, 2], ZERO_ADDRESS, { - value: 4, - }), - ) - .to.be.revertedWithCustomError(oracle, "ExitDataWasNotDelivered") - .withArgs(2, 1); + await expect(emitTx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + exitRequests[2].moduleId, + exitRequests[2].nodeOpId, + exitRequests[2].valIndex, + exitRequests[2].valPubkey, + timestamp, + ); }); it("some time passes", async () => { @@ -381,13 +392,8 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { const requestHash: string = hashExitRequest(request); - await oracle.storeNewHashRequestStatus( - requestHash, - 2, - 2, // deliveryHistoryLength = 2 - 1, - 123456, - ); + // will store request data to mock exit delivery with wrong module id + await oracle.storeNewHashRequestStatus(requestHash, 2, 123456); await expect( oracle.triggerExits(request, [0, 1, 2], ZERO_ADDRESS, { diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index df0c10d363..4925132b37 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -146,78 +146,31 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { ); }); - it("Returns empty history if deliveryHistoryLength is equal to 0", async () => { - const MAX_UINT32 = 2 ** 32 - 1; + it("reverts if request was not delivered", async () => { const exitRequestsHash = keccak256("0x1111"); - const deliveryHistoryLength = 0; const contractVersion = 42; - const lastDeliveredExitDataIndex = MAX_UINT32; - const lastDeliveredExitDataTimestamp = MAX_UINT32; + const timestamp = 0; // Call the helper to store the hash - await oracle.storeNewHashRequestStatus( - exitRequestsHash, - contractVersion, - deliveryHistoryLength, - lastDeliveredExitDataIndex, - lastDeliveredExitDataTimestamp, - ); + await oracle.storeNewHashRequestStatus(exitRequestsHash, contractVersion, timestamp); - const returnedHistory = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); - - expect(returnedHistory.length).to.equal(0); + await expect(oracle.getExitRequestsDeliveryHistory(exitRequestsHash)).to.be.revertedWithCustomError( + oracle, + "RequestsNotDelivered", + ); }); - it("Returns array with single record if deliveryHistoryLength is equal to 1", async () => { + it("returns timestamp if request was delivered", async () => { const exitRequestsHash = keccak256("0x2222"); - const deliveryHistoryLength = 1; - const timestamp = await oracle.getTime(); const contractVersion = 42; - const lastDeliveredExitDataIndex = 1; + const timestamp = 1; // Call the helper to store the hash - await oracle.storeNewHashRequestStatus( - exitRequestsHash, - contractVersion, - deliveryHistoryLength, - lastDeliveredExitDataIndex, - timestamp, - ); + await oracle.storeNewHashRequestStatus(exitRequestsHash, contractVersion, timestamp); - const returnedHistory = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + const deliveredExitDataTimestamp = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(returnedHistory.length).to.equal(1); - const [firstDelivery] = returnedHistory; - expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); - }); - - it("Returns array with multiple records if deliveryHistoryLength is equal to ", async () => { - const exitRequestsHash = keccak256("0x3333"); - const deliveryHistoryLength = 2; - const timestamp = await oracle.getTime(); - const contractVersion = 42; - // Call the helper to store the hash - await oracle.storeNewHashRequestStatus( - exitRequestsHash, - contractVersion, - deliveryHistoryLength, - 1, - timestamp + 1n, - ); - - await oracle.storeDeliveryEntry(exitRequestsHash, 0, timestamp); - - await oracle.storeDeliveryEntry(exitRequestsHash, 1, timestamp + 1n); - - const returnedHistory = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); - - expect(returnedHistory.length).to.equal(2); - const [firstDelivery, secondDelivery] = returnedHistory; - expect(firstDelivery.lastDeliveredExitDataIndex).to.equal(0); - expect(firstDelivery.timestamp).to.equal(timestamp); - - expect(secondDelivery.lastDeliveredExitDataIndex).to.equal(1); - expect(secondDelivery.timestamp).to.equal(timestamp + 1n); + expect(deliveredExitDataTimestamp).to.equal(timestamp); }); }); @@ -233,131 +186,22 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { it("updates fields correctly when valid values provided", async () => { const hash = keccak256("0xaaaa"); const contractVersion = 42; - const deliveryHistoryLength = 0; - const lastDeliveredExitDataIndex = 0; const timestamp = 0; - await oracle.storeNewHashRequestStatus( - hash, - contractVersion, - deliveryHistoryLength, - lastDeliveredExitDataIndex, - timestamp, - ); + await oracle.storeNewHashRequestStatus(hash, contractVersion, timestamp); - const newDeliveryHistoryLength = 10; - const newLastDeliveredExitDataIndex = 100; - const newLastDeliveredExitDataTimestamp = 12345; + const newTimestamp = 12345; - await oracle.updateRequestStatus( - hash, - newDeliveryHistoryLength, - newLastDeliveredExitDataIndex, - newLastDeliveredExitDataTimestamp, - ); - - await expect( - oracle.updateRequestStatus( - hash, - newDeliveryHistoryLength, - newLastDeliveredExitDataIndex, - newLastDeliveredExitDataTimestamp, - ), - ).to.not.be.reverted; + await expect(oracle.updateRequestStatus(hash, newTimestamp)).to.not.be.reverted; const requestStatus = await oracle.getRequestStatus(hash); - expect(requestStatus.deliveryHistoryLength).to.equal(newDeliveryHistoryLength); - expect(requestStatus.lastDeliveredExitDataIndex).to.equal(newLastDeliveredExitDataIndex); - expect(requestStatus.lastDeliveredExitDataTimestamp).to.equal(newLastDeliveredExitDataTimestamp); + expect(requestStatus.deliveredExitDataTimestamp).to.equal(newTimestamp); }); - it("reverts if deliveryHistoryLength exceeds uint32 max", async () => { - const hash = keccak256("0xbbbb"); - await expect(oracle.updateRequestStatus(hash, 2n ** 32n, 0, 0)).to.be.revertedWith( - "DELIVERY_HISTORY_LENGTH_OVERFLOW", - ); - }); - - it("reverts if lastDeliveredExitDataIndex exceeds uint32 max", async () => { - const hash = keccak256("0xcccc"); - await expect(oracle.updateRequestStatus(hash, 0, 2n ** 32n, 0)).to.be.revertedWith( - "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW", - ); - }); - - it("reverts if lastDeliveredExitDataTimestamp exceeds uint32 max", async () => { + it("reverts if deliveredExitDataTimestamp exceeds uint32 max", async () => { const hash = keccak256("0xdddd"); - await expect(oracle.updateRequestStatus(hash, 0, 0, 2n ** 32n)).to.be.revertedWith( - "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW", - ); - }); - }); - - context("storeDeliveryEntry", () => { - let originalState: string; - - before(async () => { - originalState = await Snapshot.take(); - }); - - after(async () => await Snapshot.restore(originalState)); - - it("adds a delivery entry to an empty history", async () => { - const exitRequestsHash = keccak256("0x1111"); - const lastDeliveredExitDataIndex = 0; - const lastDeliveredExitDataTimestamp = 123456; - - await oracle.storeNewHashRequestStatus( - exitRequestsHash, - 1, - 1, - lastDeliveredExitDataIndex, - lastDeliveredExitDataTimestamp, - ); - await oracle.storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); - - const history = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(history.length).to.equal(1); - expect(history[0].lastDeliveredExitDataIndex).to.equal(lastDeliveredExitDataIndex); - expect(history[0].timestamp).to.equal(lastDeliveredExitDataTimestamp); - }); - - it("appends multiple entries for the same hash", async () => { - const exitRequestsHash = keccak256("0x2222"); - - const lastDeliveredExitDataIndex = 1; - const lastDeliveredExitDataTimestamp = 12345; - const historyLength = 2; - - await oracle.storeNewHashRequestStatus( - exitRequestsHash, - 1, - historyLength, - lastDeliveredExitDataIndex, - lastDeliveredExitDataTimestamp, - ); - - await oracle.storeDeliveryEntry(exitRequestsHash, 0, lastDeliveredExitDataTimestamp - 1); - await oracle.storeDeliveryEntry(exitRequestsHash, lastDeliveredExitDataIndex, lastDeliveredExitDataTimestamp); - - const history = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(history.length).to.equal(2); - - expect(history[0].lastDeliveredExitDataIndex).to.equal(0); - expect(history[0].timestamp).to.equal(lastDeliveredExitDataTimestamp - 1); - }); - - it("reverts if lastDeliveredExitDataIndex exceeds uint32 max", async () => { - const exitRequestsHash = keccak256("0x3333"); - await expect(oracle.storeDeliveryEntry(exitRequestsHash, 2n ** 32n, 0)).to.be.revertedWith( - "LAST_DELIVERED_EXIT_DATA_INDEX_OVERFLOW", - ); - }); - - it("reverts if timestamp exceeds uint32 max", async () => { - const exitRequestsHash = keccak256("0x4444"); - await expect(oracle.storeDeliveryEntry(exitRequestsHash, 0, 2n ** 32n)).to.be.revertedWith( - "LAST_DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW", + await expect(oracle.updateRequestStatus(hash, 2n ** 32n)).to.be.revertedWith( + "DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW", ); }); }); diff --git a/test/integration/validators-exit-bus-multiple-delivery.ts b/test/integration/validators-exit-bus-multiple-delivery.ts deleted file mode 100644 index c10133d19a..0000000000 --- a/test/integration/validators-exit-bus-multiple-delivery.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - -import { ValidatorsExitBusOracle } from "typechain-types"; - -import { advanceChainTime, de0x, ether, numberToHex } from "lib"; -import { getProtocolContext, ProtocolContext } from "lib/protocol"; - -import { Snapshot } from "test/suite"; - -interface ExitRequest { - moduleId: number; - nodeOpId: number; - valIndex: number; - valPubkey: string; -} - -const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; -}; - -const hashExitRequest = (request: { dataFormat: number; data: string }) => { - return ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), - ); -}; - -describe("ValidatorsExitBus integration", () => { - let ctx: ProtocolContext; - let snapshot: string; - - let veb: ValidatorsExitBusOracle; - let hashReporter: HardhatEthersSigner; - let resumer: HardhatEthersSigner; - let agent: HardhatEthersSigner; - let limitManager: HardhatEthersSigner; - - const requests = [ - { - moduleId: 1, - nodeOpId: 1, - valIndex: 1, - valPubkey: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - }, - { - moduleId: 2, - nodeOpId: 2, - valIndex: 2, - valPubkey: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - { - moduleId: 3, - nodeOpId: 3, - valIndex: 3, - valPubkey: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }, - { - moduleId: 4, - nodeOpId: 4, - valIndex: 4, - valPubkey: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - }, - { - moduleId: 5, - nodeOpId: 5, - valIndex: 5, - valPubkey: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - }, - { - moduleId: 6, - nodeOpId: 6, - valIndex: 6, - valPubkey: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - }, - { - moduleId: 7, - nodeOpId: 7, - valIndex: 7, - valPubkey: "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", - }, - { - moduleId: 8, - nodeOpId: 8, - valIndex: 8, - valPubkey: "0x222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", - }, - { - moduleId: 9, - nodeOpId: 9, - valIndex: 9, - valPubkey: "0x333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", - }, - { - moduleId: 10, - nodeOpId: 10, - valIndex: 10, - valPubkey: "0x444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444", - }, - ]; - - const exitRequests = { - dataFormat: 1, - data: - "0x" + - requests - .map(({ moduleId, nodeOpId, valIndex, valPubkey }) => { - return encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey }); - }) - .join(""), - }; - - before(async () => { - ctx = await getProtocolContext(); - veb = ctx.contracts.validatorsExitBusOracle; - - [hashReporter, resumer, limitManager] = await ethers.getSigners(); - - agent = await ctx.getSigner("agent", ether("1")); - - // Grant role to submit exit hash - const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); - await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); - - const manageLimitRole = await veb.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); - await veb.connect(agent).grantRole(manageLimitRole, limitManager); - - if (await veb.isPaused()) { - const resumeRole = await veb.RESUME_ROLE(); - await veb.connect(agent).grantRole(resumeRole, resumer); - await veb.connect(resumer).resume(); - - expect(veb.isPaused()).to.be.false; - } - }); - - beforeEach(async () => (snapshot = await Snapshot.take())); - afterEach(async () => await Snapshot.restore(snapshot)); - - it("should submit hash and submit data in 4 deliveries", async () => { - // --- Setup exit limit --- - const maxLimit = 3; - const exitsPerFrame = 1; - const frameDurationInSec = 48; - await veb.connect(limitManager).setExitRequestLimit(maxLimit, exitsPerFrame, frameDurationInSec); - - // --- Prepare data --- - const exitRequestsHash: string = hashExitRequest(exitRequests); - - await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) - .to.emit(veb, "RequestsHashSubmitted") - .withArgs(exitRequestsHash); - - // --- 1st delivery: deliver maxLimit (3) requests --- - const tx1 = await veb.submitExitRequestsData(exitRequests); - const receipt1 = await tx1.wait(); - const block1 = await ethers.provider.getBlock(receipt1!.blockNumber); - const block1Timestamp = block1!.timestamp; - - // Validate logs & event count - const logs1 = receipt1!.logs.filter( - (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, - ); - expect(logs1.length).to.equal(maxLimit); - - for (let i = 0; i < maxLimit; i++) { - const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs1[i].data, logs1[i].topics); - const expected = requests[i]; - expect(decoded[0]).to.equal(expected.moduleId); - expect(decoded[1]).to.equal(expected.nodeOpId); - expect(decoded[2]).to.equal(expected.valIndex); - expect(decoded[3]).to.equal(expected.valPubkey); - expect(decoded[4]).to.equal(block1Timestamp); - } - - // Validate delivery history - const deliveryHistory1 = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(deliveryHistory1.length).to.equal(1); - expect(deliveryHistory1[0].lastDeliveredExitDataIndex).to.equal(maxLimit - 1); - - // --- 2nd delivery: only 1 request can be processed after 48 seconds --- - await advanceChainTime(BigInt(frameDurationInSec)); - - const tx2 = await veb.submitExitRequestsData(exitRequests); - const receipt2 = await tx2.wait(); - const block2 = await ethers.provider.getBlock(receipt2!.blockNumber); - const block2Timestamp = block2!.timestamp; - - const logs2 = receipt2!.logs.filter( - (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, - ); - expect(logs2.length).to.equal(1); - - const decoded2 = veb.interface.decodeEventLog("ValidatorExitRequest", logs2[0].data, logs2[0].topics); - const expected2 = requests[maxLimit]; - expect(decoded2[0]).to.equal(expected2.moduleId); - expect(decoded2[1]).to.equal(expected2.nodeOpId); - expect(decoded2[2]).to.equal(expected2.valIndex); - expect(decoded2[3]).to.equal(expected2.valPubkey); - expect(decoded2[4]).to.equal(block2Timestamp); - - const deliveryHistory2 = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(deliveryHistory2.length).to.equal(2); - expect(deliveryHistory2[1].lastDeliveredExitDataIndex).to.equal(maxLimit); - - // --- 3rd delivery: deliver remaining 6 requests after waiting (6 * 48) seconds --- - let remainingRequestsCount = requests.length - (maxLimit + 1); // 10 - 4 = 6 - await advanceChainTime(BigInt(frameDurationInSec * remainingRequestsCount)); - - const tx3 = await veb.submitExitRequestsData(exitRequests); - const receipt3 = await tx3.wait(); - - const logs3 = receipt3!.logs.filter( - (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, - ); - - expect(logs3.length).to.equal(maxLimit); - - for (let i = 0; i < logs3.length; i++) { - const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs3[i].data, logs3[i].topics); - const expected = requests[4 + i]; - expect(decoded[0]).to.equal(expected.moduleId); - expect(decoded[1]).to.equal(expected.nodeOpId); - expect(decoded[2]).to.equal(expected.valIndex); - expect(decoded[3]).to.equal(expected.valPubkey); - } - - // --- 4th delivery: final 3 requests, but no need to increase time --- - - const currentLimit = (await veb.getExitRequestLimitFullInfo()).currentExitRequestsLimit; - expect(currentLimit).to.be.equal(0); - - remainingRequestsCount = requests.length - (maxLimit * 2 + 1); // 3 - - await advanceChainTime(BigInt(frameDurationInSec * remainingRequestsCount)); - - const tx4 = await veb.submitExitRequestsData(exitRequests); - const receipt4 = await tx4.wait(); - const logs4 = receipt4!.logs.filter( - (log) => log.topics[0] === veb.interface.getEvent("ValidatorExitRequest").topicHash, - ); - expect(logs4.length).to.equal(maxLimit); - - for (let i = 0; i < logs4.length; i++) { - const decoded = veb.interface.decodeEventLog("ValidatorExitRequest", logs4[i].data, logs4[i].topics); - const expected = requests[7 + i]; - expect(decoded[0]).to.equal(expected.moduleId); - expect(decoded[1]).to.equal(expected.nodeOpId); - expect(decoded[2]).to.equal(expected.valIndex); - expect(decoded[3]).to.equal(expected.valPubkey); - } - - // --- Validate total logs delivered = 10 requests --- - const totalDelivered = logs1.length + logs2.length + logs3.length + logs4.length; - expect(totalDelivered).to.equal(requests.length); - - // --- Validate delivery history entries: 4 deliveries --- - const finalHistory = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(finalHistory.length).to.equal(4); - expect(finalHistory[3].lastDeliveredExitDataIndex).to.equal(requests.length - 1); - }); -}); From 8d6cf134f91e4c2907371940498e3850130c9a07 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 23:01:54 +0400 Subject: [PATCH 224/405] Update contracts/0.8.9/TriggerableWithdrawalsGateway.sol Co-authored-by: Logachev Nikita --- contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index edc1748a52..d000575e12 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -232,7 +232,13 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { withdrawalRequestPaidFee, exitType ) - {} catch { // (bytes memory lowLevelRevertData) + {} catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the onValidatorExitTriggered() reverts because of the + /// "out of gas" error. Here we assume that the onValidatorExitTriggered() method doesn't + /// have reverts with empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); emit StakingModuleExitNotificationFailed(data.stakingModuleId, data.nodeOperatorId, data.pubkey); } } From 85a62122b390f3e785d07194124beeabac8f9f2e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 23:03:59 +0400 Subject: [PATCH 225/405] fix: integration tests --- .../validators-exit-bus-single-delivery.ts | 5 ++--- test/integration/validators-exit-bus-trigger-exits.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/integration/validators-exit-bus-single-delivery.ts b/test/integration/validators-exit-bus-single-delivery.ts index 82803ea947..3c8e9ad521 100644 --- a/test/integration/validators-exit-bus-single-delivery.ts +++ b/test/integration/validators-exit-bus-single-delivery.ts @@ -89,8 +89,7 @@ describe("ValidatorsExitBus integration", () => { .to.emit(veb, "ValidatorExitRequest") .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - const deliveryHistory = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(deliveryHistory.length).to.equal(1); - expect(deliveryHistory[0].lastDeliveredExitDataIndex).to.equal(0); + const timestamp = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(timestamp).to.equal(blockTimestamp); }); }); diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts index d4d061808c..8e91b7b4ce 100644 --- a/test/integration/validators-exit-bus-trigger-exits.ts +++ b/test/integration/validators-exit-bus-trigger-exits.ts @@ -92,11 +92,12 @@ describe("ValidatorsExitBus integration", () => { .to.emit(veb, "ValidatorExitRequest") .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - const deliveryHistory = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); - expect(deliveryHistory.length).to.equal(1); - expect(deliveryHistory[0].lastDeliveredExitDataIndex).to.equal(0); + const timestamp = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + expect(timestamp).to.equal(blockTimestamp); - await expect(veb.triggerExits(exitRequest, [0], refundRecipient, {value: 10})) - .to.emit(wv, "WithdrawalRequestAdded"); + await expect(veb.triggerExits(exitRequest, [0], refundRecipient, { value: 10 })).to.emit( + wv, + "WithdrawalRequestAdded", + ); }); }); From 987645129d5e3f451fbec0cfc2028f527e89db68 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 23:26:17 +0400 Subject: [PATCH 226/405] fix: getExitRequestsDeliveryHistory -> validatorsExitBusOracle.getDeliveryTime --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- contracts/0.8.25/interfaces/IValidatorsExitBus.sol | 2 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 5 ++--- .../0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol | 2 +- test/0.8.25/validatorExitDelayVerifier.test.ts | 2 +- test/0.8.9/oracle/validator-exit-bus.helpers.test.ts | 11 ++++------- test/integration/report-validator-exit-delay.ts | 10 +++++----- .../validators-exit-bus-single-delivery.ts | 2 +- test/integration/validators-exit-bus-trigger-exits.ts | 2 +- 9 files changed, 17 insertions(+), 21 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 815b4f3833..57850ac23e 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -361,7 +361,7 @@ contract ValidatorExitDelayVerifier { ExitRequestData calldata exitRequests ) internal view returns (uint256 deliveryTimestamp) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); - deliveryTimestamp = veb.getExitRequestsDeliveryHistory(exitRequestsHash); + deliveryTimestamp = veb.getDeliveryTime(exitRequestsHash); if (deliveryTimestamp == 0) { revert EmptyDeliveryHistory(); diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol index 81c264a8ee..bc318a25c2 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.25; interface IValidatorsExitBus { - function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (uint256 timestamp); + function getDeliveryTime(bytes32 exitRequestsHash) external view returns (uint256 timestamp); function unpackExitRequest( bytes calldata exitRequests, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 486de81a5f..5b17b04598 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -403,7 +403,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } /** - * @notice Timestamp + * @notice Returns the timestamp when the exit request was delivered. * * @param exitRequestsHash - The exit requests hash. * @@ -411,8 +411,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * - exitRequestsHash was not submited * - Request was not delivered */ - function getExitRequestsDeliveryHistory( - // TODO: rename + function getDeliveryTime( bytes32 exitRequestsHash ) external view returns (uint256 deliveryDateTimestamp) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 3c67b17c42..2f3e5dcab8 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -30,7 +30,7 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { } } - function getExitRequestsDeliveryHistory(bytes32 exitRequestsHash) external view returns (uint256 timestamp) { + function getDeliveryTime(bytes32 exitRequestsHash) external view returns (uint256 timestamp) { require(exitRequestsHash == _hash, "Mock error, Invalid exitRequestsHash"); return _deliveryTimestamp; } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 3928b58ac1..8244cb594a 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -518,7 +518,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { // Report not unpacked, deliveryTimestamp == 0 await vebo.setExitRequests(encodedExitRequestsHash, 0, exitRequests); - expect(await vebo.getExitRequestsDeliveryHistory(encodedExitRequestsHash)).to.equal(0); + expect(await vebo.getDeliveryTime(encodedExitRequestsHash)).to.equal(0); await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 4925132b37..96ff8b8100 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -128,7 +128,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { }); }); - context("getExitRequestsDeliveryHistory", () => { + context("getDeliveryTime", () => { let originalState: string; before(async () => { @@ -140,10 +140,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { it("reverts if exitRequestsHash was never submitted (contractVersion = 0)", async () => { const fakeHash = keccak256("0x1111"); - await expect(oracle.getExitRequestsDeliveryHistory(fakeHash)).to.be.revertedWithCustomError( - oracle, - "ExitHashNotSubmitted", - ); + await expect(oracle.getDeliveryTime(fakeHash)).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); }); it("reverts if request was not delivered", async () => { @@ -154,7 +151,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { // Call the helper to store the hash await oracle.storeNewHashRequestStatus(exitRequestsHash, contractVersion, timestamp); - await expect(oracle.getExitRequestsDeliveryHistory(exitRequestsHash)).to.be.revertedWithCustomError( + await expect(oracle.getDeliveryTime(exitRequestsHash)).to.be.revertedWithCustomError( oracle, "RequestsNotDelivered", ); @@ -168,7 +165,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { // Call the helper to store the hash await oracle.storeNewHashRequestStatus(exitRequestsHash, contractVersion, timestamp); - const deliveredExitDataTimestamp = await oracle.getExitRequestsDeliveryHistory(exitRequestsHash); + const deliveredExitDataTimestamp = await oracle.getDeliveryTime(exitRequestsHash); expect(deliveredExitDataTimestamp).to.equal(timestamp); }); diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 72c6b98fdc..fb40175ed1 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -85,7 +85,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryTimestamp = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTime(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -152,7 +152,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryTimestamp = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTime(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -306,7 +306,7 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "EmptyDeliveryHistory"); + ).to.be.revertedWithCustomError(await validatorsExitBusOracle, "RequestsNotDelivered"); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -317,7 +317,7 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(await validatorExitDelayVerifier, "EmptyDeliveryHistory"); + ).to.be.revertedWithCustomError(await validatorsExitBusOracle, "RequestsNotDelivered"); }); it("Should revert when submitting validator exit delay with invalid beacon block root", async () => { @@ -375,7 +375,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryTimestamp = await validatorsExitBusOracle.getExitRequestsDeliveryHistory(encodedExitRequestsHash); + const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTime(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); diff --git a/test/integration/validators-exit-bus-single-delivery.ts b/test/integration/validators-exit-bus-single-delivery.ts index 3c8e9ad521..0b34faeed5 100644 --- a/test/integration/validators-exit-bus-single-delivery.ts +++ b/test/integration/validators-exit-bus-single-delivery.ts @@ -89,7 +89,7 @@ describe("ValidatorsExitBus integration", () => { .to.emit(veb, "ValidatorExitRequest") .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - const timestamp = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + const timestamp = await veb.getDeliveryTime(exitRequestsHash); expect(timestamp).to.equal(blockTimestamp); }); }); diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts index 8e91b7b4ce..6236e496b2 100644 --- a/test/integration/validators-exit-bus-trigger-exits.ts +++ b/test/integration/validators-exit-bus-trigger-exits.ts @@ -92,7 +92,7 @@ describe("ValidatorsExitBus integration", () => { .to.emit(veb, "ValidatorExitRequest") .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - const timestamp = await veb.getExitRequestsDeliveryHistory(exitRequestsHash); + const timestamp = await veb.getDeliveryTime(exitRequestsHash); expect(timestamp).to.equal(blockTimestamp); await expect(veb.triggerExits(exitRequest, [0], refundRecipient, { value: 10 })).to.emit( From bf7cd8db2106177e5bcc314d7b998a5dd8029719 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 29 May 2025 23:35:18 +0400 Subject: [PATCH 227/405] fix: UnrecoverableModuleError declaration --- contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index d000575e12..20aea151d9 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -76,6 +76,11 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { */ error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); + /** + * @notice Thrown when onValidatorExitTriggered() reverts with empty data (e.g., out-of-gas error) + */ + error UnrecoverableModuleError(); + struct ValidatorData { uint256 stakingModuleId; uint256 nodeOperatorId; @@ -225,13 +230,13 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { data = validatorsData[i]; try - stakingRouter.onValidatorExitTriggered( + stakingRouter.onValidatorExitTriggered( data.stakingModuleId, data.nodeOperatorId, data.pubkey, withdrawalRequestPaidFee, exitType - ) + ) {} catch (bytes memory lowLevelRevertData) { /// @dev This check is required to prevent incorrect gas estimation of the method. /// Without it, Ethereum nodes that use binary search for gas estimation may From 4639cb46ab30b01fd9e6f38e0744e42381ba43d7 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 23:21:14 +0200 Subject: [PATCH 228/405] feat: withdrawal vault accept TW requests only from gateway --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 41 +- contracts/0.8.9/WithdrawalVault.sol | 90 ++-- ...EIP7685.sol => WithdrawalVaultEIP7002.sol} | 37 +- .../steps/0090-deploy-non-aragon-contracts.ts | 25 +- .../0120-initialize-non-aragon-contracts.ts | 5 - scripts/scratch/steps/0130-grant-roles.ts | 15 - scripts/triggerable-withdrawals/tw-deploy.ts | 39 +- .../contracts/WithdrawalVault__Harness.sol | 6 +- ...ggerableWithdrawalGateway.pausable.test.ts | 367 +++++++++++++ .../withdrawalVault/withdrawalVault.test.ts | 495 +++--------------- 10 files changed, 553 insertions(+), 567 deletions(-) rename contracts/0.8.9/{WithdrawalVaultEIP7685.sol => WithdrawalVaultEIP7002.sol} (59%) create mode 100644 test/0.8.9/triggerableWithdrawalGateway.pausable.test.ts diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 1a7431d83e..18218a0427 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.9; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; +import {PausableUntil} from "./utils/PausableUntil.sol"; interface IWithdrawalVault { function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) external payable; @@ -27,7 +28,7 @@ interface IStakingRouter { * @notice TriggerableWithdrawalsGateway contract is one entrypoint for all triggerable withdrawal requests (TWRs) in protocol. * This contract is responsible for limiting TWRs, checking ADD_FULL_WITHDRAWAL_REQUEST_ROLE role before it gets to Withdrawal Vault. */ -contract TriggerableWithdrawalsGateway is AccessControlEnumerable { +contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil { using ExitLimitUtilsStorage for bytes32; using ExitLimitUtils for ExitRequestLimitData; @@ -83,6 +84,8 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { bytes pubkey; } + bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); + bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); bytes32 public constant TW_EXIT_LIMIT_MANAGER_ROLE = keccak256("TW_EXIT_LIMIT_MANAGER_ROLE"); @@ -116,6 +119,40 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } + /** + * @dev Resumes the triggerable withdrawals requests. + * @notice Reverts if: + * - The contract is not paused. + * - The sender does not have the `RESUME_ROLE`. + */ + function resume() external onlyRole(RESUME_ROLE) { + _resume(); + } + + /** + * @notice Pauses the triggerable withdrawals requests placement for a specified duration. + * @param _duration The pause duration in seconds (use `PAUSE_INFINITELY` for unlimited). + * @dev Reverts if: + * - The contract is already paused. + * - The sender does not have the `PAUSE_ROLE`. + * - A zero duration is passed. + */ + function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { + _pauseFor(_duration); + } + + /** + * @notice Pauses the triggerable withdrawals requests placement until a specified timestamp. + * @param _pauseUntilInclusive The last second to pause until (inclusive). + * @dev Reverts if: + * - The timestamp is in the past. + * - The sender does not have the `PAUSE_ROLE`. + * - The contract is already paused. + */ + function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { + _pauseUntil(_pauseUntilInclusive); + } + /** * @dev Submits Triggerable Withdrawal Requests to the Withdrawal Vault as full withdrawal requests * for the specified validator public keys. @@ -137,7 +174,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable { ValidatorData[] calldata validatorsData, address refundRecipient, uint8 exitType - ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance whenResumed { if (msg.value == 0) revert ZeroArgument("msg.value"); uint256 requestsCount = validatorsData.length; if (requestsCount == 0) revert ZeroArgument("validatorsData"); diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 21c2a5f9b7..8bc645cb1f 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -10,9 +10,8 @@ import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; -import {PausableUntil} from "./utils/PausableUntil.sol"; -import {WithdrawalVaultEIP7685} from "./WithdrawalVaultEIP7685.sol"; +import {WithdrawalVaultEIP7002} from "./WithdrawalVaultEIP7002.sol"; interface ILido { /** @@ -26,15 +25,12 @@ interface ILido { /** * @title A vault for temporary storage of withdrawals */ -contract WithdrawalVault is AccessControlEnumerable, Versioned, PausableUntil, WithdrawalVaultEIP7685 { +contract WithdrawalVault is Versioned, WithdrawalVaultEIP7002 { using SafeERC20 for IERC20; - bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); - bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - bytes32 public constant ADD_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_WITHDRAWAL_REQUEST_ROLE"); - ILido public immutable LIDO; address public immutable TREASURY; + address public immutable TRIGGERABLE_WITHDRAWALS_GATEWAY; // Events /** @@ -52,6 +48,7 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, PausableUntil, W // Errors error ZeroAddress(); error NotLido(); + error NotTriggerableWithdrawalsGateway(); error NotEnoughEther(uint256 requested, uint256 balance); error ZeroAmount(); @@ -59,12 +56,14 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, PausableUntil, W * @param _lido the Lido token (stETH) address * @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces) */ - constructor(address _lido, address _treasury) { + constructor(address _lido, address _treasury, address _triggerableWithdrawalsGateway) { _onlyNonZeroAddress(_lido); _onlyNonZeroAddress(_treasury); + _onlyNonZeroAddress(_triggerableWithdrawalsGateway); LIDO = ILido(_lido); TREASURY = _treasury; + TRIGGERABLE_WITHDRAWALS_GATEWAY = _triggerableWithdrawalsGateway; } /// @dev Ensures the contract’s ETH balance is unchanged. @@ -75,60 +74,20 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, PausableUntil, W } /// @notice Initializes the contract. Can be called only once. - /// @param _admin Lido DAO Aragon agent contract address. /// @dev Proxy initialization method. - function initialize(address _admin) external { + function initialize() external { // Initializations for v0 --> v2 _checkContractVersion(0); - - _initialize_v2(_admin); _initializeContractVersionTo(2); } /// @notice Finalizes upgrade to v2 (from v1). Can be called only once. - /// @param _admin Lido DAO Aragon agent contract address. - function finalizeUpgrade_v2(address _admin) external { + function finalizeUpgrade_v2() external { // Finalization for v1 --> v2 _checkContractVersion(1); - - _initialize_v2(_admin); _updateContractVersion(2); } - /** - * @dev Resumes the general purpose execution layer requests. - * @notice Reverts if: - * - The contract is not paused. - * - The sender does not have the `RESUME_ROLE`. - */ - function resume() external onlyRole(RESUME_ROLE) { - _resume(); - } - - /** - * @notice Pauses the general purpose execution layer requests placement for a specified duration. - * @param _duration The pause duration in seconds (use `PAUSE_INFINITELY` for unlimited). - * @dev Reverts if: - * - The contract is already paused. - * - The sender does not have the `PAUSE_ROLE`. - * - A zero duration is passed. - */ - function pauseFor(uint256 _duration) external onlyRole(PAUSE_ROLE) { - _pauseFor(_duration); - } - - /** - * @notice Pauses the general purpose execution layer requests placement until a specified timestamp. - * @param _pauseUntilInclusive The last second to pause until (inclusive). - * @dev Reverts if: - * - The timestamp is in the past. - * - The sender does not have the `PAUSE_ROLE`. - * - The contract is already paused. - */ - function pauseUntil(uint256 _pauseUntilInclusive) external onlyRole(PAUSE_ROLE) { - _pauseUntil(_pauseUntilInclusive); - } - /** * @notice Withdraw `_amount` of accumulated withdrawals to Lido contract * @dev Can be called only by the Lido contract @@ -184,19 +143,34 @@ contract WithdrawalVault is AccessControlEnumerable, Versioned, PausableUntil, W if (_address == address(0)) revert ZeroAddress(); } - function _initialize_v2(address _admin) internal { - _onlyNonZeroAddress(_admin); - _setupRole(DEFAULT_ADMIN_ROLE, _admin); - } - - /// Withdrawals EIP-7002 + /** + * @dev Submits EIP-7002 full or partial withdrawal requests for the specified public keys. + * Each full withdrawal request instructs a validator to fully withdraw its stake and exit its duties as a validator. + * Each partial withdrawal request instructs a validator to withdraw a specified amount of ETH. + * + * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting partial withdrawals. + * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * + * @param amounts An array of 8-byte unsigned integers representing the amounts to be withdrawn for each corresponding public key. + * For full withdrawal requests, the amount should be set to 0. + * For partial withdrawal requests, the amount should be greater than 0. + * + * @notice Reverts if: + * - The caller is not TriggerableWithdrawalsGateway. + * - The provided public key array is empty. + * - The provided public key array malformed. + * - The provided public key and amount arrays are not of equal length. + * - The provided total withdrawal fee value is invalid. + */ function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) external payable - onlyRole(ADD_WITHDRAWAL_REQUEST_ROLE) - whenResumed preservesEthBalance { + if (msg.sender != TRIGGERABLE_WITHDRAWALS_GATEWAY) { + revert NotTriggerableWithdrawalsGateway(); + } + _addWithdrawalRequests(pubkeys, amounts); } } diff --git a/contracts/0.8.9/WithdrawalVaultEIP7685.sol b/contracts/0.8.9/WithdrawalVaultEIP7002.sol similarity index 59% rename from contracts/0.8.9/WithdrawalVaultEIP7685.sol rename to contracts/0.8.9/WithdrawalVaultEIP7002.sol index 35e182f161..967df4477b 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7685.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7002.sol @@ -5,10 +5,9 @@ pragma solidity 0.8.9; /** - * @title A base contract for a withdrawal vault implementing EIP-7685: General Purpose Execution Layer Requests - * @dev This contract enables validators to submit EIP-7002 withdrawal requests. + * @title A base contract for a withdrawal vault, enables to submit EIP-7002 withdrawal requests. */ -abstract contract WithdrawalVaultEIP7685 { +abstract contract WithdrawalVaultEIP7002 { address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; event WithdrawalRequestAdded(bytes request); @@ -21,24 +20,13 @@ abstract contract WithdrawalVaultEIP7685 { error RequestAdditionFailed(bytes callData); /** - * @dev Submits EIP-7002 full or partial withdrawal requests for the specified public keys. - * Each full withdrawal request instructs a validator to fully withdraw its stake and exit its duties as a validator. - * Each partial withdrawal request instructs a validator to withdraw a specified amount of ETH. - * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting partial withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... - * - * @param amounts An array of 8-byte unsigned integers representing the amounts to be withdrawn for each corresponding public key. - * For full withdrawal requests, the amount should be set to 0. - * For partial withdrawal requests, the amount should be greater than 0. - * - * @notice Reverts if: - * - The caller does not have the `ADD_WITHDRAWAL_REQUEST_ROLE`. - * - The provided public key array is empty. - * - The provided public key array malformed. - * - The provided public key and amount arrays are not of equal length. - * - The provided total withdrawal fee value is invalid. + * @dev Retrieves the current EIP-7002 withdrawal fee. + * @return The minimum fee required per withdrawal request. */ + function getWithdrawalRequestFee() public view returns (uint256) { + return _getRequestFee(WITHDRAWAL_REQUEST); + } + function _addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) internal { uint256 requestsCount = pubkeys.length; if (requestsCount == 0) revert ZeroArgument("pubkeys"); @@ -52,14 +40,6 @@ abstract contract WithdrawalVaultEIP7685 { } } - /** - * @dev Retrieves the current EIP-7002 withdrawal fee. - * @return The minimum fee required per withdrawal request. - */ - function getWithdrawalRequestFee() public view returns (uint256) { - return _getRequestFee(WITHDRAWAL_REQUEST); - } - function _getRequestFee(address requestedContract) internal view returns (uint256) { (bool success, bytes memory feeData) = requestedContract.staticcall(""); @@ -74,7 +54,6 @@ abstract contract WithdrawalVaultEIP7685 { return abi.decode(feeData, (uint256)); } - // function _callAddWithdrawalRequest(uint256 fee, bytes memory request) internal { function _callAddWithdrawalRequest(bytes calldata pubkey, uint64 amount, uint256 fee) internal { assert(pubkey.length == 48); diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 88872a96f3..93f866afa4 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -66,6 +66,18 @@ export async function main() { dummyContract.address, ); + // Deploy Triggerable Withdrawals Gateway + const maxExitRequestsLimit = 13000; + const exitsPerFrame = 1; + const frameDurationInSec = 48; + + const triggerableWithdrawalsGateway = await deployWithoutProxy( + Sk.triggerableWithdrawalsGateway, + "TriggerableWithdrawalsGateway", + deployer, + [admin, locator.address, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec], + ); + // Deploy EIP712StETH await deployWithoutProxy(Sk.eip712StETH, "EIP712StETH", deployer, [lidoAddress]); @@ -85,6 +97,7 @@ export async function main() { const withdrawalVaultImpl = await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, [ lidoAddress, treasuryAddress, + triggerableWithdrawalsGateway.address, ]); const withdrawalsManagerProxyConstructorArgs = [votingAddress, withdrawalVaultImpl.address]; @@ -221,18 +234,6 @@ export async function main() { ], ); - // Deploy Triggerable Withdrawals Gateway - const maxExitRequestsLimit = 13000; - const exitsPerFrame = 1; - const frameDurationInSec = 48; - - const triggerableWithdrawalsGateway = await deployWithoutProxy( - Sk.triggerableWithdrawalsGateway, - "TriggerableWithdrawalsGateway", - deployer, - [admin, locator.address, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec], - ); - // Update LidoLocator with valid implementation const locatorConfig: string[] = [ accountingOracle.address, diff --git a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts index 4a489939c3..d63ea196a7 100644 --- a/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0120-initialize-non-aragon-contracts.ts @@ -35,7 +35,6 @@ export async function main() { const exitBusOracleAdmin = testnetAdmin; const stakingRouterAdmin = testnetAdmin; const withdrawalQueueAdmin = testnetAdmin; - const withdrawalVaultAdmin = testnetAdmin; // Initialize NodeOperatorsRegistry @@ -117,10 +116,6 @@ export async function main() { { from: deployer }, ); - // Initialize WithdrawalVault - const withdrawalVault = await loadContract("WithdrawalVault", withdrawalVaultAddress); - await makeTx(withdrawalVault, "initialize", [withdrawalVaultAdmin], { from: deployer }); - // Initialize WithdrawalQueue const withdrawalQueue = await loadContract("WithdrawalQueueERC721", withdrawalQueueAddress); await makeTx(withdrawalQueue, "initialize", [withdrawalQueueAdmin], { from: deployer }); diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index 9d2f0bb4e3..7927049f5f 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -6,7 +6,6 @@ import { TriggerableWithdrawalsGateway, ValidatorsExitBusOracle, WithdrawalQueueERC721, - WithdrawalVault, } from "typechain-types"; import { loadContract } from "lib/contract"; @@ -26,7 +25,6 @@ export async function main() { const burnerAddress = state[Sk.burner].address; const stakingRouterAddress = state[Sk.stakingRouter].proxy.address; const withdrawalQueueAddress = state[Sk.withdrawalQueueERC721].proxy.address; - const withdrawalVaultAddress = state[Sk.withdrawalVault].proxy.address; const accountingOracleAddress = state[Sk.accountingOracle].proxy.address; const validatorsExitBusOracleAddress = state[Sk.validatorsExitBusOracle].proxy.address; const depositSecurityModuleAddress = state[Sk.depositSecurityModule].address; @@ -59,7 +57,6 @@ export async function main() { { from: deployer }, ); - // ValidatorsExitBusOracle if (gateSealAddress) { const validatorsExitBusOracle = await loadContract( @@ -105,18 +102,6 @@ export async function main() { from: deployer, }); - // WithdrawalVault - const withdrawalVault = await loadContract("WithdrawalVault", withdrawalVaultAddress); - - await makeTx( - withdrawalVault, - "grantRole", - [await withdrawalVault.ADD_WITHDRAWAL_REQUEST_ROLE(), triggerableWithdrawalsGatewayAddress], - { - from: deployer, - }, - ); - // Burner const burner = await loadContract("Burner", burnerAddress); // NB: REQUEST_BURN_SHARES_ROLE is already granted to Lido in Burner constructor diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 30f95af3e3..d7bbb19c71 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -63,7 +63,16 @@ async function main() { log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle.address}`); log.emptyLine(); - const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; + const triggerableWithdrawalsGateway = await deployImplementation( + Sk.triggerableWithdrawalsGateway, + "TriggerableWithdrawalsGateway", + deployer, + [agent, locator.address, 13000, 1, 48], + ); + log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); + log.emptyLine(); + + const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY, triggerableWithdrawalsGateway.address]; const withdrawalVault = await deployImplementation( Sk.withdrawalVault, @@ -120,27 +129,13 @@ async function main() { log.success(`ValidatorExitDelayVerifier implementation address: ${validatorExitDelayVerifier.address}`); log.emptyLine(); - const triggerableWithdrawalsGateway = await deployImplementation( - Sk.triggerableWithdrawalsGateway, - "TriggerableWithdrawalsGateway", - deployer, - [agent, locator.address, 13000, 1, 48], - ); - log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); - log.emptyLine(); - - const accountingOracle = await deployImplementation( - Sk.accountingOracle, - "AccountingOracle", - deployer, - [ - locator.address, - await locator.lido(), - await locator.legacyOracle(), - Number(chainSpec.secondsPerSlot), - Number(chainSpec.genesisTime), - ], - ); + const accountingOracle = await deployImplementation(Sk.accountingOracle, "AccountingOracle", deployer, [ + locator.address, + await locator.lido(), + await locator.legacyOracle(), + Number(chainSpec.secondsPerSlot), + Number(chainSpec.genesisTime), + ]); // fetch contract addresses that will not changed const locatorConfig = [ diff --git a/test/0.8.9/contracts/WithdrawalVault__Harness.sol b/test/0.8.9/contracts/WithdrawalVault__Harness.sol index c2fc2a46e6..8bbefb2f82 100644 --- a/test/0.8.9/contracts/WithdrawalVault__Harness.sol +++ b/test/0.8.9/contracts/WithdrawalVault__Harness.sol @@ -6,7 +6,11 @@ pragma solidity 0.8.9; import {WithdrawalVault} from "contracts/0.8.9/WithdrawalVault.sol"; contract WithdrawalVault__Harness is WithdrawalVault { - constructor(address _lido, address _treasury) WithdrawalVault(_lido, _treasury) {} + constructor( + address _lido, + address _treasury, + address _triggerableWithdrawalsGateway + ) WithdrawalVault(_lido, _treasury, _triggerableWithdrawalsGateway) {} function harness__initializeContractVersionTo(uint256 _version) external { _initializeContractVersionTo(_version); diff --git a/test/0.8.9/triggerableWithdrawalGateway.pausable.test.ts b/test/0.8.9/triggerableWithdrawalGateway.pausable.test.ts new file mode 100644 index 0000000000..dbe78504d1 --- /dev/null +++ b/test/0.8.9/triggerableWithdrawalGateway.pausable.test.ts @@ -0,0 +1,367 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { + StakingRouter__MockForTWG, + TriggerableWithdrawalsGateway__Harness, + WithdrawalVault__MockForTWG, +} from "typechain-types"; + +import { advanceChainTime, getCurrentBlockTimestamp, streccak } from "lib"; + +import { Snapshot } from "test/suite"; + +import { deployLidoLocator, updateLidoLocatorImplementation } from "../deploy/locator"; + +const PAUSE_ROLE = streccak("PAUSE_ROLE"); +const RESUME_ROLE = streccak("RESUME_ROLE"); + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", +]; + +const exitRequests = [ + { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 0, valIndex: 1, valPubkey: PUBKEYS[2] }, +]; + +const ZERO_ADDRESS = ethers.ZeroAddress; + +describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { + let triggerableWithdrawalsGateway: TriggerableWithdrawalsGateway__Harness; + let withdrawalVault: WithdrawalVault__MockForTWG; + let stakingRouter: StakingRouter__MockForTWG; + let admin: HardhatEthersSigner; + let authorizedEntity: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + + let originalState: string; + + const createValidatorDataList = (requests: ExitRequest[]) => { + return requests.map((request) => ({ + stakingModuleId: request.moduleId, + nodeOperatorId: request.nodeOpId, + pubkey: request.valPubkey, + })); + }; + + before(async () => { + [admin, authorizedEntity, stranger] = await ethers.getSigners(); + + const locator = await deployLidoLocator(); + const locatorAddr = await locator.getAddress(); + + withdrawalVault = await ethers.deployContract("WithdrawalVault__MockForTWG"); + stakingRouter = await ethers.deployContract("StakingRouter__MockForTWG"); + + await updateLidoLocatorImplementation(locatorAddr, { + withdrawalVault: await withdrawalVault.getAddress(), + stakingRouter: await stakingRouter.getAddress(), + }); + + triggerableWithdrawalsGateway = await ethers.deployContract("TriggerableWithdrawalsGateway__Harness", [ + admin, + locatorAddr, + 100, + 1, + 48, + ]); + + const role = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + await triggerableWithdrawalsGateway.grantRole(role, authorizedEntity); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("pausable until", () => { + beforeEach(async () => { + // set up necessary roles + await triggerableWithdrawalsGateway.connect(admin).grantRole(PAUSE_ROLE, admin); + await triggerableWithdrawalsGateway.connect(admin).grantRole(RESUME_ROLE, admin); + }); + + context("resume", () => { + it("should revert if the sender does not have the RESUME_ROLE", async () => { + // First pause the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + + // Try to resume without the RESUME_ROLE + await expect(triggerableWithdrawalsGateway.connect(stranger).resume()).to.be.revertedWithOZAccessControlError( + stranger.address, + RESUME_ROLE, + ); + }); + + it("should revert if the contract is not paused", async () => { + // Contract is initially not paused + await expect(triggerableWithdrawalsGateway.connect(admin).resume()).to.be.revertedWithCustomError( + triggerableWithdrawalsGateway, + "PausedExpected", + ); + }); + + it("should resume the contract when paused and emit Resumed event", async () => { + // First pause the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + + // Resume the contract + await expect(triggerableWithdrawalsGateway.connect(admin).resume()).to.emit( + triggerableWithdrawalsGateway, + "Resumed", + ); + + // Verify contract is resumed + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(false); + }); + + it("should allow withdrawal requests after resuming", async () => { + // First pause and then resume the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + await triggerableWithdrawalsGateway.connect(admin).resume(); + + // Should be able to add withdrawal requests + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }); + }); + }); + + context("pauseFor", () => { + it("should revert if the sender does not have the PAUSE_ROLE", async () => { + await expect( + triggerableWithdrawalsGateway.connect(stranger).pauseFor(1000n), + ).to.be.revertedWithOZAccessControlError(stranger.address, PAUSE_ROLE); + }); + + it("should revert if the contract is already paused", async () => { + // First pause the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + + // Try to pause again + await expect(triggerableWithdrawalsGateway.connect(admin).pauseFor(500n)).to.be.revertedWithCustomError( + triggerableWithdrawalsGateway, + "ResumedExpected", + ); + }); + + it("should revert if pause duration is zero", async () => { + await expect(triggerableWithdrawalsGateway.connect(admin).pauseFor(0n)).to.be.revertedWithCustomError( + triggerableWithdrawalsGateway, + "ZeroPauseDuration", + ); + }); + + it("should pause the contract for the specified duration and emit Paused event", async () => { + await expect(triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n)) + .to.emit(triggerableWithdrawalsGateway, "Paused") + .withArgs(1000n); + + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + }); + + it("should pause the contract indefinitely with PAUSE_INFINITELY", async () => { + const pauseInfinitely = await triggerableWithdrawalsGateway.PAUSE_INFINITELY(); + + // Pause the contract indefinitely + await expect(triggerableWithdrawalsGateway.connect(admin).pauseFor(pauseInfinitely)) + .to.emit(triggerableWithdrawalsGateway, "Paused") + .withArgs(pauseInfinitely); + + // Verify contract is paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + + // Advance time significantly + await advanceChainTime(1_000_000_000n); + + // Contract should still be paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + }); + + it("should automatically resume after the pause duration passes", async () => { + // Pause the contract for 100 seconds + await triggerableWithdrawalsGateway.connect(admin).pauseFor(100n); + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Contract should be automatically resumed + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(false); + }); + }); + + context("pauseUntil", () => { + it("should revert if the sender does not have the PAUSE_ROLE", async () => { + const timestamp = await getCurrentBlockTimestamp(); + await expect( + triggerableWithdrawalsGateway.connect(stranger).pauseUntil(timestamp + 1000n), + ).to.be.revertedWithOZAccessControlError(stranger.address, PAUSE_ROLE); + }); + + it("should revert if the contract is already paused", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // First pause the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + + // Try to pause again with pauseUntil + await expect( + triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp + 500n), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ResumedExpected"); + }); + + it("should revert if timestamp is in the past", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // Try to pause until a past timestamp + await expect( + triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp - 100n), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "PauseUntilMustBeInFuture"); + }); + + it("should pause the contract until the specified timestamp and emit Paused event", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // Pause the contract until timestamp + 1000 + await expect(triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp + 1000n)).to.emit( + triggerableWithdrawalsGateway, + "Paused", + ); + + // Verify contract is paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + }); + + it("should pause the contract indefinitely with PAUSE_INFINITELY", async () => { + const pauseInfinitely = await triggerableWithdrawalsGateway.PAUSE_INFINITELY(); + + // Pause the contract indefinitely + await expect(triggerableWithdrawalsGateway.connect(admin).pauseUntil(pauseInfinitely)).to.emit( + triggerableWithdrawalsGateway, + "Paused", + ); + + // Verify contract is paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + + // Advance time significantly + await advanceChainTime(100000n); + + // Contract should still be paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + }); + + it("should automatically resume after the pause timestamp passes", async () => { + const timestamp = await getCurrentBlockTimestamp(); + + // Pause the contract until timestamp + 100 + await triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp + 100n); + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(true); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Contract should be automatically resumed + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(false); + }); + }); + + context("Interaction with addWithdrawalRequests", () => { + it("pauseFor: should prevent withdrawal requests immediately after pausing", async () => { + // Initially contract should be resumed + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(false); + + // Pause the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + + // Attempt to add withdrawal request should fail + await expect( + triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ResumedExpected"); + }); + + it("pauseUntil: should prevent withdrawal requests immediately after pausing", async () => { + // Initially contract should be resumed + expect(await triggerableWithdrawalsGateway.isPaused()).to.equal(false); + + // Pause the contract + const timestamp = await getCurrentBlockTimestamp(); + await triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp + 100n); + + // Attempt to add withdrawal request should fail + await expect( + triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ResumedExpected"); + }); + + it("pauseFor: should allow withdrawal requests immediately after resuming", async () => { + // Pause and then resume the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000n); + await triggerableWithdrawalsGateway.connect(admin).resume(); + + // Should be able to add withdrawal requests immediately + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }); + }); + + it("pauseUntil: should allow withdrawal requests immediately after resuming", async () => { + // Pause and then resume the contract + const timestamp = await getCurrentBlockTimestamp(); + await triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp + 100n); + await triggerableWithdrawalsGateway.connect(admin).resume(); + + // Should be able to add withdrawal requests immediately + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }); + }); + + it("pauseFor: should allow withdrawal requests after pause duration automatically expires", async () => { + // Pause for 100 seconds + await triggerableWithdrawalsGateway.connect(admin).pauseFor(100n); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Should be able to add withdrawal requests after pause expires + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }); + }); + + it("pauseUntil: should allow withdrawal requests after pause duration automatically expires", async () => { + // Pause for 100 seconds + const timestamp = await getCurrentBlockTimestamp(); + await triggerableWithdrawalsGateway.connect(admin).pauseUntil(timestamp + 100n); + + // Advance time by 101 seconds + await advanceChainTime(101n); + + // Should be able to add withdrawal requests after pause expires + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(createValidatorDataList(exitRequests), ZERO_ADDRESS, 0, { value: 4 }); + }); + }); + }); +}); diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 5d623deddf..852c29f482 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -13,25 +13,19 @@ import { WithdrawalVault__Harness, } from "typechain-types"; -import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, proxify, streccak } from "lib"; +import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, proxify } from "lib"; import { Snapshot } from "test/suite"; -import { advanceChainTime, getCurrentBlockTimestamp } from "../../../lib/time"; - import { encodeEIP7002Payload, findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; import { generateWithdrawalRequestPayload } from "./utils"; const PETRIFIED_VERSION = MAX_UINT256; -const ADD_WITHDRAWAL_REQUEST_ROLE = streccak("ADD_WITHDRAWAL_REQUEST_ROLE"); -const PAUSE_ROLE = streccak("PAUSE_ROLE"); -const RESUME_ROLE = streccak("RESUME_ROLE"); - describe("WithdrawalVault.sol", () => { let owner: HardhatEthersSigner; let treasury: HardhatEthersSigner; - let validatorsExitBus: HardhatEthersSigner; + let triggerableWithdrawalsGateway: HardhatEthersSigner; let stranger: HardhatEthersSigner; let originalState: string; @@ -46,7 +40,7 @@ describe("WithdrawalVault.sol", () => { let vaultAddress: string; before(async () => { - [owner, treasury, validatorsExitBus, stranger] = await ethers.getSigners(); + [owner, treasury, triggerableWithdrawalsGateway, stranger] = await ethers.getSigners(); withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); @@ -55,7 +49,11 @@ describe("WithdrawalVault.sol", () => { lido = await ethers.deployContract("Lido__MockForWithdrawalVault"); lidoAddress = await lido.getAddress(); - impl = await ethers.deployContract("WithdrawalVault__Harness", [lidoAddress, treasury.address], owner); + impl = await ethers.deployContract( + "WithdrawalVault__Harness", + [lidoAddress, treasury.address, triggerableWithdrawalsGateway.address], + owner, + ); [vault] = await proxify({ impl, admin: owner }); vaultAddress = await vault.getAddress(); @@ -68,15 +66,24 @@ describe("WithdrawalVault.sol", () => { context("Constructor", () => { it("Reverts if the Lido address is zero", async () => { await expect( - ethers.deployContract("WithdrawalVault", [ZeroAddress, treasury.address]), + ethers.deployContract("WithdrawalVault", [ + ZeroAddress, + treasury.address, + triggerableWithdrawalsGateway.address, + ]), ).to.be.revertedWithCustomError(vault, "ZeroAddress"); }); it("Reverts if the treasury address is zero", async () => { - await expect(ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress])).to.be.revertedWithCustomError( - vault, - "ZeroAddress", - ); + await expect( + ethers.deployContract("WithdrawalVault", [lidoAddress, ZeroAddress, triggerableWithdrawalsGateway.address]), + ).to.be.revertedWithCustomError(vault, "ZeroAddress"); + }); + + it("Reverts if the triggerable withdrawal gateway address is zero", async () => { + await expect( + ethers.deployContract("WithdrawalVault", [lidoAddress, treasury.address, ZeroAddress]), + ).to.be.revertedWithCustomError(vault, "ZeroAddress"); }); it("Sets initial properties", async () => { @@ -95,45 +102,27 @@ describe("WithdrawalVault.sol", () => { context("initialize", () => { it("Should revert if the contract is already initialized", async () => { - await vault.initialize(owner); + await vault.initialize(); - await expect(vault.initialize(owner)) - .to.be.revertedWithCustomError(vault, "UnexpectedContractVersion") - .withArgs(2, 0); + await expect(vault.initialize()).to.be.revertedWithCustomError(vault, "UnexpectedContractVersion").withArgs(2, 0); }); it("Initializes the contract", async () => { - await expect(vault.initialize(owner)).to.emit(vault, "ContractVersionSet").withArgs(2); - }); - - it("Should revert if admin address is zero", async () => { - await expect(vault.initialize(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); - }); - - it("Should set admin role during initialization", async () => { - const adminRole = await vault.DEFAULT_ADMIN_ROLE(); - expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); - expect(await vault.hasRole(adminRole, owner)).to.equal(false); - - await vault.initialize(owner); - - expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); - expect(await vault.hasRole(adminRole, owner)).to.equal(true); - expect(await vault.hasRole(adminRole, stranger)).to.equal(false); + await expect(vault.initialize()).to.emit(vault, "ContractVersionSet").withArgs(2); }); }); context("finalizeUpgrade_v2()", () => { it("Should revert with UnexpectedContractVersion error when called on implementation", async () => { - await expect(impl.finalizeUpgrade_v2(owner)) + await expect(impl.finalizeUpgrade_v2()) .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") .withArgs(MAX_UINT256, 1); }); it("Should revert with UnexpectedContractVersion error when called on deployed from scratch WithdrawalVaultV2", async () => { - await vault.initialize(owner); + await vault.initialize(); - await expect(vault.finalizeUpgrade_v2(owner)) + await expect(vault.finalizeUpgrade_v2()) .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") .withArgs(2, 1); }); @@ -143,50 +132,16 @@ describe("WithdrawalVault.sol", () => { await vault.harness__initializeContractVersionTo(1); }); - it("Should revert if admin address is zero", async () => { - await expect(vault.finalizeUpgrade_v2(ZeroAddress)).to.be.revertedWithCustomError(vault, "ZeroAddress"); - }); - it("Should set correct contract version", async () => { expect(await vault.getContractVersion()).to.equal(1); - await vault.finalizeUpgrade_v2(owner); + await vault.finalizeUpgrade_v2(); expect(await vault.getContractVersion()).to.be.equal(2); }); - - it("Should set admin role during finalization", async () => { - const adminRole = await vault.DEFAULT_ADMIN_ROLE(); - expect(await vault.getRoleMemberCount(adminRole)).to.equal(0); - expect(await vault.hasRole(adminRole, owner)).to.equal(false); - - await vault.finalizeUpgrade_v2(owner); - - expect(await vault.getRoleMemberCount(adminRole)).to.equal(1); - expect(await vault.hasRole(adminRole, owner)).to.equal(true); - expect(await vault.hasRole(adminRole, stranger)).to.equal(false); - }); - }); - }); - - context("Access control", () => { - it("Returns ACL roles", async () => { - expect(await vault.ADD_WITHDRAWAL_REQUEST_ROLE()).to.equal(ADD_WITHDRAWAL_REQUEST_ROLE); - }); - - it("Sets up roles", async () => { - await vault.initialize(owner); - - expect(await vault.getRoleMemberCount(ADD_WITHDRAWAL_REQUEST_ROLE)).to.equal(0); - expect(await vault.hasRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(false); - - await vault.connect(owner).grantRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); - - expect(await vault.getRoleMemberCount(ADD_WITHDRAWAL_REQUEST_ROLE)).to.equal(1); - expect(await vault.hasRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus)).to.equal(true); }); }); context("withdrawWithdrawals", () => { - beforeEach(async () => await vault.initialize(owner)); + beforeEach(async () => await vault.initialize()); it("Reverts if the caller is not Lido", async () => { await expect(vault.connect(stranger).withdrawWithdrawals(0)).to.be.revertedWithCustomError(vault, "NotLido"); @@ -311,18 +266,18 @@ describe("WithdrawalVault.sol", () => { context("add triggerable withdrawal requests", () => { beforeEach(async () => { - await vault.initialize(owner); - await vault.connect(owner).grantRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); + await vault.initialize(); }); - it("Should revert if the caller is not Validator Exit Bus", async () => { - await expect( - vault.connect(stranger).addWithdrawalRequests(["0x1234"], [1n]), - ).to.be.revertedWithOZAccessControlError(stranger.address, ADD_WITHDRAWAL_REQUEST_ROLE); + it("Should revert if the caller is not Triggerable Withdrawal Gateway", async () => { + await expect(vault.connect(stranger).addWithdrawalRequests(["0x1234"], [1n])).to.be.revertedWithCustomError( + vault, + "NotTriggerableWithdrawalsGateway", + ); }); it("Should revert if empty arrays are provided", async function () { - await expect(vault.connect(validatorsExitBus).addWithdrawalRequests([], [], { value: 1n })) + await expect(vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests([], [], { value: 1n })) .to.be.revertedWithCustomError(vault, "ZeroArgument") .withArgs("pubkeys"); }); @@ -335,13 +290,17 @@ describe("WithdrawalVault.sol", () => { const totalWithdrawalFee = (await getFee()) * BigInt(requestCount); await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, amounts, { value: totalWithdrawalFee }), + vault + .connect(triggerableWithdrawalsGateway) + .addWithdrawalRequests(pubkeysHexArray, amounts, { value: totalWithdrawalFee }), ) .to.be.revertedWithCustomError(vault, "ArraysLengthMismatch") .withArgs(requestCount, amounts.length); await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, [], { value: totalWithdrawalFee }), + vault + .connect(triggerableWithdrawalsGateway) + .addWithdrawalRequests(pubkeysHexArray, [], { value: totalWithdrawalFee }), ) .to.be.revertedWithCustomError(vault, "ArraysLengthMismatch") .withArgs(requestCount, 0); @@ -353,7 +312,9 @@ describe("WithdrawalVault.sol", () => { await withdrawalsPredeployed.mock__setFee(3n); // Set fee to 3 gwei // 1. Should revert if no fee is sent - await expect(vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts)) + await expect( + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts), + ) .to.be.revertedWithCustomError(vault, "IncorrectFee") .withArgs(0, 3n); @@ -361,7 +322,7 @@ describe("WithdrawalVault.sol", () => { const insufficientFee = 2n; await expect( vault - .connect(validatorsExitBus) + .connect(triggerableWithdrawalsGateway) .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: insufficientFee }), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") @@ -374,7 +335,9 @@ describe("WithdrawalVault.sol", () => { const fee = await getFee(); await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(invalidPubkeyHexString, [1n], { value: fee }), + vault + .connect(triggerableWithdrawalsGateway) + .addWithdrawalRequests(invalidPubkeyHexString, [1n], { value: fee }), ).to.be.revertedWithPanic(1); // assertion }); @@ -387,7 +350,7 @@ describe("WithdrawalVault.sol", () => { const fee = (await getFee()) * 2n; // 2 requests await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, [1n, 2n], { value: fee }), + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, [1n, 2n], { value: fee }), ).to.be.revertedWithPanic(1); // assertion }); @@ -399,7 +362,9 @@ describe("WithdrawalVault.sol", () => { await withdrawalsPredeployed.mock__setFailOnAddRequest(true); await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), + vault + .connect(triggerableWithdrawalsGateway) + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), ).to.be.revertedWithCustomError(vault, "RequestAdditionFailed"); }); @@ -410,7 +375,9 @@ describe("WithdrawalVault.sol", () => { const fee = 10n; await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), + vault + .connect(triggerableWithdrawalsGateway) + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), ).to.be.revertedWithCustomError(vault, "FeeReadFailed"); }); @@ -424,7 +391,7 @@ describe("WithdrawalVault.sol", () => { await expect( vault - .connect(validatorsExitBus) + .connect(triggerableWithdrawalsGateway) .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: withdrawalFee }), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") @@ -440,7 +407,7 @@ describe("WithdrawalVault.sol", () => { await expect( vault - .connect(validatorsExitBus) + .connect(triggerableWithdrawalsGateway) .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: fee }), ).to.be.revertedWithCustomError(vault, "FeeInvalidData"); }); @@ -456,7 +423,7 @@ describe("WithdrawalVault.sol", () => { await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -471,7 +438,7 @@ describe("WithdrawalVault.sol", () => { await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedLargeTotalWithdrawalFee, }), pubkeys, @@ -489,7 +456,7 @@ describe("WithdrawalVault.sol", () => { const expectedTotalWithdrawalFee = 9n; // 3 requests * 3 gwei (fee) = 9 gwei await expect( - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), ) @@ -513,7 +480,7 @@ describe("WithdrawalVault.sol", () => { await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -534,7 +501,7 @@ describe("WithdrawalVault.sol", () => { const initialBalance = await getWithdrawalsPredeployedContractBalance(); await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { + vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedTotalWithdrawalFee, }), pubkeys, @@ -550,7 +517,7 @@ describe("WithdrawalVault.sol", () => { const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); const tx = await vault - .connect(validatorsExitBus) + .connect(triggerableWithdrawalsGateway) .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: 16n }); const receipt = await tx.wait(); @@ -584,20 +551,22 @@ describe("WithdrawalVault.sol", () => { const expectedTotalWithdrawalFee = expectedFee * BigInt(requestCount); const initialBalance = await getWithdrawalCredentialsContractBalance(); - const vebInitialBalance = await ethers.provider.getBalance(validatorsExitBus.address); + const vebInitialBalance = await ethers.provider.getBalance(triggerableWithdrawalsGateway.address); const { receipt: receiptPartialWithdrawal } = await testEIP7002Mock( () => - vault.connect(validatorsExitBus).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { - value: expectedTotalWithdrawalFee, - }), + vault + .connect(triggerableWithdrawalsGateway) + .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { + value: expectedTotalWithdrawalFee, + }), pubkeys, mixedWithdrawalAmounts, expectedFee, ); expect(await getWithdrawalCredentialsContractBalance()).to.equal(initialBalance); - expect(await ethers.provider.getBalance(validatorsExitBus.address)).to.equal( + expect(await ethers.provider.getBalance(triggerableWithdrawalsGateway.address)).to.equal( vebInitialBalance - expectedTotalWithdrawalFee - receiptPartialWithdrawal.gasUsed * receiptPartialWithdrawal.gasPrice, @@ -605,324 +574,4 @@ describe("WithdrawalVault.sol", () => { }); }); }); - - context("pausable until", () => { - beforeEach(async () => { - // Initialize the vault and set up necessary roles - await vault.initialize(owner); - await vault.connect(owner).grantRole(ADD_WITHDRAWAL_REQUEST_ROLE, validatorsExitBus); - await vault.connect(owner).grantRole(PAUSE_ROLE, owner); - await vault.connect(owner).grantRole(RESUME_ROLE, owner); - }); - - context("resume", () => { - it("should revert if the sender does not have the RESUME_ROLE", async () => { - // First pause the contract - await vault.connect(owner).pauseFor(1000n); - - // Try to resume without the RESUME_ROLE - await expect(vault.connect(stranger).resume()).to.be.revertedWithOZAccessControlError( - stranger.address, - RESUME_ROLE, - ); - }); - - it("should revert if the contract is not paused", async () => { - // Contract is initially not paused - await expect(vault.connect(owner).resume()).to.be.revertedWithCustomError(vault, "PausedExpected"); - }); - - it("should resume the contract when paused and emit Resumed event", async () => { - // First pause the contract - await vault.connect(owner).pauseFor(1000n); - expect(await vault.isPaused()).to.equal(true); - - // Resume the contract - await expect(vault.connect(owner).resume()).to.emit(vault, "Resumed"); - - // Verify contract is resumed - expect(await vault.isPaused()).to.equal(false); - }); - - it("should allow withdrawal requests after resuming", async () => { - const requestCount = 1; - const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // First pause and then resume the contract - await vault.connect(owner).pauseFor(1000n); - await vault.connect(owner).resume(); - - // Should be able to add withdrawal requests - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - pubkeys, - mixedWithdrawalAmounts, - expectedFee, - ); - }); - }); - - context("pauseFor", () => { - it("should revert if the sender does not have the PAUSE_ROLE", async () => { - await expect(vault.connect(stranger).pauseFor(1000n)).to.be.revertedWithOZAccessControlError( - stranger.address, - PAUSE_ROLE, - ); - }); - - it("should revert if the contract is already paused", async () => { - // First pause the contract - await vault.connect(owner).pauseFor(1000n); - - // Try to pause again - await expect(vault.connect(owner).pauseFor(500n)).to.be.revertedWithCustomError(vault, "ResumedExpected"); - }); - - it("should revert if pause duration is zero", async () => { - await expect(vault.connect(owner).pauseFor(0n)).to.be.revertedWithCustomError(vault, "ZeroPauseDuration"); - }); - - it("should pause the contract for the specified duration and emit Paused event", async () => { - await expect(vault.connect(owner).pauseFor(1000n)).to.emit(vault, "Paused").withArgs(1000n); - - expect(await vault.isPaused()).to.equal(true); - }); - - it("should pause the contract indefinitely with PAUSE_INFINITELY", async () => { - const pauseInfinitely = await vault.PAUSE_INFINITELY(); - - // Pause the contract indefinitely - await expect(vault.connect(owner).pauseFor(pauseInfinitely)).to.emit(vault, "Paused").withArgs(pauseInfinitely); - - // Verify contract is paused - expect(await vault.isPaused()).to.equal(true); - - // Advance time significantly - await advanceChainTime(1_000_000_000n); - - // Contract should still be paused - expect(await vault.isPaused()).to.equal(true); - }); - - it("should automatically resume after the pause duration passes", async () => { - // Pause the contract for 100 seconds - await vault.connect(owner).pauseFor(100n); - expect(await vault.isPaused()).to.equal(true); - - // Advance time by 101 seconds - await advanceChainTime(101n); - - // Contract should be automatically resumed - expect(await vault.isPaused()).to.equal(false); - }); - }); - - context("pauseUntil", () => { - it("should revert if the sender does not have the PAUSE_ROLE", async () => { - const timestamp = await getCurrentBlockTimestamp(); - await expect(vault.connect(stranger).pauseUntil(timestamp + 1000n)).to.be.revertedWithOZAccessControlError( - stranger.address, - PAUSE_ROLE, - ); - }); - - it("should revert if the contract is already paused", async () => { - const timestamp = await getCurrentBlockTimestamp(); - - // First pause the contract - await vault.connect(owner).pauseFor(1000n); - - // Try to pause again with pauseUntil - await expect(vault.connect(owner).pauseUntil(timestamp + 500n)).to.be.revertedWithCustomError( - vault, - "ResumedExpected", - ); - }); - - it("should revert if timestamp is in the past", async () => { - const timestamp = await getCurrentBlockTimestamp(); - - // Try to pause until a past timestamp - await expect(vault.connect(owner).pauseUntil(timestamp - 100n)).to.be.revertedWithCustomError( - vault, - "PauseUntilMustBeInFuture", - ); - }); - - it("should pause the contract until the specified timestamp and emit Paused event", async () => { - const timestamp = await getCurrentBlockTimestamp(); - - // Pause the contract until timestamp + 1000 - await expect(vault.connect(owner).pauseUntil(timestamp + 1000n)).to.emit(vault, "Paused"); - - // Verify contract is paused - expect(await vault.isPaused()).to.equal(true); - }); - - it("should pause the contract indefinitely with PAUSE_INFINITELY", async () => { - const pauseInfinitely = await vault.PAUSE_INFINITELY(); - - // Pause the contract indefinitely - await expect(vault.connect(owner).pauseUntil(pauseInfinitely)).to.emit(vault, "Paused"); - - // Verify contract is paused - expect(await vault.isPaused()).to.equal(true); - - // Advance time significantly - await advanceChainTime(100000n); - - // Contract should still be paused - expect(await vault.isPaused()).to.equal(true); - }); - - it("should automatically resume after the pause timestamp passes", async () => { - const timestamp = await getCurrentBlockTimestamp(); - - // Pause the contract until timestamp + 100 - await vault.connect(owner).pauseUntil(timestamp + 100n); - expect(await vault.isPaused()).to.equal(true); - - // Advance time by 101 seconds - await advanceChainTime(101n); - - // Contract should be automatically resumed - expect(await vault.isPaused()).to.equal(false); - }); - }); - - context("Interaction with addWithdrawalRequests", () => { - it("pauseFor: should prevent withdrawal requests immediately after pausing", async () => { - const requestCount = 1; - const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Initially contract should be resumed - expect(await vault.isPaused()).to.equal(false); - - // Pause the contract - await vault.connect(owner).pauseFor(1000n); - - // Attempt to add withdrawal request should fail - await expect( - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - ).to.be.revertedWithCustomError(vault, "ResumedExpected"); - }); - - it("pauseUntil: should prevent withdrawal requests immediately after pausing", async () => { - const requestCount = 1; - const { pubkeysHexArray, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Initially contract should be resumed - expect(await vault.isPaused()).to.equal(false); - - // Pause the contract - const timestamp = await getCurrentBlockTimestamp(); - await vault.connect(owner).pauseUntil(timestamp + 100n); - - // Attempt to add withdrawal request should fail - await expect( - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - ).to.be.revertedWithCustomError(vault, "ResumedExpected"); - }); - - it("pauseFor: should allow withdrawal requests immediately after resuming", async () => { - const requestCount = 1; - const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Pause and then resume the contract - await vault.connect(owner).pauseFor(1000n); - await vault.connect(owner).resume(); - - // Should be able to add withdrawal requests immediately - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - pubkeys, - mixedWithdrawalAmounts, - expectedFee, - ); - }); - - it("pauseUntil: should allow withdrawal requests immediately after resuming", async () => { - const requestCount = 1; - const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Pause and then resume the contract - const timestamp = await getCurrentBlockTimestamp(); - await vault.connect(owner).pauseUntil(timestamp + 100n); - await vault.connect(owner).resume(); - - // Should be able to add withdrawal requests immediately - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - pubkeys, - mixedWithdrawalAmounts, - expectedFee, - ); - }); - - it("pauseFor: should allow withdrawal requests after pause duration automatically expires", async () => { - const requestCount = 1; - const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Pause for 100 seconds - await vault.connect(owner).pauseFor(100n); - - // Advance time by 101 seconds - await advanceChainTime(101n); - - // Should be able to add withdrawal requests after pause expires - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - pubkeys, - mixedWithdrawalAmounts, - expectedFee, - ); - }); - - it("pauseUntil: should allow withdrawal requests after pause duration automatically expires", async () => { - const requestCount = 1; - const { pubkeysHexArray, pubkeys, mixedWithdrawalAmounts } = generateWithdrawalRequestPayload(requestCount); - const expectedFee = await getFee(); - - // Pause for 100 seconds - const timestamp = await getCurrentBlockTimestamp(); - await vault.connect(owner).pauseUntil(timestamp + 100n); - - // Advance time by 101 seconds - await advanceChainTime(101n); - - // Should be able to add withdrawal requests after pause expires - await testEIP7002Mock( - () => - vault - .connect(validatorsExitBus) - .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: expectedFee }), - pubkeys, - mixedWithdrawalAmounts, - expectedFee, - ); - }); - }); - }); }); From 03ab49499319b1aebfef8d6fa4565fc93a3682c6 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Thu, 29 May 2025 23:29:38 +0200 Subject: [PATCH 229/405] fix: merge conflict fix --- contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 6bc9c3dee0..8df8714ce8 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -178,7 +178,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil ValidatorData[] calldata validatorsData, address refundRecipient, uint256 exitType - ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance { + ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance whenResumed { if (msg.value == 0) revert ZeroArgument("msg.value"); uint256 requestsCount = validatorsData.length; if (requestsCount == 0) revert ZeroArgument("validatorsData"); From a2dd3d46dc778e6b70d3f14d29433d51317772c3 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 10:56:02 +0400 Subject: [PATCH 230/405] fix: _consumeLimit --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 39debea2bb..e29149cf2d 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -546,10 +546,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V emit ExitRequestsLimitSet(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } - function _consumeLimit(uint256 requestsCount) internal returns (uint256 requestsLimitedCount) { + function _consumeLimit(uint256 requestsCount) internal { ExitRequestLimitData memory exitRequestLimitData = EXIT_REQUEST_LIMIT_POSITION.getStorageExitRequestLimit(); if (!exitRequestLimitData.isExitLimitSet()) { - return requestsCount; + return; } uint256 limit = exitRequestLimitData.calculateCurrentExitLimit(_getTimestamp()); From 4350459b594aa86e0df5a7410dcaf76919dc4ffb Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 11:54:08 +0400 Subject: [PATCH 231/405] fix: refund test --- .../validators-exit-bus-trigger-exits.ts | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts index d4d061808c..29a90c4bd4 100644 --- a/test/integration/validators-exit-bus-trigger-exits.ts +++ b/test/integration/validators-exit-bus-trigger-exits.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import { ZeroAddress } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; @@ -96,7 +97,43 @@ describe("ValidatorsExitBus integration", () => { expect(deliveryHistory.length).to.equal(1); expect(deliveryHistory[0].lastDeliveredExitDataIndex).to.equal(0); - await expect(veb.triggerExits(exitRequest, [0], refundRecipient, {value: 10})) - .to.emit(wv, "WithdrawalRequestAdded"); + const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); + + const triggerExitsTx = await veb + .connect(refundRecipient) + .triggerExits(exitRequest, [0], ZeroAddress, { value: 10 }); + + await expect(triggerExitsTx).to.emit(wv, "WithdrawalRequestAdded"); + + const ethAfter = await ethers.provider.getBalance(refundRecipient.getAddress()); + + const fee = await wv.getWithdrawalRequestFee(); + + const txReceipt = await triggerExitsTx.wait(); + const gasUsed = BigInt(txReceipt!.gasUsed) * txReceipt!.gasPrice; + + expect(ethAfter).to.equal(ethBefore - gasUsed - fee); + }); + + it("should handle non-zero refundRecipient and refund the correct amount", async () => { + const dataFormat = 1; + + const exitRequest = { dataFormat, data: exitRequestPacked }; + + const exitRequestsHash = hashExitRequest(exitRequest); + + await veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash); + await veb.submitExitRequestsData(exitRequest); + + const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); + + await veb.triggerExits(exitRequest, [0], refundRecipient.getAddress(), { value: 10 }); + + const fee = await wv.getWithdrawalRequestFee(); + + const ethAfter = await ethers.provider.getBalance(refundRecipient.getAddress()); + + // Should be increased by (10 - fee) + expect(ethAfter - ethBefore).to.equal(10n - fee); }); }); From 106a06d09fce73d20fc3e8d7374f76a58dfa1ccf Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 30 May 2025 10:44:54 +0200 Subject: [PATCH 232/405] refactor: clean-up withdrawal vault --- contracts/0.8.9/WithdrawalVault.sol | 8 ++++++++ contracts/0.8.9/WithdrawalVaultEIP7002.sol | 14 +++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 8bc645cb1f..3814dc6cac 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -173,4 +173,12 @@ contract WithdrawalVault is Versioned, WithdrawalVaultEIP7002 { _addWithdrawalRequests(pubkeys, amounts); } + + /** + * @dev Retrieves the current EIP-7002 withdrawal fee. + * @return The minimum fee required per withdrawal request. + */ + function getWithdrawalRequestFee() public view returns (uint256) { + return _getWithdrawalRequestFee(); + } } diff --git a/contracts/0.8.9/WithdrawalVaultEIP7002.sol b/contracts/0.8.9/WithdrawalVaultEIP7002.sol index 967df4477b..f4ff11da3c 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7002.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7002.sol @@ -19,20 +19,12 @@ abstract contract WithdrawalVaultEIP7002 { error IncorrectFee(uint256 providedFee, uint256 requiredFee); error RequestAdditionFailed(bytes callData); - /** - * @dev Retrieves the current EIP-7002 withdrawal fee. - * @return The minimum fee required per withdrawal request. - */ - function getWithdrawalRequestFee() public view returns (uint256) { - return _getRequestFee(WITHDRAWAL_REQUEST); - } - function _addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) internal { uint256 requestsCount = pubkeys.length; if (requestsCount == 0) revert ZeroArgument("pubkeys"); if (requestsCount != amounts.length) revert ArraysLengthMismatch(requestsCount, amounts.length); - uint256 fee = getWithdrawalRequestFee(); + uint256 fee = _getWithdrawalRequestFee(); _checkFee(requestsCount * fee); for (uint256 i = 0; i < requestsCount; ++i) { @@ -40,8 +32,8 @@ abstract contract WithdrawalVaultEIP7002 { } } - function _getRequestFee(address requestedContract) internal view returns (uint256) { - (bool success, bytes memory feeData) = requestedContract.staticcall(""); + function _getWithdrawalRequestFee() internal view returns (uint256) { + (bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall(""); if (!success) { revert FeeReadFailed(); From c2dda5536c99d5f9d46b0c5621209be9405e991d Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 15:10:11 +0400 Subject: [PATCH 233/405] fix: MAX_VALIDATORS_PER_BATCH -> MAX_VALIDATORS_PER_REPORT --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 46 +++++++++---------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 12 ++--- .../validator-exit-bus-oracle.deploy.test.ts | 4 +- ...idator-exit-bus-oracle.finalize_v2.test.ts | 2 +- ...-bus-oracle.submitExitRequestsData.test.ts | 30 +++++------- 5 files changed, 44 insertions(+), 50 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index e29149cf2d..310ef2d193 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -100,10 +100,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V error RequestsNotDelivered(); /** - * @notice Thrown when exit requests in report exceed the maximum allowed number of requests per batch. + * @notice Thrown when exit requests in report exceed the maximum allowed number of requests per report. * @param requestsCount Amount of requests that were sent for processing */ - error ToManyExitRequestsInReport(uint256 requestsCount, uint256 maxRequestsPerBatch); + error ToManyExitRequestsInReport(uint256 requestsCount, uint256 maxRequestsPerReport); /// @dev Events @@ -166,8 +166,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); /// @notice An ACL role granting the permission to resume accepting validator exit requests bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - /// @notice An ACL role granting the permission to set MAX_VALIDATORS_PER_BATCH value - bytes32 public constant MAX_VALIDATORS_PER_BATCH_ROLE = keccak256("MAX_VALIDATORS_PER_BATCH_ROLE"); + /// @notice An ACL role granting the permission to set MAX_VALIDATORS_PER_REPORT value + bytes32 public constant MAX_VALIDATORS_PER_REPORT_ROLE = keccak256("MAX_VALIDATORS_PER_REPORT_ROLE"); /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; @@ -196,9 +196,9 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V // Storage slot for exit request limit configuration and current quota tracking bytes32 internal constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); - // Storage slot for the maximum number of validator exit requests allowed per processing batch - bytes32 internal constant MAX_VALIDATORS_PER_BATCH_POSITION = - keccak256("lido.ValidatorsExitBus.maxValidatorsPerBatch"); + // Storage slot for the maximum number of validator exit requests allowed per processing report + bytes32 internal constant MAX_VALIDATORS_PER_REPORT_POSITION = + keccak256("lido.ValidatorsExitBus.maxValidatorsPerReport"); // Storage slot for mapping(bytes32 => RequestStatus), keyed by exitRequestsHash bytes32 internal constant REQUEST_STATUS_POSITION = keccak256("lido.ValidatorsExitBus.requestStatus"); @@ -259,10 +259,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V _checkContractVersion(requestStatus.contractVersion); uint256 requestsCount = request.data.length / PACKED_REQUEST_LENGTH; - uint256 maxRequestsPerBatch = _getMaxRequestsPerBatch(); + uint256 maxRequestsPerReport = _getMaxValidatorsPerReport(); - if (requestsCount > maxRequestsPerBatch) { - revert ToManyExitRequestsInReport(requestsCount, maxRequestsPerBatch); + if (requestsCount > maxRequestsPerReport) { + revert ToManyExitRequestsInReport(requestsCount, maxRequestsPerReport); } _consumeLimit(requestsCount); @@ -387,19 +387,19 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } /** - * @notice Sets the maximum allowed number of validator exit requests to process in a single batch. - * @param maxRequests The new maximum number of exit requests allowed per batch. + * @notice Sets the maximum allowed number of validator exit requests to process in a single report. + * @param maxRequests The new maximum number of exit requests allowed per report. */ - function setMaxRequestsPerBatch(uint256 maxRequests) external onlyRole(MAX_VALIDATORS_PER_BATCH_ROLE) { - _setMaxRequestsPerBatch(maxRequests); + function setMaxValidatorsPerReport(uint256 maxRequests) external onlyRole(MAX_VALIDATORS_PER_REPORT_ROLE) { + _setMaxValidatorsPerReport(maxRequests); } /** - * @notice Returns information about allowed number of validator exit requests to process in a single batch. - * @return The new maximum number of exit requests allowed per batch + * @notice Returns information about allowed number of validator exit requests to process in a single report. + * @return The new maximum number of exit requests allowed per report */ - function getMaxRequestsPerBatch() external view returns (uint256) { - return _getMaxRequestsPerBatch(); + function getMaxValidatorsPerReport() external view returns (uint256) { + return _getMaxValidatorsPerReport(); } /** @@ -517,14 +517,14 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V return block.timestamp; // solhint-disable-line not-rely-on-time } - function _setMaxRequestsPerBatch(uint256 value) internal { - require(value > 0, "MAX_BATCH_SIZE_ZERO"); + function _setMaxValidatorsPerReport(uint256 value) internal { + require(value > 0, "ZERO_MAX_VALIDATORS_PER_REPORT"); - MAX_VALIDATORS_PER_BATCH_POSITION.setStorageUint256(value); + MAX_VALIDATORS_PER_REPORT_POSITION.setStorageUint256(value); } - function _getMaxRequestsPerBatch() internal view returns (uint256) { - return MAX_VALIDATORS_PER_BATCH_POSITION.getStorageUint256(); + function _getMaxValidatorsPerReport() internal view returns (uint256) { + return MAX_VALIDATORS_PER_REPORT_POSITION.getStorageUint256(); } function _setExitRequestLimit( diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index f0a98325a0..535e7eadd9 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -64,7 +64,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { address consensusContract, uint256 consensusVersion, uint256 lastProcessingRefSlot, - uint256 maxValidatorsPerBatch, + uint256 maxValidatorsPerRequest, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec @@ -75,7 +75,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { _pauseFor(PAUSE_INFINITELY); _initialize(consensusContract, consensusVersion, lastProcessingRefSlot); - _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); + _initialize_v2(maxValidatorsPerRequest, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } /** @@ -84,22 +84,22 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { * For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md */ function finalizeUpgrade_v2( - uint256 maxValidatorsPerBatch, + uint256 maxValidatorsPerReport, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec ) external { - _initialize_v2(maxValidatorsPerBatch, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); + _initialize_v2(maxValidatorsPerReport, maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } function _initialize_v2( - uint256 maxValidatorsPerBatch, + uint256 maxValidatorsPerReport, uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec ) internal { _updateContractVersion(2); - _setMaxRequestsPerBatch(maxValidatorsPerBatch); + _setMaxValidatorsPerReport(maxValidatorsPerReport); _setExitRequestLimit(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec); } diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts index 7071a02e6a..61df2b7175 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.deploy.test.ts @@ -23,7 +23,7 @@ describe("ValidatorsExitBusOracle.sol:deploy", () => { it("initialize reverts if admin address is zero", async () => { const deployed = await deployVEBO(admin.address); - const maxValidatorsPerBatch = 50; + const maxValidatorsPerReport = 50; const maxExitRequestsLimit = 100; const exitsPerFrame = 1; const frameDuration = 48; @@ -34,7 +34,7 @@ describe("ValidatorsExitBusOracle.sol:deploy", () => { await deployed.consensus.getAddress(), CONSENSUS_VERSION, 0, - maxValidatorsPerBatch, + maxValidatorsPerReport, maxExitRequestsLimit, exitsPerFrame, frameDuration, diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts index 7b7db88feb..0a048438bf 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.finalize_v2.test.ts @@ -58,7 +58,7 @@ describe("ValidatorsExitBusOracle.sol:finalizeUpgrade_v2", () => { expect(exitRequestLimitData.exitsPerFrame).to.equal(1); expect(exitRequestLimitData.frameDurationInSec).to.equal(48); - expect(await oracle.getMaxRequestsPerBatch()).to.equal(15); + expect(await oracle.getMaxValidatorsPerReport()).to.equal(15); // should not allow to run finalizeUpgrade_v2 again await expect(oracle.finalizeUpgrade_v2(10, 100, 1, 48)).to.be.revertedWithCustomError( diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index a3b704d064..fb2c496642 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -417,40 +417,34 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); - it("Should not give to set new maximum requests per batch value without MAX_VALIDATORS_PER_BATCH_ROLE role", async () => { - const maxRequestsPerBatch = 4; + it("Should not give to set new maximum requests per report value without MAX_VALIDATORS_PER_REPORT_ROLE role", async () => { + const maxRequestsPerReport = 4; await expect( - oracle.connect(stranger).setMaxRequestsPerBatch(maxRequestsPerBatch), + oracle.connect(stranger).setMaxValidatorsPerReport(maxRequestsPerReport), ).to.be.revertedWithOZAccessControlError( await stranger.getAddress(), - await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(), + await oracle.MAX_VALIDATORS_PER_REPORT_ROLE(), ); }); - it("Should not allow to set new maximum requests per batch value eq to 0", async () => { - const role = await oracle.MAX_VALIDATORS_PER_BATCH_ROLE(); + it("Should not allow to set new maximum requests per report value eq to 0", async () => { + const role = await oracle.MAX_VALIDATORS_PER_REPORT_ROLE(); await oracle.grantRole(role, authorizedEntity); - await expect(oracle.connect(authorizedEntity).setMaxRequestsPerBatch(0)).to.be.revertedWith( - "MAX_BATCH_SIZE_ZERO", + await expect(oracle.connect(authorizedEntity).setMaxValidatorsPerReport(0)).to.be.revertedWith( + "ZERO_MAX_VALIDATORS_PER_REPORT", ); }); - it("Should not allow to ", async () => { - // if (totalItemsCount > maxRequestsPerBatch) { - // revert ToManyExitRequestsInReport(totalItemsCount, maxRequestsPerBatch); - // } - }); - - it("Should not allow to process request larger than MAX_VALIDATORS_PER_BATCH", async () => { + it("Should not allow to process request larger than MAX_VALIDATORS_PER_REPORT", async () => { await consensus.advanceTimeBy(MAX_EXIT_REQUESTS_LIMIT * 4 * 12); const data = await oracle.getExitRequestLimitFullInfo(); expect(data.currentExitRequestsLimit).to.equal(MAX_EXIT_REQUESTS_LIMIT); - const maxRequestsPerBatch = 4; + const maxRequestsPerReport = 4; - await oracle.connect(authorizedEntity).setMaxRequestsPerBatch(maxRequestsPerBatch); - expect(await oracle.connect(authorizedEntity).getMaxRequestsPerBatch()).to.equal(maxRequestsPerBatch); + await oracle.connect(authorizedEntity).setMaxValidatorsPerReport(maxRequestsPerReport); + expect(await oracle.connect(authorizedEntity).getMaxValidatorsPerReport()).to.equal(maxRequestsPerReport); const exitRequestsRandom = [ { moduleId: 100, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, From 09f469736699290ffd52b02d77d6f0381ac2c934 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 15:24:58 +0400 Subject: [PATCH 234/405] fix: dont limit oracle by common veb limit --- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 -- ...r-exit-bus-oracle.submitReportData.test.ts | 20 +------------------ 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index f0a98325a0..6bac04de1f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -254,8 +254,6 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()).checkExitBusOracleReport(data.requestsCount); - // Check VEB common limit - _consumeLimit(data.requestsCount); _processExitRequestsList(data.data, 0, data.requestsCount); _storageDataProcessingState().value = DataProcessingState({ diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 770bff2bd0..8f2b1c6704 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -654,25 +654,7 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { .withArgs(requests[3].moduleId, requests[3].nodeOpId, requests[3].valIndex, requests[3].valPubkey, timestamp); }); - it("emits ValidatorExitRequest events", async () => { - const requests = [ - { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, - { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, - { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[2] }, - { moduleId: 2, nodeOpId: 3, valIndex: 4, valPubkey: PUBKEYS[4] }, - ]; - const { reportData } = await prepareReportAndSubmitHash(requests); - - await expect(oracle.connect(member1).submitReportData(reportData, oracleVersion)) - .to.be.revertedWithCustomError(oracle, "ExitRequestsLimitExceeded") - .withArgs(4, 3); - }); - - it("day passes", async () => { - await consensus.advanceTimeBy(24 * 60 * 60); - }); - - it("Limit regenerated in a day", async () => { + it("oracle doesnt consume common veb limits", async () => { const requests = [ { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, From 4a46733945d13745eaf6cb3af0c9dffe667699d1 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 16:50:52 +0400 Subject: [PATCH 235/405] fix: veb review refactoring --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 32 ++++++++----------- .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- .../contracts/ValidatorsExitBus__Harness.sol | 8 ++--- ...-bus-oracle.submitExitRequestsData.test.ts | 7 ++-- .../oracle/validator-exit-bus.helpers.test.ts | 13 ++------ 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 310ef2d193..c91668af1a 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -160,14 +160,12 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /// @notice An ACL role granting the permission to submit a hash of the exit requests data bytes32 public constant SUBMIT_REPORT_HASH_ROLE = keccak256("SUBMIT_REPORT_HASH_ROLE"); - /// @notice An ACL role granting the permission to set maximum exit request limit and the frame limit restoring values + /// @notice An ACL role granting the permission to set limits configs and MAX_VALIDATORS_PER_REPORT value bytes32 public constant EXIT_REQUEST_LIMIT_MANAGER_ROLE = keccak256("EXIT_REQUEST_LIMIT_MANAGER_ROLE"); /// @notice An ACL role granting the permission to pause accepting validator exit requests bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); /// @notice An ACL role granting the permission to resume accepting validator exit requests bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); - /// @notice An ACL role granting the permission to set MAX_VALIDATORS_PER_REPORT value - bytes32 public constant MAX_VALIDATORS_PER_REPORT_ROLE = keccak256("MAX_VALIDATORS_PER_REPORT_ROLE"); /// Length in bytes of packed request uint256 internal constant PACKED_REQUEST_LENGTH = 64; @@ -267,9 +265,9 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V _consumeLimit(requestsCount); - _processExitRequestsList(request.data, 0, requestsCount); + _processExitRequestsList(request.data); - _updateRequestStatus(requestStatus, _getTimestamp()); + _updateRequestStatus(requestStatus); emit ExitDataProcessing(exitRequestsHash); } @@ -343,7 +341,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } /** - * @notice Sets the maximum exit request limit and the frame during which a portion of the limit can be restored. + * @notice Sets the limits config * @param maxExitRequestsLimit The maximum number of exit requests. * @param exitsPerFrame The number of exits that can be restored per frame. * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. @@ -390,7 +388,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @notice Sets the maximum allowed number of validator exit requests to process in a single report. * @param maxRequests The new maximum number of exit requests allowed per report. */ - function setMaxValidatorsPerReport(uint256 maxRequests) external onlyRole(MAX_VALIDATORS_PER_REPORT_ROLE) { + function setMaxValidatorsPerReport(uint256 maxRequests) external onlyRole(EXIT_REQUEST_LIMIT_MANAGER_ROLE) { _setMaxValidatorsPerReport(maxRequests); } @@ -411,9 +409,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * - exitRequestsHash was not submited * - Request was not delivered */ - function getDeliveryTime( - bytes32 exitRequestsHash - ) external view returns (uint256 deliveryDateTimestamp) { + function getDeliveryTime(bytes32 exitRequestsHash) external view returns (uint256 deliveryDateTimestamp) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; @@ -513,8 +509,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } } - function _getTimestamp() internal view virtual returns (uint256) { - return block.timestamp; // solhint-disable-line not-rely-on-time + function _getTimestamp() internal view virtual returns (uint32) { + return uint32(block.timestamp); // solhint-disable-line not-rely-on-time } function _setMaxValidatorsPerReport(uint256 value) internal { @@ -582,10 +578,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V emit RequestsHashSubmitted(exitRequestsHash); } - function _updateRequestStatus(RequestStatus storage requestStatus, uint256 deliveredExitDataTimestamp) internal { - require(deliveredExitDataTimestamp <= type(uint32).max, "DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW"); - - requestStatus.deliveredExitDataTimestamp = uint32(deliveredExitDataTimestamp); + function _updateRequestStatus(RequestStatus storage requestStatus) internal { + requestStatus.deliveredExitDataTimestamp = _getTimestamp(); } /// Methods for reading data from tightly packed validator exit requests @@ -636,15 +630,15 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * This method read report data (DATA_FORMAT=1) within a range * Check dataWithoutPubkey <= lastDataWithoutPubkey needs to prevent duplicates */ - function _processExitRequestsList(bytes calldata data, uint256 startIndex, uint256 count) internal { + function _processExitRequestsList(bytes calldata data) internal { uint256 offset; uint256 offsetPastEnd; uint256 lastDataWithoutPubkey = 0; uint256 timestamp = _getTimestamp(); assembly { - offset := add(data.offset, mul(startIndex, PACKED_REQUEST_LENGTH)) - offsetPastEnd := add(offset, mul(count, PACKED_REQUEST_LENGTH)) + offset := data.offset + offsetPastEnd := add(offset, data.length) } bytes calldata pubkey; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index dab2952f88..a927875aba 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -254,7 +254,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker()).checkExitBusOracleReport(data.requestsCount); - _processExitRequestsList(data.data, 0, data.requestsCount); + _processExitRequestsList(data.data); _storageDataProcessingState().value = DataProcessingState({ refSlot: data.refSlot.toUint64(), diff --git a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol index 9c14a7c1f3..c7382f9ad8 100644 --- a/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol +++ b/test/0.8.9/contracts/ValidatorsExitBus__Harness.sol @@ -34,8 +34,8 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { } // Method used in VEB - function _getTimestamp() internal view override returns (uint256) { - return _getTime(); + function _getTimestamp() internal view override returns (uint32) { + return uint32(_getTime()); } function getDataProcessingState() external view returns (DataProcessingState memory) { @@ -50,9 +50,9 @@ contract ValidatorsExitBus__Harness is ValidatorsExitBusOracle, ITimeProvider { CONTRACT_VERSION_POSITION.setStorageUint256(version); } - function updateRequestStatus(bytes32 exitRequestHash, uint256 deliveredExitDataTimestamp) external { + function updateRequestStatus(bytes32 exitRequestHash) external { RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestHash]; - _updateRequestStatus(requestStatus, deliveredExitDataTimestamp); + _updateRequestStatus(requestStatus); } function getRequestStatus(bytes32 exitRequestHash) external view returns (RequestStatus memory requestStatus) { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index fb2c496642..0949c950b7 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -417,18 +417,19 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { ); }); - it("Should not give to set new maximum requests per report value without MAX_VALIDATORS_PER_REPORT_ROLE role", async () => { + it("Should not give to set new maximum requests per report value without EXIT_REQUEST_LIMIT_MANAGER_ROLE role", async () => { const maxRequestsPerReport = 4; + await expect( oracle.connect(stranger).setMaxValidatorsPerReport(maxRequestsPerReport), ).to.be.revertedWithOZAccessControlError( await stranger.getAddress(), - await oracle.MAX_VALIDATORS_PER_REPORT_ROLE(), + await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(), ); }); it("Should not allow to set new maximum requests per report value eq to 0", async () => { - const role = await oracle.MAX_VALIDATORS_PER_REPORT_ROLE(); + const role = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await oracle.grantRole(role, authorizedEntity); await expect(oracle.connect(authorizedEntity).setMaxValidatorsPerReport(0)).to.be.revertedWith( diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index 96ff8b8100..d266ac701f 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -187,19 +187,10 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { await oracle.storeNewHashRequestStatus(hash, contractVersion, timestamp); - const newTimestamp = 12345; - - await expect(oracle.updateRequestStatus(hash, newTimestamp)).to.not.be.reverted; + await expect(oracle.updateRequestStatus(hash)).to.not.be.reverted; const requestStatus = await oracle.getRequestStatus(hash); - expect(requestStatus.deliveredExitDataTimestamp).to.equal(newTimestamp); - }); - - it("reverts if deliveredExitDataTimestamp exceeds uint32 max", async () => { - const hash = keccak256("0xdddd"); - await expect(oracle.updateRequestStatus(hash, 2n ** 32n)).to.be.revertedWith( - "DELIVERED_EXIT_DATA_TIMESTAMP_OVERFLOW", - ); + expect(requestStatus.deliveredExitDataTimestamp).to.equal(await oracle.getTime()); }); }); }); From 48fe1026f8de936f4aa2c25f0497a45ccab681cc Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 17:58:25 +0400 Subject: [PATCH 236/405] fix: require -> revert --- contracts/0.8.9/lib/ExitLimitUtils.sol | 26 +++++++--- test/0.8.9/lib/exitLimitUtils.test.ts | 48 +++++++++++++++---- ...-bus-oracle.submitExitRequestsData.test.ts | 6 +-- ...awalGateway.triggerFullWithdrawals.test.ts | 2 +- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index dd42503617..615cca3462 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -31,7 +31,21 @@ library ExitLimitUtilsStorage { } library ExitLimitUtils { - // What should happen with limits if pause is enabled + /// @notice Error when new value for remaining limit exceeds maximum limit. + error LimitExceeded(); + + /// @notice Error when max exit request limit exceeds uint32 max. + error TooLargeMaxExitRequestsLimit(); + + /// @notice Error when frame duration exceeds uint32 max. + error TooLargeFrameDuration(); + + /// @notice Error when exits per frame exceed the maximum exit request limit. + error TooLargeExitsPerFrame(); + + /// @notice Error when frame duration is zero. + error ZeroFrameDuration(); + function calculateCurrentExitLimit( ExitRequestLimitData memory _data, uint256 timestamp @@ -58,7 +72,7 @@ library ExitLimitUtils { uint256 newExitRequestLimit, uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { - require(_data.maxExitRequestsLimit >= newExitRequestLimit, "LIMIT_EXCEEDED"); + if (_data.maxExitRequestsLimit < newExitRequestLimit) revert LimitExceeded(); uint256 secondsPassed = timestamp - _data.prevTimestamp; uint256 framesPassed = secondsPassed / _data.frameDurationInSec; @@ -77,10 +91,10 @@ library ExitLimitUtils { uint256 frameDurationInSec, uint256 timestamp ) internal pure returns (ExitRequestLimitData memory) { - require(maxExitRequestsLimit <= type(uint32).max, "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT"); - require(frameDurationInSec <= type(uint32).max, "TOO_LARGE_FRAME_DURATION"); - require(exitsPerFrame <= maxExitRequestsLimit, "TOO_LARGE_EXITS_PER_FRAME"); - require(frameDurationInSec != 0, "ZERO_FRAME_DURATION"); + if (maxExitRequestsLimit > type(uint32).max) revert TooLargeMaxExitRequestsLimit(); + if (frameDurationInSec > type(uint32).max) revert TooLargeFrameDuration(); + if (exitsPerFrame > maxExitRequestsLimit) revert TooLargeExitsPerFrame(); + if (frameDurationInSec == 0) revert ZeroFrameDuration(); _data.exitsPerFrame = uint32(exitsPerFrame); _data.frameDurationInSec = uint32(frameDurationInSec); diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts index cc3b0ad0f5..8db9799ec9 100644 --- a/test/0.8.9/lib/exitLimitUtils.test.ts +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -249,7 +249,10 @@ describe("ExitLimitUtils.sol", () => { prevTimestamp, ); - await expect(exitLimit.updatePrevExitLimit(11, prevTimestamp + 10)).to.be.revertedWith("LIMIT_EXCEEDED"); + await expect(exitLimit.updatePrevExitLimit(11, prevTimestamp + 10)).to.be.revertedWithCustomError( + exitLimit, + "LimitExceeded", + ); }); it("should increase prevTimestamp on frame duration if one frame passed", async () => { @@ -345,7 +348,12 @@ describe("ExitLimitUtils.sol", () => { const exitsPerFrame = 2; const frameDurationInSec = 10; - const result = await exitLimit.setExitLimits(maxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); + const result = await exitLimit.setExitLimits( + maxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); expect(result.maxExitRequestsLimit).to.equal(maxExitRequestsLimit); expect(result.exitsPerFrame).to.equal(exitsPerFrame); @@ -370,7 +378,12 @@ describe("ExitLimitUtils.sol", () => { ); const newMaxExitRequestsLimit = 50; - const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); @@ -394,7 +407,12 @@ describe("ExitLimitUtils.sol", () => { const newMaxExitRequestsLimit = 150; - const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(prevExitRequestsLimit); @@ -417,7 +435,12 @@ describe("ExitLimitUtils.sol", () => { ); const newMaxExitRequestsLimit = 77; - const result = await exitLimit.setExitLimits(newMaxExitRequestsLimit, exitsPerFrame, frameDurationInSec, timestamp); + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); @@ -426,18 +449,25 @@ describe("ExitLimitUtils.sol", () => { it("should revert if maxExitRequestsLimit is too large", async () => { const MAX_UINT32 = 2 ** 32; - await expect(exitLimit.setExitLimits(MAX_UINT32, 1, 10, 1000)).to.be.revertedWith( - "TOO_LARGE_MAX_EXIT_REQUESTS_LIMIT", + await expect(exitLimit.setExitLimits(MAX_UINT32, 1, 10, 1000)).to.be.revertedWithCustomError( + exitLimit, + "TooLargeMaxExitRequestsLimit", ); }); it("should revert if exitsPerFrame bigger than maxExitRequestsLimit", async () => { - await expect(exitLimit.setExitLimits(100, 101, 10, 1000)).to.be.revertedWith("TOO_LARGE_EXITS_PER_FRAME"); + await expect(exitLimit.setExitLimits(100, 101, 10, 1000)).to.be.revertedWithCustomError( + exitLimit, + "TooLargeExitsPerFrame", + ); }); it("should revert if frameDurationInSec is too large", async () => { const MAX_UINT32 = 2 ** 32; - await expect(exitLimit.setExitLimits(100, 2, MAX_UINT32, 1000)).to.be.revertedWith("TOO_LARGE_FRAME_DURATION"); + await expect(exitLimit.setExitLimits(100, 2, MAX_UINT32, 1000)).to.be.revertedWithCustomError( + exitLimit, + "TooLargeFrameDuration", + ); }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 0949c950b7..b00d6d8f43 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -330,9 +330,9 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { }); it("Should not allow to set exits per frame bigger than max limit", async () => { - await expect(oracle.connect(authorizedEntity).setExitRequestLimit(10, 12, FRAME_DURATION)).to.be.revertedWith( - "TOO_LARGE_EXITS_PER_FRAME", - ); + await expect( + oracle.connect(authorizedEntity).setExitRequestLimit(10, 12, FRAME_DURATION), + ).to.be.revertedWithCustomError(oracle, "TooLargeExitsPerFrame"); }); it("Should deliver request as it is below limit", async () => { diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 866f771168..3c9c3a3c9a 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -354,7 +354,7 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { it("Should not allow to set exitsPerFrame bigger than maxExitRequestsLimit", async () => { await expect( triggerableWithdrawalsGateway.connect(authorizedEntity).setExitRequestLimit(0, 1, 48), - ).to.be.revertedWith("TOO_LARGE_EXITS_PER_FRAME"); + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "TooLargeExitsPerFrame"); }); it("should emit StakingModuleExitNotificationFailed if onValidatorExitTriggered reverts", async () => { From be882945ffab97268dadfde5a2eb3886f5821be6 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 18:16:40 +0400 Subject: [PATCH 237/405] fix: getDeliveryTime -> getDeliveryTimestamp & require -> revert --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- contracts/0.8.25/interfaces/IValidatorsExitBus.sol | 2 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 ++-- test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol | 2 +- test/0.8.25/validatorExitDelayVerifier.test.ts | 2 +- ...lidator-exit-bus-oracle.submitExitRequestsData.test.ts | 6 +++--- test/0.8.9/oracle/validator-exit-bus.helpers.test.ts | 8 ++++---- test/integration/report-validator-exit-delay.ts | 6 +++--- test/integration/validators-exit-bus-single-delivery.ts | 2 +- test/integration/validators-exit-bus-trigger-exits.ts | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 57850ac23e..43435cf170 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -361,7 +361,7 @@ contract ValidatorExitDelayVerifier { ExitRequestData calldata exitRequests ) internal view returns (uint256 deliveryTimestamp) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); - deliveryTimestamp = veb.getDeliveryTime(exitRequestsHash); + deliveryTimestamp = veb.getDeliveryTimestamp(exitRequestsHash); if (deliveryTimestamp == 0) { revert EmptyDeliveryHistory(); diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol index bc318a25c2..dfc8984047 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/0.8.25/interfaces/IValidatorsExitBus.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.25; interface IValidatorsExitBus { - function getDeliveryTime(bytes32 exitRequestsHash) external view returns (uint256 timestamp); + function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp); function unpackExitRequest( bytes calldata exitRequests, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index c91668af1a..f3f7c027f3 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -409,7 +409,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * - exitRequestsHash was not submited * - Request was not delivered */ - function getDeliveryTime(bytes32 exitRequestsHash) external view returns (uint256 deliveryDateTimestamp) { + function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 deliveryDateTimestamp) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); RequestStatus storage storedRequest = requestStatusMap[exitRequestsHash]; @@ -514,7 +514,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V } function _setMaxValidatorsPerReport(uint256 value) internal { - require(value > 0, "ZERO_MAX_VALIDATORS_PER_REPORT"); + if (value == 0) revert ZeroArgument("maxValidatorsPerReport"); MAX_VALIDATORS_PER_REPORT_POSITION.setStorageUint256(value); } diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 2f3e5dcab8..8eb3df7354 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -30,7 +30,7 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { } } - function getDeliveryTime(bytes32 exitRequestsHash) external view returns (uint256 timestamp) { + function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp) { require(exitRequestsHash == _hash, "Mock error, Invalid exitRequestsHash"); return _deliveryTimestamp; } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 8244cb594a..a71704f805 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -518,7 +518,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { // Report not unpacked, deliveryTimestamp == 0 await vebo.setExitRequests(encodedExitRequestsHash, 0, exitRequests); - expect(await vebo.getDeliveryTime(encodedExitRequestsHash)).to.equal(0); + expect(await vebo.getDeliveryTimestamp(encodedExitRequestsHash)).to.equal(0); await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index b00d6d8f43..6f195a081f 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -432,9 +432,9 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const role = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); await oracle.grantRole(role, authorizedEntity); - await expect(oracle.connect(authorizedEntity).setMaxValidatorsPerReport(0)).to.be.revertedWith( - "ZERO_MAX_VALIDATORS_PER_REPORT", - ); + await expect(oracle.connect(authorizedEntity).setMaxValidatorsPerReport(0)) + .to.be.revertedWithCustomError(oracle, "ZeroArgument") + .withArgs("maxValidatorsPerReport"); }); it("Should not allow to process request larger than MAX_VALIDATORS_PER_REPORT", async () => { diff --git a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts index d266ac701f..69be2e121d 100644 --- a/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus.helpers.test.ts @@ -128,7 +128,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { }); }); - context("getDeliveryTime", () => { + context("getDeliveryTimestamp", () => { let originalState: string; before(async () => { @@ -140,7 +140,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { it("reverts if exitRequestsHash was never submitted (contractVersion = 0)", async () => { const fakeHash = keccak256("0x1111"); - await expect(oracle.getDeliveryTime(fakeHash)).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); + await expect(oracle.getDeliveryTimestamp(fakeHash)).to.be.revertedWithCustomError(oracle, "ExitHashNotSubmitted"); }); it("reverts if request was not delivered", async () => { @@ -151,7 +151,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { // Call the helper to store the hash await oracle.storeNewHashRequestStatus(exitRequestsHash, contractVersion, timestamp); - await expect(oracle.getDeliveryTime(exitRequestsHash)).to.be.revertedWithCustomError( + await expect(oracle.getDeliveryTimestamp(exitRequestsHash)).to.be.revertedWithCustomError( oracle, "RequestsNotDelivered", ); @@ -165,7 +165,7 @@ describe("ValidatorsExitBusOracle.sol:helpers", () => { // Call the helper to store the hash await oracle.storeNewHashRequestStatus(exitRequestsHash, contractVersion, timestamp); - const deliveredExitDataTimestamp = await oracle.getDeliveryTime(exitRequestsHash); + const deliveredExitDataTimestamp = await oracle.getDeliveryTimestamp(exitRequestsHash); expect(deliveredExitDataTimestamp).to.equal(timestamp); }); diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index fb40175ed1..ca9566e0ec 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -85,7 +85,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTime(encodedExitRequestsHash); + const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTimestamp(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); @@ -152,7 +152,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTime(encodedExitRequestsHash); + const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTimestamp(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -375,7 +375,7 @@ describe("Report Validator Exit Delay", () => { await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); - const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTime(encodedExitRequestsHash); + const deliveryTimestamp = await validatorsExitBusOracle.getDeliveryTimestamp(encodedExitRequestsHash); const eligibleToExitInSec = proofSlotTimestamp - deliveryTimestamp; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); diff --git a/test/integration/validators-exit-bus-single-delivery.ts b/test/integration/validators-exit-bus-single-delivery.ts index 0b34faeed5..219b5bbc1b 100644 --- a/test/integration/validators-exit-bus-single-delivery.ts +++ b/test/integration/validators-exit-bus-single-delivery.ts @@ -89,7 +89,7 @@ describe("ValidatorsExitBus integration", () => { .to.emit(veb, "ValidatorExitRequest") .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - const timestamp = await veb.getDeliveryTime(exitRequestsHash); + const timestamp = await veb.getDeliveryTimestamp(exitRequestsHash); expect(timestamp).to.equal(blockTimestamp); }); }); diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts index 536887f99a..dc932f9357 100644 --- a/test/integration/validators-exit-bus-trigger-exits.ts +++ b/test/integration/validators-exit-bus-trigger-exits.ts @@ -93,7 +93,7 @@ describe("ValidatorsExitBus integration", () => { .to.emit(veb, "ValidatorExitRequest") .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - const timestamp = await veb.getDeliveryTime(exitRequestsHash); + const timestamp = await veb.getDeliveryTimestamp(exitRequestsHash); expect(timestamp).to.equal(blockTimestamp); const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); From 0794a352ee45c1a3b9a069f20298c55b46af0f18 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 30 May 2025 18:18:26 +0400 Subject: [PATCH 238/405] fix: rename value -> maxValidatorsPerReport --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index f3f7c027f3..8833a76a5f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -513,10 +513,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V return uint32(block.timestamp); // solhint-disable-line not-rely-on-time } - function _setMaxValidatorsPerReport(uint256 value) internal { - if (value == 0) revert ZeroArgument("maxValidatorsPerReport"); + function _setMaxValidatorsPerReport(uint256 maxValidatorsPerReport) internal { + if (maxValidatorsPerReport == 0) revert ZeroArgument("maxValidatorsPerReport"); - MAX_VALIDATORS_PER_REPORT_POSITION.setStorageUint256(value); + MAX_VALIDATORS_PER_REPORT_POSITION.setStorageUint256(maxValidatorsPerReport); } function _getMaxValidatorsPerReport() internal view returns (uint256) { From cd35f7e528e23f50f0fb8a435cc6bdff356605bc Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 30 May 2025 16:44:21 +0200 Subject: [PATCH 239/405] feat: improve comments in withdrawal vault --- contracts/0.8.9/WithdrawalVault.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 3814dc6cac..3cfc545f4c 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -151,7 +151,8 @@ contract WithdrawalVault is Versioned, WithdrawalVaultEIP7002 { * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting partial withdrawals. * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... * - * @param amounts An array of 8-byte unsigned integers representing the amounts to be withdrawn for each corresponding public key. + * @param amounts An array of 8-byte unsigned integers that represent the amounts, denominated in Gwei, + * to be withdrawn for each corresponding public key. * For full withdrawal requests, the amount should be set to 0. * For partial withdrawal requests, the amount should be greater than 0. * From fe013d2cc33e41bc5101b63983500a1c5546d2ee Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 30 May 2025 17:55:12 +0200 Subject: [PATCH 240/405] feat: fix type in error name --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 4 ++-- test/0.8.25/validatorExitDelayVerifier.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 43435cf170..f4b5403f3d 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -100,7 +100,7 @@ contract ValidatorExitDelayVerifier { error UnsupportedSlot(uint64 slot); error InvalidPivotSlot(); error ZeroLidoLocatorAddress(); - error ExitIstNotEligibleOnProvableBeaconBlock( + error ExitIsNotEligibleOnProvableBeaconBlock( uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); @@ -341,7 +341,7 @@ contract ValidatorExitDelayVerifier { : earliestPossibleVoluntaryExitTimestamp; if (referenceSlotTimestamp < eligibleExitRequestTimestamp) { - revert ExitIstNotEligibleOnProvableBeaconBlock(referenceSlotTimestamp, eligibleExitRequestTimestamp); + revert ExitIsNotEligibleOnProvableBeaconBlock(referenceSlotTimestamp, eligibleExitRequestTimestamp); } return referenceSlotTimestamp - eligibleExitRequestTimestamp; diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index a71704f805..1446aef40c 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -389,7 +389,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); - it("reverts with 'ExitIstNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { + it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { const intervalInSecondsAfterProofSlot = 1; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; @@ -417,7 +417,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIstNotEligibleOnProvableBeaconBlock"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); @@ -428,7 +428,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIstNotEligibleOnProvableBeaconBlock"); + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); }); it("reverts if the validator proof is incorrect", async () => { From d9fe8ba4dd15d4d240d09a482f5bc00ceeec1939 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 30 May 2025 19:05:14 +0200 Subject: [PATCH 241/405] feat: fix typo in error name --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 4 ++-- .../validator-exit-bus-oracle.submitExitRequestsData.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 8833a76a5f..126e039a63 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -103,7 +103,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @notice Thrown when exit requests in report exceed the maximum allowed number of requests per report. * @param requestsCount Amount of requests that were sent for processing */ - error ToManyExitRequestsInReport(uint256 requestsCount, uint256 maxRequestsPerReport); + error TooManyExitRequestsInReport(uint256 requestsCount, uint256 maxRequestsPerReport); /// @dev Events @@ -260,7 +260,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V uint256 maxRequestsPerReport = _getMaxValidatorsPerReport(); if (requestsCount > maxRequestsPerReport) { - revert ToManyExitRequestsInReport(requestsCount, maxRequestsPerReport); + revert TooManyExitRequestsInReport(requestsCount, maxRequestsPerReport); } _consumeLimit(requestsCount); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 6f195a081f..861d97aaad 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -465,7 +465,7 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHashRandom); await expect(oracle.submitExitRequestsData(exitRequestRandom)) - .to.be.revertedWithCustomError(oracle, "ToManyExitRequestsInReport") + .to.be.revertedWithCustomError(oracle, "TooManyExitRequestsInReport") .withArgs(5, 4); }); From 0900b1931c2bba953e985f35e590552e8b18171b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 30 May 2025 20:34:27 +0200 Subject: [PATCH 242/405] refactor: pass single property instead of entire object to inner function in exit delay verifier --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index f4b5403f3d..8f357afa21 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -186,7 +186,7 @@ contract ValidatorExitDelayVerifier { uint256 eligibleToExitInSec = _getSecondsSinceExitIsEligible( deliveredTimestamp, - witness, + witness.activationEpoch, proofSlotTimestamp ); @@ -233,7 +233,7 @@ contract ValidatorExitDelayVerifier { uint256 eligibleToExitInSec = _getSecondsSinceExitIsEligible( deliveredTimestamp, - witness, + witness.activationEpoch, proofSlotTimestamp ); @@ -325,13 +325,13 @@ contract ValidatorExitDelayVerifier { */ function _getSecondsSinceExitIsEligible( uint256 deliveredTimestamp, - ValidatorWitness calldata witness, + uint256 activationEpoch, uint256 referenceSlotTimestamp ) internal view returns (uint256) { // The earliest a validator can voluntarily exit is after the Shard Committee Period // subsequent to its activation epoch. uint256 earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + - (witness.activationEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + + (activationEpoch * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + SHARD_COMMITTEE_PERIOD_IN_SECONDS; // The actual eligible timestamp is the max between the exit request submission time From e675406cebaf7a032afe4e1c3076741142639826 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 30 May 2025 20:57:13 +0200 Subject: [PATCH 243/405] feat: fix linter problems --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- contracts/0.8.9/WithdrawalVault.sol | 9 +++------ contracts/0.8.9/WithdrawalVaultEIP7002.sol | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 8f357afa21..095901715e 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -55,7 +55,7 @@ contract ValidatorExitDelayVerifier { /// @notice EIP-4788 contract address that provides a mapping of timestamp -> known beacon block root. address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; - uint64 constant FAR_FUTURE_EPOCH = type(uint64).max; + uint64 private constant FAR_FUTURE_EPOCH = type(uint64).max; uint64 public immutable GENESIS_TIME; uint32 public immutable SLOTS_PER_EPOCH; diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 3cfc545f4c..6793027747 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -4,13 +4,10 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; -import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; -import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; - +import {IERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol"; +import {IERC721} from "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol"; +import {SafeERC20} from "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol"; import {Versioned} from "./utils/Versioned.sol"; -import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; - import {WithdrawalVaultEIP7002} from "./WithdrawalVaultEIP7002.sol"; interface ILido { diff --git a/contracts/0.8.9/WithdrawalVaultEIP7002.sol b/contracts/0.8.9/WithdrawalVaultEIP7002.sol index f4ff11da3c..970b54dfbd 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7002.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7002.sol @@ -8,7 +8,7 @@ pragma solidity 0.8.9; * @title A base contract for a withdrawal vault, enables to submit EIP-7002 withdrawal requests. */ abstract contract WithdrawalVaultEIP7002 { - address constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; + address public constant WITHDRAWAL_REQUEST = 0x00000961Ef480Eb55e80D19ad83579A64c007002; event WithdrawalRequestAdded(bytes request); From bd1c794a2241d954a2ee315aff1b90478c4d3d6b Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 2 Jun 2025 09:54:46 +0200 Subject: [PATCH 244/405] feat: clean-up text comments --- contracts/0.8.9/oracle/AccountingOracle.sol | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 166d508f8d..5801ff0e12 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -343,8 +343,6 @@ contract AccountingOracle is BaseOracle { /// @dev Hash of the extra data. See the constant defining a specific extra data /// format for the info on how to calculate the hash. /// - /// Must be set to a `ZERO_BYTES32` if the oracle report contains no extra data. - /// bytes32 extraDataHash; /// @dev Number of the extra data items. @@ -359,21 +357,21 @@ contract AccountingOracle is BaseOracle { /// @notice The extra data format used to signify that the oracle report contains no extra data. /// + /// The `extraDataHash` in `ReportData` must be set to a `ZERO_BYTES32` + /// uint256 public constant EXTRA_DATA_FORMAT_EMPTY = 0; /// @notice The list format for the extra data array. Used when the oracle reports contains extra data. /// - /// Depending on the extra data size, it's passed within a single or multiple transactions. - /// Each transaction contains data consisting of 1) the keccak256 hash of the next - /// transaction's data or `ZERO_BYTES32` if there are no more data chunks, and 2) a chunk - /// of report data (an array of items). + /// When extra data is included in a report, it may be split across one or more transactions. + /// Each transaction contains: + /// 1) A 32-byte keccak256 hash of the next transaction's data (or `ZERO_BYTES32` if none), + /// 2) A chunk of report items (an array of items). /// /// | 32 bytes | X bytes | /// | Next transaction's data hash or `ZERO_BYTES32` | array of items | /// - /// The `extraDataHash` field of the `ReportData` struct is calculated as a keccak256 hash - /// over the first transaction's data, i.e. over the first data chunk with the second - /// transaction's data hash (or `ZERO_BYTES32`) prepended. + /// The `extraDataHash` in `ReportData` is calculated as shown in the example below: /// /// ReportData.extraDataHash := hash0 /// hash0 := keccak256(| hash1 | extraData[0], ... extraData[n] |) From 68eafaa76d87113373c0861d633cd7231d68fa96 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 2 Jun 2025 10:09:41 +0200 Subject: [PATCH 245/405] feat: fix comment --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index ffe53bfb9f..c97a321aae 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -81,7 +81,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { bytes32 public constant MANAGE_SIGNING_KEYS = 0x75abc64490e17b40ea1e66691c3eb493647b24430b358bd87ec3e5127f1621ee; // bytes32 public constant SET_NODE_OPERATOR_LIMIT_ROLE = keccak256("SET_NODE_OPERATOR_LIMIT_ROLE"); bytes32 public constant SET_NODE_OPERATOR_LIMIT_ROLE = 0x07b39e0faf2521001ae4e58cb9ffd3840a63e205d288dc9c93c3774f0d794754; - // bytes32 public constant ACTIVATE_NODE_OPERATOR_ROLE = keccak256("MANAGE_NODE_OPERATOR_ROLE"); + // bytes32 public constant MANAGE_NODE_OPERATOR_ROLE = keccak256("MANAGE_NODE_OPERATOR_ROLE"); bytes32 public constant MANAGE_NODE_OPERATOR_ROLE = 0x78523850fdd761612f46e844cf5a16bda6b3151d6ae961fd7e8e7b92bfbca7f8; // bytes32 public constant STAKING_ROUTER_ROLE = keccak256("STAKING_ROUTER_ROLE"); bytes32 public constant STAKING_ROUTER_ROLE = 0xbb75b874360e0bfd87f964eadd8276d8efb7c942134fc329b513032d0803e0c6; From d30d55172dd279d5d91d29967888023500f4c642 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 2 Jun 2025 11:50:49 +0200 Subject: [PATCH 246/405] feat: add test for trigger full withdrawals --- lib/protocol/discover.ts | 5 + lib/protocol/types.ts | 4 + scripts/scratch/steps/0150-transfer-roles.ts | 1 + test/integration/trigger-full-withdrawals.ts | 229 +++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 test/integration/trigger-full-withdrawals.ts diff --git a/lib/protocol/discover.ts b/lib/protocol/discover.ts index 3c37390121..2e714a5521 100644 --- a/lib/protocol/discover.ts +++ b/lib/protocol/discover.ts @@ -92,6 +92,10 @@ const getCoreContracts = async (locator: LoadedContract, config: Pr "ValidatorsExitBusOracle", config.get("validatorsExitBusOracle") || (await locator.validatorsExitBusOracle()), ), + triggerableWithdrawalsGateway: loadContract( + "TriggerableWithdrawalsGateway", + config.get("triggerableWithdrawalsGateway") || (await locator.triggerableWithdrawalsGateway()), + ), withdrawalQueue: loadContract( "WithdrawalQueueERC721", config.get("withdrawalQueue") || (await locator.withdrawalQueue()), @@ -192,6 +196,7 @@ export async function discover() { "Burner": foundationContracts.burner.address, "Legacy Oracle": foundationContracts.legacyOracle.address, "wstETH": contracts.wstETH.address, + "Triggered Withdrawal Gateway": contracts.triggerableWithdrawalsGateway.address, }); const signers = { diff --git a/lib/protocol/types.ts b/lib/protocol/types.ts index 44cc3841a2..bfe63497da 100644 --- a/lib/protocol/types.ts +++ b/lib/protocol/types.ts @@ -17,6 +17,7 @@ import { OracleDaemonConfig, OracleReportSanityChecker, StakingRouter, + TriggerableWithdrawalsGateway, ValidatorExitDelayVerifier, ValidatorsExitBusOracle, WithdrawalQueueERC721, @@ -41,6 +42,7 @@ export type ProtocolNetworkItems = { stakingRouter: string; validatorExitDelayVerifier: string; validatorsExitBusOracle: string; + triggerableWithdrawalsGateway: string; withdrawalQueue: string; withdrawalVault: string; oracleDaemonConfig: string; @@ -75,6 +77,7 @@ export interface ContractTypes { HashConsensus: HashConsensus; NodeOperatorsRegistry: NodeOperatorsRegistry; WstETH: WstETH; + TriggerableWithdrawalsGateway: TriggerableWithdrawalsGateway; } export type ContractName = keyof ContractTypes; @@ -101,6 +104,7 @@ export type CoreContracts = { withdrawalVault: LoadedContract; oracleDaemonConfig: LoadedContract; wstETH: LoadedContract; + triggerableWithdrawalsGateway: LoadedContract; }; export type AragonContracts = { diff --git a/scripts/scratch/steps/0150-transfer-roles.ts b/scripts/scratch/steps/0150-transfer-roles.ts index e7804196d0..ef8ccf4209 100644 --- a/scripts/scratch/steps/0150-transfer-roles.ts +++ b/scripts/scratch/steps/0150-transfer-roles.ts @@ -23,6 +23,7 @@ export async function main() { { name: "WithdrawalQueueERC721", address: state.withdrawalQueueERC721.proxy.address }, { name: "OracleDaemonConfig", address: state.oracleDaemonConfig.address }, { name: "OracleReportSanityChecker", address: state.oracleReportSanityChecker.address }, + { name: "TriggerableWithdrawalsGateway", address: state.triggerableWithdrawalsGateway.address }, ]; for (const contract of ozAdminTransfers) { diff --git a/test/integration/trigger-full-withdrawals.ts b/test/integration/trigger-full-withdrawals.ts new file mode 100644 index 0000000000..4776d7766d --- /dev/null +++ b/test/integration/trigger-full-withdrawals.ts @@ -0,0 +1,229 @@ +// ToDo: write test for triggerFullWithdrawals +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { StakingRouter, TriggerableWithdrawalsGateway, WithdrawalVault } from "typechain-types"; + +import { ether } from "lib"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { bailOnFailure, Snapshot } from "test/suite"; + +describe("TriggerFullWithdrawals Integration", () => { + let ctx: ProtocolContext; + let snapshot: string; + + let triggerableWithdrawalsGateway: TriggerableWithdrawalsGateway; + let withdrawalVault: WithdrawalVault; + let stakingRouter: StakingRouter; + let authorizedEntity: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + let admin: HardhatEthersSigner; + + // Test validator pubkeys (48 bytes each) + const PUBKEYS = [ + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + ]; + + // Validator data for triggering full withdrawals + const validatorData = [ + { stakingModuleId: 1, nodeOperatorId: 0, pubkey: PUBKEYS[0] }, + { stakingModuleId: 1, nodeOperatorId: 1, pubkey: PUBKEYS[1] }, + { stakingModuleId: 2, nodeOperatorId: 0, pubkey: PUBKEYS[2] }, + ]; + + before(async () => { + ctx = await getProtocolContext(); + + [authorizedEntity, stranger, admin] = await ethers.getSigners(); + + // Get contract instances from the context + withdrawalVault = ctx.contracts.withdrawalVault as WithdrawalVault; + stakingRouter = ctx.contracts.stakingRouter as StakingRouter; + triggerableWithdrawalsGateway = ctx.contracts.triggerableWithdrawalsGateway as TriggerableWithdrawalsGateway; + + // Take a snapshot to restore state after tests + snapshot = await Snapshot.take(); + }); + + beforeEach(bailOnFailure); + + after(async () => await Snapshot.restore(snapshot)); + + it("Should properly setup TriggerableWithdrawalsGateway", async () => { + // Verify that the TriggerableWithdrawalsGateway is properly set up + const withdrawalVaultAddress = await ctx.contracts.locator.withdrawalVault(); + expect(withdrawalVaultAddress).to.equal(await withdrawalVault.getAddress()); + + const stakingRouterAddress = await ctx.contracts.locator.stakingRouter(); + expect(stakingRouterAddress).to.equal(await stakingRouter.getAddress()); + }); + + it("Should revert when non-authorized entity tries to trigger full withdrawals", async () => { + const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + + // Check if stranger doesn't have permission + const hasRole = await triggerableWithdrawalsGateway.hasRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, stranger.address); + expect(hasRole).to.be.false; + + // Attempt to trigger full withdrawals with unauthorized account + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + const totalFee = BigInt(validatorData.length) * withdrawalFee; + + await expect( + triggerableWithdrawalsGateway + .connect(stranger) + .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalFee }), + ).to.be.revertedWithCustomError; + }); + + it("Should revert when insufficient fee is provided", async () => { + // Grant role to authorizedEntity + const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + const agentSigner = await ctx.getSigner("agent"); + await triggerableWithdrawalsGateway + .connect(agentSigner) + .grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, authorizedEntity); + + // Ensure authorizedEntity has the role + const hasRole = await triggerableWithdrawalsGateway.hasRole( + ADD_FULL_WITHDRAWAL_REQUEST_ROLE, + authorizedEntity.address, + ); + expect(hasRole).to.be.true; + + // Get withdrawal fee + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + const totalFee = BigInt(validatorData.length) * withdrawalFee; + const insufficientFee = totalFee - 1n; + + // Try to trigger with insufficient fee + await expect( + triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: insufficientFee }), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "InsufficientFee"); + }); + + it("Should successfully trigger full withdrawals", async () => { + // Setup TW_EXIT_LIMIT_MANAGER_ROLE + const TW_EXIT_LIMIT_MANAGER_ROLE = await triggerableWithdrawalsGateway.TW_EXIT_LIMIT_MANAGER_ROLE(); + const agent = await ctx.getSigner("agent", ether("1")); + + console.log("Agent address:", agent.address); + + // Grant roles if needed + const ADD_FULL_WITHDRAWAL_REQUEST_ROLE = await triggerableWithdrawalsGateway.ADD_FULL_WITHDRAWAL_REQUEST_ROLE(); + if (!(await triggerableWithdrawalsGateway.hasRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, authorizedEntity.address))) { + await triggerableWithdrawalsGateway.connect(agent).grantRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE, authorizedEntity); + } + + if (!(await triggerableWithdrawalsGateway.hasRole(TW_EXIT_LIMIT_MANAGER_ROLE, agent.address))) { + await triggerableWithdrawalsGateway.connect(agent).grantRole(TW_EXIT_LIMIT_MANAGER_ROLE, agent); + } + + // Configure exit request limits to allow our test + await triggerableWithdrawalsGateway.connect(agent).setExitRequestLimit(100, 10, 48); + + // Get the withdrawal fee + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + const totalFee = BigInt(validatorData.length) * withdrawalFee; + + // Send some extra ETH for refund testing + const extraAmount = ether("0.01"); + const totalAmount = totalFee + extraAmount; + + // Create a refund recipient + const refundRecipient = stranger; + const balanceBefore = await ethers.provider.getBalance(refundRecipient.address); + + // Trigger full withdrawals + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(validatorData, refundRecipient.address, 0, { value: totalAmount }); + + // Check refund was processed + const balanceAfter = await ethers.provider.getBalance(refundRecipient.address); + expect(balanceAfter).to.equal(balanceBefore + extraAmount); + + // Verify exit limits were consumed + const exitLimitInfo = await triggerableWithdrawalsGateway.getExitRequestLimitFullInfo(); + const prevExitRequestsLimit = exitLimitInfo[3]; + expect(prevExitRequestsLimit).to.equal(100n - BigInt(validatorData.length)); + }); + + it("Should successfully trigger full withdrawals with fee refund to sender", async () => { + // Get the withdrawal fee + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + const totalFee = BigInt(validatorData.length) * withdrawalFee; + + // Send some extra ETH for refund testing + const extraAmount = ether("0.01"); + const totalAmount = totalFee + extraAmount; + + // Get sender balance before transaction + const balanceBefore = await ethers.provider.getBalance(authorizedEntity.address); + + // Trigger full withdrawals with refund to sender (ZeroAddress means refund to sender) + const tx = await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalAmount }); + + // Get gas costs + const receipt = await tx.wait(); + const gasCost = receipt!.gasUsed * receipt!.gasPrice; + + // Check balance after (should be: initial - gas - totalFee) + const balanceAfter = await ethers.provider.getBalance(authorizedEntity.address); + expect(balanceAfter).to.be.approximately(balanceBefore - gasCost - totalFee, 10n ** 10n); + }); + + it("Should reject new withdrawal requests when gateway is paused", async () => { + // Setup PAUSE_ROLE and RESUME_ROLE + const PAUSE_ROLE = await triggerableWithdrawalsGateway.PAUSE_ROLE(); + const RESUME_ROLE = await triggerableWithdrawalsGateway.RESUME_ROLE(); + + const agentSigner = await ctx.getSigner("agent"); + + // Grant roles to admin if not already granted + if (!(await triggerableWithdrawalsGateway.hasRole(PAUSE_ROLE, admin.address))) { + await triggerableWithdrawalsGateway.connect(agentSigner).grantRole(PAUSE_ROLE, admin); + } + + if (!(await triggerableWithdrawalsGateway.hasRole(RESUME_ROLE, admin.address))) { + await triggerableWithdrawalsGateway.connect(agentSigner).grantRole(RESUME_ROLE, admin); + } + + // Pause the contract + await triggerableWithdrawalsGateway.connect(admin).pauseFor(1000); + + // Verify contract is paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.be.true; + + // Try to trigger withdrawals when paused + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + const totalFee = BigInt(validatorData.length) * withdrawalFee; + + await expect( + triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalFee }), + ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ResumedExpected"); + + // Resume the contract + await triggerableWithdrawalsGateway.connect(admin).resume(); + + // Verify contract is no longer paused + expect(await triggerableWithdrawalsGateway.isPaused()).to.be.false; + + // Trigger withdrawals should now work + await triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalFee }); + }); +}); From c6f340cc05dd325c31a29945dac809b474cfcf8d Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 2 Jun 2025 14:05:51 +0400 Subject: [PATCH 247/405] fix: check wv event --- test/integration/trigger-full-withdrawals.ts | 8 ++++++-- test/integration/validators-exit-bus-trigger-exits.ts | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/test/integration/trigger-full-withdrawals.ts b/test/integration/trigger-full-withdrawals.ts index 4776d7766d..e3fe849099 100644 --- a/test/integration/trigger-full-withdrawals.ts +++ b/test/integration/trigger-full-withdrawals.ts @@ -143,9 +143,10 @@ describe("TriggerFullWithdrawals Integration", () => { const balanceBefore = await ethers.provider.getBalance(refundRecipient.address); // Trigger full withdrawals - await triggerableWithdrawalsGateway + const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(validatorData, refundRecipient.address, 0, { value: totalAmount }); + await expect(tx).to.emit(withdrawalVault, "WithdrawalRequestAdded"); // Check refund was processed const balanceAfter = await ethers.provider.getBalance(refundRecipient.address); @@ -173,6 +174,7 @@ describe("TriggerFullWithdrawals Integration", () => { const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalAmount }); + await expect(tx).to.emit(withdrawalVault, "WithdrawalRequestAdded"); // Get gas costs const receipt = await tx.wait(); @@ -222,8 +224,10 @@ describe("TriggerFullWithdrawals Integration", () => { expect(await triggerableWithdrawalsGateway.isPaused()).to.be.false; // Trigger withdrawals should now work - await triggerableWithdrawalsGateway + const tx = await triggerableWithdrawalsGateway .connect(authorizedEntity) .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalFee }); + + await expect(tx).to.emit(withdrawalVault, "WithdrawalRequestAdded"); }); }); diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts index dc932f9357..e4332ef839 100644 --- a/test/integration/validators-exit-bus-trigger-exits.ts +++ b/test/integration/validators-exit-bus-trigger-exits.ts @@ -126,7 +126,8 @@ describe("ValidatorsExitBus integration", () => { const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); - await veb.triggerExits(exitRequest, [0], refundRecipient.getAddress(), { value: 10 }); + const tx = await veb.triggerExits(exitRequest, [0], refundRecipient.getAddress(), { value: 10 }); + await expect(tx).to.emit(wv, "WithdrawalRequestAdded"); const fee = await wv.getWithdrawalRequestFee(); From 9410bd56589b2cb4e22ce37720d16784f3b0b130 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 2 Jun 2025 15:01:06 +0400 Subject: [PATCH 248/405] fix: veb methods description --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 126e039a63..f8caf4be3c 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -237,9 +237,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @dev Reverts if: * - The contract is paused. * - The keccak256 hash of `requestsData` does not exist in storage (i.e., was not submitted). - * - The provided Exit Requests Data has already been fully unpacked. - * - The contract version does not match the version at the time of report submission. + * - The provided Exit Requests Data has already been submitted. + * - The contract version does not match the version at the time of hash submission. * - The data format is not supported. + * - The data length is less than the maximum number of requests allowed per payload. * - There is no remaining quota available for the current limits. * * Emits `ValidatorExitRequest` events; @@ -247,7 +248,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @param request - The exit requests structure. */ function submitExitRequestsData(ExitRequestsData calldata request) external whenResumed { - // bytes calldata data = request.data; bytes32 exitRequestsHash = keccak256(abi.encode(request.data, request.dataFormat)); RequestStatus storage requestStatus = _storageRequestStatus()[exitRequestsHash]; @@ -275,14 +275,16 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /** * @notice Submits Triggerable Withdrawal Requests to the Triggerable Withdrawals Gateway. * - * @param exitsData The report data previously unpacked and emitted by the VEB. - * @param exitDataIndexes Array of of sorted indexes pointing to validators in `exitsData.data` + * @param exitsData The report data previously submitted by the VEB. + * @param exitDataIndexes Array of sorted indexes pointing to validators in `exitsData.data` * to be exited via TWR. * @param refundRecipient Address to return extra fee on TW (eip-7002) exit. * * @dev Reverts if: - * - The hash of `exitsData` was not previously submitted in the VEB. - * - Any of the provided `exitDataIndexes` refers to a validator that was not yet delivered (i.e., exit request not emitted). + * - The contract is paused. + * - The keccak256 hash of `requestsData` does not exist in storage (i.e., was not submitted). + * - The provided Exit Requests Data has not been previously submitted. + * - Any of the provided `exitDataIndexes` refers to an index out of range. * - `exitDataIndexes` is not strictly increasing array */ function triggerExits( From 599894f14e05e584bab95d59adb1f8a770bdd3ca Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 2 Jun 2025 15:06:20 +0400 Subject: [PATCH 249/405] fix: veb methods description --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index f8caf4be3c..0613cd440c 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -240,7 +240,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * - The provided Exit Requests Data has already been submitted. * - The contract version does not match the version at the time of hash submission. * - The data format is not supported. - * - The data length is less than the maximum number of requests allowed per payload. + * - The data length exceeds the maximum number of requests allowed per payload. * - There is no remaining quota available for the current limits. * * Emits `ValidatorExitRequest` events; @@ -408,8 +408,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @param exitRequestsHash - The exit requests hash. * * @dev Reverts if: - * - exitRequestsHash was not submited - * - Request was not delivered + * - exitRequestsHash was not submitted + * - Request was not submitted */ function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 deliveryDateTimestamp) { mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); From 69a4f0c4ff099a6162468c31655848dc582d9bb6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 3 Jun 2025 12:39:50 +0200 Subject: [PATCH 250/405] feat: add event for staking module exit notification failure and refactor staking module retrieval --- contracts/0.8.9/StakingRouter.sol | 100 +++++++----------- .../0.8.9/TriggerableWithdrawalsGateway.sol | 32 ++---- .../contracts/StakingRouter__Harness.sol | 2 +- 3 files changed, 49 insertions(+), 85 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 3350a11c6b..078adb0f0e 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -38,6 +38,12 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// Emitted when the StakingRouter received ETH event StakingRouterETHDeposited(uint256 indexed stakingModuleId, uint256 amount); + event StakingModuleExitNotificationFailed( + uint256 indexed stakingModuleId, + uint256 indexed nodeOperatorId, + bytes _publicKey + ); + /// @dev Errors error ZeroAddressLido(); error ZeroAddressAdmin(); @@ -188,39 +194,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @param _maxDepositsPerBlock Array of max deposits per block. /// @param _minDepositBlockDistances Array of min deposit block distances. /// @dev https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md - function finalizeUpgrade_v2( - uint256[] memory _priorityExitShareThresholds, - uint256[] memory _maxDepositsPerBlock, - uint256[] memory _minDepositBlockDistances - ) external { - _checkContractVersion(1); - - uint256 stakingModulesCount = getStakingModulesCount(); - - _validateEqualArrayLengths(stakingModulesCount, _priorityExitShareThresholds.length); - _validateEqualArrayLengths(stakingModulesCount, _maxDepositsPerBlock.length); - _validateEqualArrayLengths(stakingModulesCount, _minDepositBlockDistances.length); - - for (uint256 i; i < stakingModulesCount; ) { - StakingModule storage stakingModule = _getStakingModuleByIndex(i); - _updateStakingModule( - stakingModule, - stakingModule.id, - stakingModule.stakeShareLimit, - _priorityExitShareThresholds[i], - stakingModule.stakingModuleFee, - stakingModule.treasuryFee, - _maxDepositsPerBlock[i], - _minDepositBlockDistances[i] - ); - - unchecked { - ++i; - } - } - - _updateContractVersion(2); - } + // function finalizeUpgrade_v2( + // uint256[] memory _priorityExitShareThresholds, + // uint256[] memory _maxDepositsPerBlock, + // uint256[] memory _minDepositBlockDistances + // ) external /// @notice Returns Lido contract address. /// @return Lido contract address. @@ -316,7 +294,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _maxDepositsPerBlock, uint256 _minDepositBlockDistance ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { - StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); + StakingModule storage stakingModule = _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); _updateStakingModule( stakingModule, _stakingModuleId, @@ -473,7 +451,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version for (uint256 i = 0; i < _stakingModuleIds.length; ) { uint256 stakingModuleId = _stakingModuleIds[i]; - StakingModule storage stakingModule = _getStakingModuleById(stakingModuleId); + StakingModule storage stakingModule = _getStakingModuleByIndex(_getStakingModuleIndexById(stakingModuleId)); uint256 prevReportedExitedValidatorsCount = stakingModule.exitedValidatorsCount; if (_exitedValidatorsCounts[i] < prevReportedExitedValidatorsCount) { @@ -572,7 +550,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version external onlyRole(UNSAFE_SET_EXITED_VALIDATORS_ROLE) { - StakingModule storage stakingModuleState = _getStakingModuleById(_stakingModuleId); + StakingModule storage stakingModuleState = _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); IStakingModule stakingModule = IStakingModule(stakingModuleState.stakingModuleAddress); ( @@ -723,7 +701,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version view returns (StakingModule memory) { - return _getStakingModuleById(_stakingModuleId); + return _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); } /// @notice Returns total number of staking modules. @@ -747,7 +725,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version view returns (StakingModuleStatus) { - return StakingModuleStatus(_getStakingModuleById(_stakingModuleId).status); + return StakingModuleStatus(_getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)).status); } /// @notice A summary of the staking module's validators. @@ -804,8 +782,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version view returns (StakingModuleSummary memory summary) { - StakingModule memory stakingModuleState = getStakingModule(_stakingModuleId); - IStakingModule stakingModule = IStakingModule(stakingModuleState.stakingModuleAddress); + IStakingModule stakingModule = IStakingModule(getStakingModule(_stakingModuleId).stakingModuleAddress); ( summary.totalExitedValidators, summary.totalDepositedValidators, @@ -823,8 +800,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version view returns (NodeOperatorSummary memory summary) { - StakingModule memory stakingModuleState = getStakingModule(_stakingModuleId); - IStakingModule stakingModule = IStakingModule(stakingModuleState.stakingModuleAddress); + IStakingModule stakingModule = IStakingModule(getStakingModule(_stakingModuleId).stakingModuleAddress); /// @dev using intermediate variables below due to "Stack too deep" error in case of /// assigning directly into the NodeOperatorSummary struct ( @@ -971,7 +947,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 _stakingModuleId, StakingModuleStatus _status ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { - StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); + StakingModule storage stakingModule = _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); if (StakingModuleStatus(stakingModule.status) == _status) revert StakingModuleStatusTheSame(); _setStakingModuleStatus(stakingModule, _status); } @@ -1017,21 +993,21 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version view returns (uint256) { - return _getStakingModuleById(_stakingModuleId).lastDepositBlock; + return _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)).lastDepositBlock; } /// @notice Returns the min deposit block distance for the staking module. /// @param _stakingModuleId Id of the staking module. /// @return Min deposit block distance for the staking module. function getStakingModuleMinDepositBlockDistance(uint256 _stakingModuleId) external view returns (uint256) { - return _getStakingModuleById(_stakingModuleId).minDepositBlockDistance; + return _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)).minDepositBlockDistance; } /// @notice Returns the max deposits count per block for the staking module. /// @param _stakingModuleId Id of the staking module. /// @return Max deposits count per block for the staking module. function getStakingModuleMaxDepositsPerBlock(uint256 _stakingModuleId) external view returns (uint256) { - return _getStakingModuleById(_stakingModuleId).maxDepositsPerBlock; + return _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)).maxDepositsPerBlock; } /// @notice Returns active validators count for the staking module. @@ -1042,7 +1018,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version view returns (uint256 activeValidatorsCount) { - StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); + StakingModule storage stakingModule = _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); ( uint256 totalExitedValidators, uint256 totalDepositedValidators, @@ -1223,7 +1199,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version bytes32 withdrawalCredentials = getWithdrawalCredentials(); if (withdrawalCredentials == 0) revert EmptyWithdrawalsCredentials(); - StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); + StakingModule storage stakingModule = _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); if (StakingModuleStatus(stakingModule.status) != StakingModuleStatus.Active) revert StakingModuleNotActive(); @@ -1423,10 +1399,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version _stakingModuleIndicesOneBased[_stakingModuleId] = _stakingModuleIndex + 1; } - function _getStakingModuleById(uint256 _stakingModuleId) internal view returns (StakingModule storage) { - return _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); - } - function _getIStakingModuleById(uint256 _stakingModuleId) internal view returns (IStakingModule) { return IStakingModule(_getStakingModuleAddressById(_stakingModuleId)); } @@ -1437,7 +1409,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } function _getStakingModuleAddressById(uint256 _stakingModuleId) internal view returns (address) { - return _getStakingModuleById(_stakingModuleId).stakingModuleAddress; + return _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)).stakingModuleAddress; } function _getStorageStakingModulesMapping() internal pure returns (mapping(uint256 => StakingModule) storage result) { @@ -1514,11 +1486,21 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version external onlyRole(REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE) { - _getIStakingModuleById(_stakingModuleId).onValidatorExitTriggered( - _nodeOperatorId, - _publicKey, - _withdrawalRequestPaidFee, - _exitType - ); + try + _getIStakingModuleById(_stakingModuleId).onValidatorExitTriggered( + _nodeOperatorId, + _publicKey, + _withdrawalRequestPaidFee, + _exitType + ) + {} catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the onValidatorExitTriggered() reverts because of the + /// "out of gas" error. Here we assume that the onValidatorExitTriggered() method doesn't + /// have reverts with empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); + emit StakingModuleExitNotificationFailed(_stakingModuleId, _nodeOperatorId, _publicKey); + } } } diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 8df8714ce8..28ca552c64 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -62,14 +62,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); - /** - * @notice Emitted when notifying a staking module about a validator exit fails. - * @param stakingModuleId Id of staking module. - * @param nodeOperatorId Id of node operator. - * @param validatorPubkey Public key of validator. - */ - event StakingModuleExitNotificationFailed(uint256 stakingModuleId, uint256 nodeOperatorId, bytes validatorPubkey); - /** * @notice Thrown when remaining exit requests limit is not enough to cover sender requests * @param requestsCount Amount of requests that were sent for processing @@ -266,23 +258,13 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil for (uint256 i = 0; i < validatorsData.length; ++i) { data = validatorsData[i]; - try - stakingRouter.onValidatorExitTriggered( - data.stakingModuleId, - data.nodeOperatorId, - data.pubkey, - withdrawalRequestPaidFee, - exitType - ) - {} catch (bytes memory lowLevelRevertData) { - /// @dev This check is required to prevent incorrect gas estimation of the method. - /// Without it, Ethereum nodes that use binary search for gas estimation may - /// return an invalid value when the onValidatorExitTriggered() reverts because of the - /// "out of gas" error. Here we assume that the onValidatorExitTriggered() method doesn't - /// have reverts with empty error data except "out of gas". - if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); - emit StakingModuleExitNotificationFailed(data.stakingModuleId, data.nodeOperatorId, data.pubkey); - } + stakingRouter.onValidatorExitTriggered( + data.stakingModuleId, + data.nodeOperatorId, + data.pubkey, + withdrawalRequestPaidFee, + exitType + ); } } diff --git a/test/0.8.9/contracts/StakingRouter__Harness.sol b/test/0.8.9/contracts/StakingRouter__Harness.sol index 737fa222c1..054a39b452 100644 --- a/test/0.8.9/contracts/StakingRouter__Harness.sol +++ b/test/0.8.9/contracts/StakingRouter__Harness.sol @@ -24,7 +24,7 @@ contract StakingRouter__Harness is StakingRouter { } function testing_setStakingModuleStatus(uint256 _stakingModuleId, StakingModuleStatus _status) external { - StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); + StakingModule storage stakingModule = _getStakingModuleByIndex(_getStakingModuleIndexById(_stakingModuleId)); _setStakingModuleStatus(stakingModule, _status); } } From 0408ea3a352d3ff6b9af066009a857638af00db3 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 3 Jun 2025 13:00:00 +0200 Subject: [PATCH 251/405] refactor: remove unused error handling and related test cases for TWG --- contracts/0.8.9/StakingRouter.sol | 4 +- .../0.8.9/TriggerableWithdrawalsGateway.sol | 5 - .../contracts/StakingRouter_MockForTWG.sol | 18 --- .../stakingRouter/stakingRouter.misc.test.ts | 116 ------------------ ...awalGateway.triggerFullWithdrawals.test.ts | 35 ------ 5 files changed, 1 insertion(+), 177 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 078adb0f0e..ecde31f4cc 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -190,10 +190,8 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } /// @notice Finalizes upgrade to v2 (from v1). Can be called only once. - /// @param _priorityExitShareThresholds Array of priority exit share thresholds. - /// @param _maxDepositsPerBlock Array of max deposits per block. - /// @param _minDepositBlockDistances Array of min deposit block distances. /// @dev https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md + /// See historical usage in commit: https://github.com/lidofinance/core/blob/3015a734ed4dd70cfbad5d18c3f68f13ec6a6a60/contracts/0.8.9/StakingRouter.sol#L191 // function finalizeUpgrade_v2( // uint256[] memory _priorityExitShareThresholds, // uint256[] memory _maxDepositsPerBlock, diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 28ca552c64..2f3d1645cf 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -69,11 +69,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil */ error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); - /** - * @notice Thrown when onValidatorExitTriggered() reverts with empty data (e.g., out-of-gas error) - */ - error UnrecoverableModuleError(); - struct ValidatorData { uint256 stakingModuleId; uint256 nodeOperatorId; diff --git a/test/0.8.9/contracts/StakingRouter_MockForTWG.sol b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol index 6ca91bfafa..75bd9d4181 100644 --- a/test/0.8.9/contracts/StakingRouter_MockForTWG.sol +++ b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol @@ -11,17 +11,6 @@ contract StakingRouter__MockForTWG { uint256 exitType ); - bool private shouldRevert; - bool private shouldRevertWithCustomError; - - function setShouldRevert(bool _shouldRevert) external { - shouldRevert = _shouldRevert; - } - - function setShouldRevertWithCustomError(bool _shouldRevert) external { - shouldRevertWithCustomError = _shouldRevert; - } - function onValidatorExitTriggered( uint256 _stakingModuleId, uint256 _nodeOperatorId, @@ -29,13 +18,6 @@ contract StakingRouter__MockForTWG { uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external { - if (shouldRevert) { - revert("some reason"); - } - - if (shouldRevertWithCustomError) { - revert CustomRevertError(42, "custom fail"); - } emit Mock__onValidatorExitTriggered( _stakingModuleId, diff --git a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts index b6077f0620..e355f26a39 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts @@ -74,122 +74,6 @@ describe("StakingRouter.sol:misc", () => { }); }); - context("finalizeUpgrade_v2()", () => { - const STAKE_SHARE_LIMIT = 1_00n; - const PRIORITY_EXIT_SHARE_THRESHOLD = STAKE_SHARE_LIMIT; - const MODULE_FEE = 5_00n; - const TREASURY_FEE = 5_00n; - const MAX_DEPOSITS_PER_BLOCK = 150n; - const MIN_DEPOSIT_BLOCK_DISTANCE = 25n; - - const modulesCount = 3; - const newPriorityExitShareThresholds = [2_01n, 2_02n, 2_03n]; - const newMaxDepositsPerBlock = [201n, 202n, 203n]; - const newMinDepositBlockDistances = [31n, 32n, 33n]; - - beforeEach(async () => { - // initialize staking router - await stakingRouter.initialize(stakingRouterAdmin.address, lido, withdrawalCredentials); - // grant roles - await stakingRouter - .connect(stakingRouterAdmin) - .grantRole(await stakingRouter.STAKING_MODULE_MANAGE_ROLE(), stakingRouterAdmin); - - for (let i = 0; i < modulesCount; i++) { - await stakingRouter - .connect(stakingRouterAdmin) - .addStakingModule( - randomString(8), - certainAddress(`test:staking-router:staking-module-${i}`), - STAKE_SHARE_LIMIT, - PRIORITY_EXIT_SHARE_THRESHOLD, - MODULE_FEE, - TREASURY_FEE, - MAX_DEPOSITS_PER_BLOCK, - MIN_DEPOSIT_BLOCK_DISTANCE, - ); - } - expect(await stakingRouter.getStakingModulesCount()).to.equal(modulesCount); - }); - - it("fails with UnexpectedContractVersion error when called on implementation", async () => { - await expect(impl.finalizeUpgrade_v2([], [], [])) - .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") - .withArgs(MAX_UINT256, 1); - }); - - it("fails with UnexpectedContractVersion error when called on deployed from scratch SRv2", async () => { - await expect(stakingRouter.finalizeUpgrade_v2([], [], [])) - .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") - .withArgs(2, 1); - }); - - context("simulate upgrade from v1", () => { - beforeEach(async () => { - // reset contract version - await stakingRouter.testing_setBaseVersion(1); - }); - - it("fails with ArraysLengthMismatch error when _priorityExitShareThresholds input array length mismatch", async () => { - const wrongPriorityExitShareThresholds = [1n]; - await expect( - stakingRouter.finalizeUpgrade_v2( - wrongPriorityExitShareThresholds, - newMaxDepositsPerBlock, - newMinDepositBlockDistances, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "ArraysLengthMismatch") - .withArgs(3, 1); - }); - - it("fails with ArraysLengthMismatch error when _maxDepositsPerBlock input array length mismatch", async () => { - const wrongMaxDepositsPerBlock = [100n, 101n]; - await expect( - stakingRouter.finalizeUpgrade_v2( - newPriorityExitShareThresholds, - wrongMaxDepositsPerBlock, - newMinDepositBlockDistances, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "ArraysLengthMismatch") - .withArgs(3, 2); - }); - - it("fails with ArraysLengthMismatch error when _minDepositBlockDistances input array length mismatch", async () => { - const wrongMinDepositBlockDistances = [41n, 42n, 43n, 44n]; - await expect( - stakingRouter.finalizeUpgrade_v2( - newPriorityExitShareThresholds, - newMaxDepositsPerBlock, - wrongMinDepositBlockDistances, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "ArraysLengthMismatch") - .withArgs(3, 4); - }); - - it("sets correct contract version", async () => { - expect(await stakingRouter.getContractVersion()).to.equal(1); - await stakingRouter.finalizeUpgrade_v2( - newPriorityExitShareThresholds, - newMaxDepositsPerBlock, - newMinDepositBlockDistances, - ); - expect(await stakingRouter.getContractVersion()).to.be.equal(2); - - const modules = await stakingRouter.getStakingModules(); - expect(modules.length).to.be.equal(modulesCount); - - for (let i = 0; i < modulesCount; i++) { - expect(modules[i].priorityExitShareThreshold).to.be.equal(newPriorityExitShareThresholds[i]); - expect(modules[i].maxDepositsPerBlock).to.be.equal(newMaxDepositsPerBlock[i]); - expect(modules[i].minDepositBlockDistance).to.be.equal(newMinDepositBlockDistances[i]); - } - }); - }); - }); - context("receive", () => { it("Reverts", async () => { await expect( diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 3c9c3a3c9a..4e3a584a60 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -357,39 +357,4 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { ).to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "TooLargeExitsPerFrame"); }); - it("should emit StakingModuleExitNotificationFailed if onValidatorExitTriggered reverts", async () => { - const requests = createValidatorDataList(exitRequests); - - await stakingRouter.setShouldRevert(true); - - const tx = await triggerableWithdrawalsGateway - .connect(authorizedEntity) - .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - - for (const request of exitRequests) { - await expect(tx) - .to.emit(triggerableWithdrawalsGateway, "StakingModuleExitNotificationFailed") - .withArgs(request.moduleId, request.nodeOpId, request.valPubkey); - } - - await stakingRouter.connect(admin).setShouldRevert(false); - }); - - it("should emit StakingModuleExitNotificationFailed with custom error revert reason", async () => { - const requests = createValidatorDataList(exitRequests); - - await stakingRouter.setShouldRevertWithCustomError(true); - - const tx = await triggerableWithdrawalsGateway - .connect(authorizedEntity) - .triggerFullWithdrawals(requests, ZERO_ADDRESS, 0, { value: 4 }); - - for (const request of exitRequests) { - await expect(tx) - .to.emit(triggerableWithdrawalsGateway, "StakingModuleExitNotificationFailed") - .withArgs(request.moduleId, request.nodeOpId, request.valPubkey); - } - - await stakingRouter.setShouldRevertWithCustomError(false); - }); }); From 7887d5e9bfe1581c51f79c4c8d1fda4a57eb2715 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 3 Jun 2025 13:34:29 +0200 Subject: [PATCH 252/405] refactor: remove unused import from stakingRouter tests --- test/0.8.9/stakingRouter/stakingRouter.misc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts index e355f26a39..ffa53748d6 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { DepositContract__MockForBeaconChainDepositor, StakingRouter__Harness } from "typechain-types"; -import { certainAddress, ether, MAX_UINT256, proxify, randomString } from "lib"; +import { certainAddress, ether, proxify } from "lib"; import { Snapshot } from "test/suite"; From 891459eccd1491cb5ba00d7b8637ff080d08b68b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 4 Jun 2025 16:28:24 +0400 Subject: [PATCH 253/405] fix: oracle allow to update delivery time if report was not submitted earlier --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 20 ++ .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- ...r-exit-bus-oracle.submitReportData.test.ts | 238 +++++++++++++++++- 3 files changed, 255 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 0613cd440c..4354735a78 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -561,6 +561,26 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V ); } + function _storeOracleNewHashRequestStatus( + bytes32 exitRequestsHash, + uint32 contractVersion, + uint32 deliveredExitDataTimestamp + ) internal { + mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); + + if (requestStatusMap[exitRequestsHash].deliveredExitDataTimestamp != 0) { + return; + } + + requestStatusMap[exitRequestsHash] = RequestStatus({ + contractVersion: contractVersion, + deliveredExitDataTimestamp: deliveredExitDataTimestamp + }); + + emit RequestsHashSubmitted(exitRequestsHash); + } + + function _storeNewHashRequestStatus( bytes32 exitRequestsHash, uint32 contractVersion, diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index a927875aba..e70d935cc7 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -273,7 +273,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { } function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 contractVersion) internal { - _storeNewHashRequestStatus(exitRequestsHash, uint32(contractVersion), uint32(_getTime())); + _storeOracleNewHashRequestStatus(exitRequestsHash, uint32(contractVersion), uint32(_getTime())); } /// diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 8f2b1c6704..8438ef4db9 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -620,9 +620,9 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { { moduleId: 2, nodeOpId: 2, valIndex: 3, valPubkey: PUBKEYS[2] }, { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[3] }, ]; - const { reportData } = await prepareReportAndSubmitHash(requests); + const data = await encodeExitRequestsDataList(requests); const exitRequestHash = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [reportData.data, reportData.dataFormat]), + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [data, DATA_FORMAT_LIST]), ); const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); @@ -632,8 +632,8 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); const exitRequest = { - dataFormat: reportData.dataFormat, - data: reportData.data, + dataFormat: DATA_FORMAT_LIST, + data, }; const emitTx = await oracle.submitExitRequestsData(exitRequest); @@ -683,4 +683,234 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { .withArgs(requests[3].moduleId, requests[3].nodeOpId, requests[3].valIndex, requests[3].valPubkey, timestamp); }); }); + + context("Allow oracle to submit a report for a previously submitted hash if the original submitter did not.", () => { + let originalState: string; + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + after(async () => await Snapshot.restore(originalState)); + + const validators = [ + { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 2, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[3] }, + ]; + + let exitRequestHash: string; + + it("create hash", async () => { + const data = await encodeExitRequestsDataList(validators); + exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [data, DATA_FORMAT_LIST]), + ); + }); + + it("submit hash by actor different from oracle", async () => { + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + await oracle.grantRole(role, authorizedEntity); + + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); + + const { deliveredExitDataTimestamp } = await oracle.getRequestStatus(exitRequestHash); + expect(deliveredExitDataTimestamp).to.equal(0); + }); + + it("oracle allowed to submit report", async () => { + const { reportData } = await prepareReportAndSubmitHash(validators); + + const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); + const timestamp = await consensus.getTime(); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[0].moduleId, + validators[0].nodeOpId, + validators[0].valIndex, + validators[0].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[1].moduleId, + validators[1].nodeOpId, + validators[1].valIndex, + validators[1].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[2].moduleId, + validators[2].nodeOpId, + validators[2].valIndex, + validators[2].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[3].moduleId, + validators[3].nodeOpId, + validators[3].valIndex, + validators[3].valPubkey, + timestamp, + ); + + const { deliveredExitDataTimestamp } = await oracle.getRequestStatus(exitRequestHash); + expect(deliveredExitDataTimestamp).to.equal(timestamp); + }); + }); + + context( + "Dont allow oracle to change deliveredExitDataTimestamp for a previously submitted report by another submitter.", + () => { + let originalState: string; + const prevTime = 2000; + + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + + await consensus.setTime(prevTime); + + const role = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); + await oracle.grantRole(role, admin); + await oracle.connect(admin).setExitRequestLimit(100, 1, 48); + }); + after(async () => await Snapshot.restore(originalState)); + + const validators = [ + { moduleId: 1, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[0] }, + { moduleId: 1, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[1] }, + { moduleId: 2, nodeOpId: 2, valIndex: 3, valPubkey: PUBKEYS[2] }, + { moduleId: 2, nodeOpId: 3, valIndex: 3, valPubkey: PUBKEYS[3] }, + ]; + + let exitRequestHash: string; + let exitRequests: string; + + it("create hash", async () => { + exitRequests = await encodeExitRequestsDataList(validators); + exitRequestHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [exitRequests, DATA_FORMAT_LIST]), + ); + }); + + it("submit hash by actor different from oracle", async () => { + const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); + await oracle.grantRole(role, authorizedEntity); + + const submitTx = await oracle.connect(authorizedEntity).submitExitRequestsHash(exitRequestHash); + await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); + + const { deliveredExitDataTimestamp } = await oracle.getRequestStatus(exitRequestHash); + expect(deliveredExitDataTimestamp).to.equal(0); + }); + + it("submit report by actor different from oracle", async () => { + const exitRequest = { + dataFormat: DATA_FORMAT_LIST, + data: exitRequests, + }; + + const tx = await oracle.submitExitRequestsData(exitRequest); + const timestamp = await oracle.getTime(); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[0].moduleId, + validators[0].nodeOpId, + validators[0].valIndex, + validators[0].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[1].moduleId, + validators[1].nodeOpId, + validators[1].valIndex, + validators[1].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[2].moduleId, + validators[2].nodeOpId, + validators[2].valIndex, + validators[2].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[3].moduleId, + validators[3].nodeOpId, + validators[3].valIndex, + validators[3].valPubkey, + timestamp, + ); + + const { deliveredExitDataTimestamp } = await oracle.getRequestStatus(exitRequestHash); + expect(deliveredExitDataTimestamp).to.equal(2000); + }); + + it("dont allow oracle to change deliveredExitDataTimestamp", async () => { + await consensus.advanceTimeBy(10); + const { reportData } = await prepareReportAndSubmitHash(validators); + + const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); + const timestamp = await consensus.getTime(); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[1].moduleId, + validators[1].nodeOpId, + validators[1].valIndex, + validators[1].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[2].moduleId, + validators[2].nodeOpId, + validators[2].valIndex, + validators[2].valPubkey, + timestamp, + ); + + await expect(tx) + .to.emit(oracle, "ValidatorExitRequest") + .withArgs( + validators[3].moduleId, + validators[3].nodeOpId, + validators[3].valIndex, + validators[3].valPubkey, + timestamp, + ); + + const { deliveredExitDataTimestamp } = await oracle.getRequestStatus(exitRequestHash); + expect(deliveredExitDataTimestamp).to.equal(prevTime); + + expect(timestamp).to.eq(prevTime + 10); + }); + }, + ); }); From ce097e6e9da99e9c5146943b79661bc1506eb7b4 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 4 Jun 2025 14:33:37 +0200 Subject: [PATCH 254/405] feat: use eip7002 contract byte code in integration tests --- lib/eips/eip7002.ts | 31 ++++++++++++------- test/0.8.9/withdrawalVault/eip7002Mock.ts | 18 ++++++++++- .../withdrawalVault/withdrawalVault.test.ts | 11 +++++-- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/lib/eips/eip7002.ts b/lib/eips/eip7002.ts index 18179bca15..c201b0da17 100644 --- a/lib/eips/eip7002.ts +++ b/lib/eips/eip7002.ts @@ -1,23 +1,18 @@ import { ethers } from "hardhat"; -import { EIP7002WithdrawalRequest__Mock } from "typechain-types"; - +import { impersonate } from "lib"; import { log } from "lib"; // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7002.md#configuration export const EIP7002_ADDRESS = "0x00000961Ef480Eb55e80D19ad83579A64c007002"; export const EIP7002_MIN_WITHDRAWAL_REQUEST_FEE = 1n; -export const deployEIP7002WithdrawalRequestContract = async (fee: bigint): Promise => { - const eip7002Mock = await ethers.deployContract("EIP7002WithdrawalRequest__Mock"); - const eip7002MockAddress = await eip7002Mock.getAddress(); - - await ethers.provider.send("hardhat_setCode", [EIP7002_ADDRESS, await ethers.provider.getCode(eip7002MockAddress)]); - - const contract = await ethers.getContractAt("EIP7002WithdrawalRequest__Mock", EIP7002_ADDRESS); - await contract.mock__setFee(fee); +const EIP7002_RUNTIME_BYTECODE = + "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd"; - return contract; +export const deployEIP7002WithdrawalRequestContract = async (): Promise => { + // Inject the byte-code directly at the fixed address. + await ethers.provider.send("hardhat_setCode", [EIP7002_ADDRESS, EIP7002_RUNTIME_BYTECODE]); }; export const ensureEIP7002WithdrawalRequestContractPresent = async (): Promise => { @@ -26,7 +21,19 @@ export const ensureEIP7002WithdrawalRequestContractPresent = async (): Promise => { + const sysAddress = await impersonate("0xfffffffffffffffffffffffffffffffffffffffe", 999999999999999999999999999n); + + await sysAddress.sendTransaction({ + to: EIP7002_ADDRESS, + value: 0, + }); +}; diff --git a/test/0.8.9/withdrawalVault/eip7002Mock.ts b/test/0.8.9/withdrawalVault/eip7002Mock.ts index 88eb7a2454..7a1093ade8 100644 --- a/test/0.8.9/withdrawalVault/eip7002Mock.ts +++ b/test/0.8.9/withdrawalVault/eip7002Mock.ts @@ -2,12 +2,28 @@ import { expect } from "chai"; import { ContractTransactionReceipt, ContractTransactionResponse } from "ethers"; import { ethers } from "hardhat"; -import { findEventsWithInterfaces } from "lib"; +import { EIP7002WithdrawalRequest__Mock } from "typechain-types"; + +import { EIP7002_ADDRESS, findEventsWithInterfaces } from "lib"; const eventName = "RequestAdded__Mock"; const eip7002MockEventABI = [`event ${eventName}(bytes request, uint256 fee)`]; const eip7002MockInterface = new ethers.Interface(eip7002MockEventABI); +export const deployEIP7002WithdrawalRequestContractMock = async ( + fee: bigint, +): Promise => { + const eip7002Mock = await ethers.deployContract("EIP7002WithdrawalRequest__Mock"); + const eip7002MockAddress = await eip7002Mock.getAddress(); + + await ethers.provider.send("hardhat_setCode", [EIP7002_ADDRESS, await ethers.provider.getCode(eip7002MockAddress)]); + + const contract = await ethers.getContractAt("EIP7002WithdrawalRequest__Mock", EIP7002_ADDRESS); + await contract.mock__setFee(fee); + + return contract; +}; + export function encodeEIP7002Payload(pubkey: string, amount: bigint): string { return `0x${pubkey}${amount.toString(16).padStart(16, "0")}`; } diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index 852c29f482..b125bb4bc3 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -13,11 +13,16 @@ import { WithdrawalVault__Harness, } from "typechain-types"; -import { deployEIP7002WithdrawalRequestContract, EIP7002_ADDRESS, MAX_UINT256, proxify } from "lib"; +import { EIP7002_ADDRESS, EIP7002_MIN_WITHDRAWAL_REQUEST_FEE, MAX_UINT256, proxify } from "lib"; import { Snapshot } from "test/suite"; -import { encodeEIP7002Payload, findEIP7002MockEvents, testEIP7002Mock } from "./eip7002Mock"; +import { + deployEIP7002WithdrawalRequestContractMock, + encodeEIP7002Payload, + findEIP7002MockEvents, + testEIP7002Mock, +} from "./eip7002Mock"; import { generateWithdrawalRequestPayload } from "./utils"; const PETRIFIED_VERSION = MAX_UINT256; @@ -42,7 +47,7 @@ describe("WithdrawalVault.sol", () => { before(async () => { [owner, treasury, triggerableWithdrawalsGateway, stranger] = await ethers.getSigners(); - withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContract(1n); + withdrawalsPredeployed = await deployEIP7002WithdrawalRequestContractMock(EIP7002_MIN_WITHDRAWAL_REQUEST_FEE); expect(await withdrawalsPredeployed.getAddress()).to.equal(EIP7002_ADDRESS); From 1f207ad52ce31b907a13b03650768702fe02fe85 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 4 Jun 2025 18:53:51 +0400 Subject: [PATCH 255/405] fix: added event ExitDataProcessing for oracle submit --- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index e70d935cc7..f2b17163e6 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -161,6 +161,7 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { _startProcessing(); _handleConsensusReportData(data); _storeOracleExitRequestHash(dataHash, contractVersion); + emit ExitDataProcessing(dataHash); } /// @notice Returns the total number of validator exit requests ever processed From 22635774623b904643e08c98eacbaf028ad6e357 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 4 Jun 2025 21:24:57 +0200 Subject: [PATCH 256/405] feat: ignore fails on exit delay report --- .../0.8.25/ValidatorExitDelayVerifier.sol | 41 ++++++- test/0.8.25/contracts/StakingRouter_Mock.sol | 18 ++- .../0.8.25/validatorExitDelayVerifier.test.ts | 112 +++++++++++++++++- .../validatorExitDelayVerifierHelpers.ts | 4 +- .../report-validator-exit-delay.ts | 50 ++++++-- 5 files changed, 207 insertions(+), 18 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 095901715e..c0230b9dbb 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -105,6 +105,16 @@ contract ValidatorExitDelayVerifier { uint256 eligibleExitRequestTimestamp ); error EmptyDeliveryHistory(); + error UnrecoverableModuleError(); + + event ReportValidatorExitDelayFailed( + uint256 moduleId, + uint256 nodeOpId, + bytes pubkey, + uint256 proofSlotTimestamp, + uint256 eligibleToExitInSec + ); + /** * @dev The previous and current forks can be essentially the same. @@ -192,7 +202,7 @@ contract ValidatorExitDelayVerifier { _verifyValidatorExitUnset(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); - stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); + _reportValidatorExitDelay(stakingRouter, moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); } } @@ -239,7 +249,34 @@ contract ValidatorExitDelayVerifier { _verifyValidatorExitUnset(oldBlock.header, witness, pubkey, valIndex); - stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); + _reportValidatorExitDelay(stakingRouter, moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); + } + } + + function _reportValidatorExitDelay( + IStakingRouter stakingRouter, + uint256 moduleId, + uint256 nodeOpId, + uint256 proofSlotTimestamp, + bytes memory pubkey, + uint256 eligibleToExitInSec + ) internal { + try + stakingRouter.reportValidatorExitDelay( + moduleId, + nodeOpId, + proofSlotTimestamp, + pubkey, + eligibleToExitInSec + ) {} + catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the reportValidatorExitDelay() reverts because + /// of the "out of gas" error. Here we assume that the reportValidatorExitDelay() + /// method doesn't have reverts with empty error data except "out of gas". + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); + emit ReportValidatorExitDelayFailed(moduleId, nodeOpId, pubkey, proofSlotTimestamp, eligibleToExitInSec); } } diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol index c9c610d073..78de436421 100644 --- a/test/0.8.25/contracts/StakingRouter_Mock.sol +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -4,8 +4,16 @@ pragma solidity 0.8.25; import {IStakingRouter} from "contracts/0.8.25/interfaces/IStakingRouter.sol"; contract StakingRouter_Mock is IStakingRouter { + bool private onReportingValidatorExitDelayShouldRevert = false; + bool private onReportingValidatorExitDelayShouldRunOutOfGas = false; + + function mock__revertOnReportingValidatorExitDelay(bool shouldRevert, bool shouldRunOutOfGas) external { + onReportingValidatorExitDelayShouldRevert = shouldRevert; + onReportingValidatorExitDelayShouldRunOutOfGas = shouldRunOutOfGas; + } + // An event to track when reportValidatorExitDelay is called - event UnexitedValidatorReported( + event Mock_UnexitedValidatorReported( uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, @@ -20,8 +28,14 @@ contract StakingRouter_Mock is IStakingRouter { bytes calldata publicKey, uint256 secondsSinceEligibleExitRequest ) external { + require(!onReportingValidatorExitDelayShouldRevert, "revert reason"); + + if (onReportingValidatorExitDelayShouldRunOutOfGas) { + revert(); + } + // Emit an event so that testing frameworks can detect this call - emit UnexitedValidatorReported( + emit Mock_UnexitedValidatorReported( moduleId, nodeOperatorId, _proofSlotTimestamp, diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 1446aef40c..689c0a61f4 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -190,7 +190,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + const events = findStakingRouterMockEvents(receipt!, "Mock_UnexitedValidatorReported"); expect(events.length).to.equal(2); const firstEvent = events[0]; @@ -229,6 +229,114 @@ describe("ValidatorExitDelayVerifier.sol", () => { ); }); + it("Reverts with `UnrecoverableModuleError` if the staking router hook fails without reason, e.g. ran out of gas", async () => { + const shouldRunOutOfGas = true; + + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + const moduleId = 1; + const nodeOpId = 2; + const pubkey = ACTIVE_VALIDATOR_PROOF.validator.pubkey; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + await stakingRouter.mock__revertOnReportingValidatorExitDelay(false, shouldRunOutOfGas); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnrecoverableModuleError"); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnrecoverableModuleError"); + }); + + it("accepts a valid proof and does not revert if report delay failed", async () => { + const shouldRevert = true; + + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; + + const moduleId = 1; + const nodeOpId = 2; + const pubkey = ACTIVE_VALIDATOR_PROOF.validator.pubkey; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + await stakingRouter.mock__revertOnReportingValidatorExitDelay(shouldRevert, false); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ) + .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") + .withArgs( + moduleId, + nodeOpId, + pubkey, + proofSlotTimestamp, + intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT, + ); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ) + .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") + .withArgs( + moduleId, + nodeOpId, + pubkey, + proofSlotTimestamp, + intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT, + ); + }); + it("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { const activationEpochTimestamp = GENESIS_TIME + Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT; @@ -259,7 +367,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + const events = findStakingRouterMockEvents(receipt!, "Mock_UnexitedValidatorReported"); expect(events.length).to.equal(1); const event = events[0]; diff --git a/test/0.8.25/validatorExitDelayVerifierHelpers.ts b/test/0.8.25/validatorExitDelayVerifierHelpers.ts index eb9401b80c..a0d66e1531 100644 --- a/test/0.8.25/validatorExitDelayVerifierHelpers.ts +++ b/test/0.8.25/validatorExitDelayVerifierHelpers.ts @@ -42,10 +42,10 @@ export const encodeExitRequestsDataListWithFormat = (requests: ExitRequest[]) => }; const stakingRouterMockEventABI = [ - "event UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", + "event Mock_UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", ]; const stakingRouterMockInterface = new ethers.Interface(stakingRouterMockEventABI); -type StakingRouterMockEvents = "UnexitedValidatorReported"; +type StakingRouterMockEvents = "Mock_UnexitedValidatorReported"; export function findStakingRouterMockEvents(receipt: ContractTransactionReceipt, event: StakingRouterMockEvents) { return findEventsWithInterfaces(receipt!, event, [stakingRouterMockInterface]); diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index ca9566e0ec..7bc1609c97 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -124,7 +124,9 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + ) + .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") + .withArgs(moduleId, nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, proofSlotTimestamp, eligibleToExitInSec); }); it("Should report validator exit delay historically", async () => { @@ -193,10 +195,12 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + ) + .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") + .withArgs(moduleId, nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, proofSlotTimestamp, eligibleToExitInSec); }); - it("Should revert when validator reported multiple times in a single transaction", async () => { + it("Should ignore when validator reported multiple times in a single transaction", async () => { const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; // Setup multiple exit requests with the same pubkey @@ -222,25 +226,51 @@ describe("Report Validator Exit Delay", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const witnesses = nodeOpIds.map((_, index) => toValidatorWitness(ACTIVE_VALIDATOR_PROOF, index)); + // Second item should be ignored await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - witnesses, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], encodedExitRequests, ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + ).to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed"); + }); + + it("Should ignore when validator reported multiple times historically in a single transaction", async () => { + const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + + // Setup multiple exit requests with the same pubkey + const nodeOpIds = [1, 2]; + const exitRequests = nodeOpIds.map((nodeOpId) => ({ + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + })); + + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + const currentBlockTimestamp = await getCurrentBlockTimestamp(); + const proofSlotTimestamp = + (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); + + // Set the block timestamp to 7 days before the proof time + await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); + + await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); + await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + // Second item should be ignored await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - witnesses, + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], encodedExitRequests, ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + ).to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed"); }); it("Should revert when exit request hash is not submitted", async () => { @@ -350,7 +380,7 @@ describe("Report Validator Exit Delay", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); - it("Should revert when reporting validator exit delay before exit deadline threshold", async () => { + it("Should emit `ReportValidatorExitDelayFailed` when reporting validator exit delay before exit deadline threshold", async () => { const { nor, validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; const nodeOpId = 2; @@ -395,6 +425,6 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.be.revertedWith("EXIT_DELAY_BELOW_THRESHOLD"); + ).to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed"); }); }); From 754ebdb3afeaeb0608d0c979466cfc5e9b8d63f6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 5 Jun 2025 12:54:02 +0200 Subject: [PATCH 257/405] feat: upgrade StakingRouter contract to version 3 and update related tests --- contracts/0.8.9/StakingRouter.sol | 48 +++--------- .../stakingRouter/stakingRouter.misc.test.ts | 78 +++---------------- .../stakingRouter.versioned.test.ts | 2 +- 3 files changed, 25 insertions(+), 103 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 3350a11c6b..b5afa01b43 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -169,7 +169,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version if (_admin == address(0)) revert ZeroAddressAdmin(); if (_lido == address(0)) revert ZeroAddressLido(); - _initializeContractVersionTo(2); + _initializeContractVersionTo(3); _setupRole(DEFAULT_ADMIN_ROLE, _admin); @@ -184,42 +184,18 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } /// @notice Finalizes upgrade to v2 (from v1). Can be called only once. - /// @param _priorityExitShareThresholds Array of priority exit share thresholds. - /// @param _maxDepositsPerBlock Array of max deposits per block. - /// @param _minDepositBlockDistances Array of min deposit block distances. /// @dev https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md - function finalizeUpgrade_v2( - uint256[] memory _priorityExitShareThresholds, - uint256[] memory _maxDepositsPerBlock, - uint256[] memory _minDepositBlockDistances - ) external { - _checkContractVersion(1); - - uint256 stakingModulesCount = getStakingModulesCount(); - - _validateEqualArrayLengths(stakingModulesCount, _priorityExitShareThresholds.length); - _validateEqualArrayLengths(stakingModulesCount, _maxDepositsPerBlock.length); - _validateEqualArrayLengths(stakingModulesCount, _minDepositBlockDistances.length); - - for (uint256 i; i < stakingModulesCount; ) { - StakingModule storage stakingModule = _getStakingModuleByIndex(i); - _updateStakingModule( - stakingModule, - stakingModule.id, - stakingModule.stakeShareLimit, - _priorityExitShareThresholds[i], - stakingModule.stakingModuleFee, - stakingModule.treasuryFee, - _maxDepositsPerBlock[i], - _minDepositBlockDistances[i] - ); - - unchecked { - ++i; - } - } - - _updateContractVersion(2); + /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.8.9/StakingRouter.sol#L190 + // function finalizeUpgrade_v2( + // uint256[] memory _priorityExitShareThresholds, + // uint256[] memory _maxDepositsPerBlock, + // uint256[] memory _minDepositBlockDistances + // ) external + + /// @notice Finalizes upgrade to v3 (from v2). Can be called only once. + function finalizeUpgrade_v3() external { + _checkContractVersion(2); + _updateContractVersion(3); } /// @notice Returns Lido contract address. diff --git a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts index b6077f0620..270a578928 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts @@ -62,19 +62,19 @@ describe("StakingRouter.sol:misc", () => { it("Initializes the contract version, sets up roles and variables", async () => { await expect(stakingRouter.initialize(stakingRouterAdmin.address, lido, withdrawalCredentials)) .to.emit(stakingRouter, "ContractVersionSet") - .withArgs(2) + .withArgs(3) .and.to.emit(stakingRouter, "RoleGranted") .withArgs(await stakingRouter.DEFAULT_ADMIN_ROLE(), stakingRouterAdmin.address, user.address) .and.to.emit(stakingRouter, "WithdrawalCredentialsSet") .withArgs(withdrawalCredentials, user.address); - expect(await stakingRouter.getContractVersion()).to.equal(2); + expect(await stakingRouter.getContractVersion()).to.equal(3); expect(await stakingRouter.getLido()).to.equal(lido); expect(await stakingRouter.getWithdrawalCredentials()).to.equal(withdrawalCredentials); }); }); - context("finalizeUpgrade_v2()", () => { + context("finalizeUpgrade_v3()", () => { const STAKE_SHARE_LIMIT = 1_00n; const PRIORITY_EXIT_SHARE_THRESHOLD = STAKE_SHARE_LIMIT; const MODULE_FEE = 5_00n; @@ -83,9 +83,6 @@ describe("StakingRouter.sol:misc", () => { const MIN_DEPOSIT_BLOCK_DISTANCE = 25n; const modulesCount = 3; - const newPriorityExitShareThresholds = [2_01n, 2_02n, 2_03n]; - const newMaxDepositsPerBlock = [201n, 202n, 203n]; - const newMinDepositBlockDistances = [31n, 32n, 33n]; beforeEach(async () => { // initialize staking router @@ -113,79 +110,28 @@ describe("StakingRouter.sol:misc", () => { }); it("fails with UnexpectedContractVersion error when called on implementation", async () => { - await expect(impl.finalizeUpgrade_v2([], [], [])) + await expect(impl.finalizeUpgrade_v3()) .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") - .withArgs(MAX_UINT256, 1); + .withArgs(MAX_UINT256, 2); }); it("fails with UnexpectedContractVersion error when called on deployed from scratch SRv2", async () => { - await expect(stakingRouter.finalizeUpgrade_v2([], [], [])) + await expect(stakingRouter.finalizeUpgrade_v3()) .to.be.revertedWithCustomError(impl, "UnexpectedContractVersion") - .withArgs(2, 1); + .withArgs(3, 2); }); - context("simulate upgrade from v1", () => { + context("simulate upgrade from v2", () => { beforeEach(async () => { // reset contract version - await stakingRouter.testing_setBaseVersion(1); - }); - - it("fails with ArraysLengthMismatch error when _priorityExitShareThresholds input array length mismatch", async () => { - const wrongPriorityExitShareThresholds = [1n]; - await expect( - stakingRouter.finalizeUpgrade_v2( - wrongPriorityExitShareThresholds, - newMaxDepositsPerBlock, - newMinDepositBlockDistances, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "ArraysLengthMismatch") - .withArgs(3, 1); - }); - - it("fails with ArraysLengthMismatch error when _maxDepositsPerBlock input array length mismatch", async () => { - const wrongMaxDepositsPerBlock = [100n, 101n]; - await expect( - stakingRouter.finalizeUpgrade_v2( - newPriorityExitShareThresholds, - wrongMaxDepositsPerBlock, - newMinDepositBlockDistances, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "ArraysLengthMismatch") - .withArgs(3, 2); - }); - - it("fails with ArraysLengthMismatch error when _minDepositBlockDistances input array length mismatch", async () => { - const wrongMinDepositBlockDistances = [41n, 42n, 43n, 44n]; - await expect( - stakingRouter.finalizeUpgrade_v2( - newPriorityExitShareThresholds, - newMaxDepositsPerBlock, - wrongMinDepositBlockDistances, - ), - ) - .to.be.revertedWithCustomError(stakingRouter, "ArraysLengthMismatch") - .withArgs(3, 4); + await stakingRouter.testing_setBaseVersion(2); }); it("sets correct contract version", async () => { - expect(await stakingRouter.getContractVersion()).to.equal(1); - await stakingRouter.finalizeUpgrade_v2( - newPriorityExitShareThresholds, - newMaxDepositsPerBlock, - newMinDepositBlockDistances, + expect(await stakingRouter.getContractVersion()).to.equal(2); + await stakingRouter.finalizeUpgrade_v3( ); - expect(await stakingRouter.getContractVersion()).to.be.equal(2); - - const modules = await stakingRouter.getStakingModules(); - expect(modules.length).to.be.equal(modulesCount); - - for (let i = 0; i < modulesCount; i++) { - expect(modules[i].priorityExitShareThreshold).to.be.equal(newPriorityExitShareThresholds[i]); - expect(modules[i].maxDepositsPerBlock).to.be.equal(newMaxDepositsPerBlock[i]); - expect(modules[i].minDepositBlockDistance).to.be.equal(newMinDepositBlockDistances[i]); - } + expect(await stakingRouter.getContractVersion()).to.be.equal(3); }); }); }); diff --git a/test/0.8.9/stakingRouter/stakingRouter.versioned.test.ts b/test/0.8.9/stakingRouter/stakingRouter.versioned.test.ts index d02a966d13..059ee1148c 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.versioned.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.versioned.test.ts @@ -50,7 +50,7 @@ describe("StakingRouter.sol:Versioned", () => { it("Increments version", async () => { await versioned.initialize(randomAddress(), randomAddress(), randomBytes(32)); - expect(await versioned.getContractVersion()).to.equal(2n); + expect(await versioned.getContractVersion()).to.equal(3n); }); }); }); From e0e6bb56a38ca3f34a6eba22c51a8e34174ea1d7 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 12:44:37 +0200 Subject: [PATCH 258/405] refactor: move for loop from twg to sr --- contracts/0.8.9/StakingRouter.sol | 47 ++++++++++--------- .../0.8.9/TriggerableWithdrawalsGateway.sol | 33 +++++-------- .../contracts/StakingRouter_MockForTWG.sol | 27 ++++++----- .../stakingRouter/stakingRouter.misc.test.ts | 2 +- 4 files changed, 55 insertions(+), 54 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 79ea6a7f6e..6442b309a5 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -134,6 +134,12 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 availableValidatorsCount; } + struct ValidatorExitData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; + } + bytes32 public constant MANAGE_WITHDRAWAL_CREDENTIALS_ROLE = keccak256("MANAGE_WITHDRAWAL_CREDENTIALS_ROLE"); bytes32 public constant STAKING_MODULE_MANAGE_ROLE = keccak256("STAKING_MODULE_MANAGE_ROLE"); bytes32 public constant STAKING_MODULE_UNVETTING_ROLE = keccak256("STAKING_MODULE_UNVETTING_ROLE"); @@ -1472,39 +1478,38 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ); } - /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. - /// @dev This function is called when a validator is exited using the triggerable exit request on the Execution Layer (EL). - /// @param _stakingModuleId The ID of the staking module. - /// @param _nodeOperatorId The ID of the node operator. - /// @param _publicKey The public key of the validator being reported. + /// @notice Handles the triggerable exit event for a set of validators. + /// @dev This function is called when validators are exited using triggerable exit requests on the Execution Layer (EL). + /// @param validatorExitData An array of `ValidatorExitData` structs, each representing a validator + /// for which a triggerable exit was requested. Each entry includes: + /// - `stakingModuleId`: ID of the staking module. + /// - `nodeOperatorId`: ID of the node operator. + /// - `pubkey`: Validator public key, 48 bytes length. /// @param _withdrawalRequestPaidFee Fee amount paid to send a withdrawal request on the Execution Layer (EL). /// @param _exitType The type of exit being performed. - /// This parameter may be interpreted differently across various staking modules, depending on their specific implementation. + /// This parameter may be interpreted differently across various staking modules depending on their specific implementation. function onValidatorExitTriggered( - uint256 _stakingModuleId, - uint256 _nodeOperatorId, - bytes calldata _publicKey, + ValidatorExitData[] calldata validatorExitData, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external onlyRole(REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE) { - try - _getIStakingModuleById(_stakingModuleId).onValidatorExitTriggered( - _nodeOperatorId, - _publicKey, + ValidatorExitData calldata data; + for (uint256 i = 0; i < validatorExitData.length; ++i) { + data = validatorExitData[i]; + + try _getIStakingModuleById(data.stakingModuleId).onValidatorExitTriggered( + data.nodeOperatorId, + data.pubkey, _withdrawalRequestPaidFee, _exitType ) - {} catch (bytes memory lowLevelRevertData) { - /// @dev This check is required to prevent incorrect gas estimation of the method. - /// Without it, Ethereum nodes that use binary search for gas estimation may - /// return an invalid value when the onValidatorExitTriggered() reverts because of the - /// "out of gas" error. Here we assume that the onValidatorExitTriggered() method doesn't - /// have reverts with empty error data except "out of gas". - if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); - emit StakingModuleExitNotificationFailed(_stakingModuleId, _nodeOperatorId, _publicKey); + {} catch (bytes memory lowLevelRevertData) { + if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); + emit StakingModuleExitNotificationFailed(data.stakingModuleId, data.nodeOperatorId, data.pubkey); + } } } } diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 2f3d1645cf..c838636e5b 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -7,6 +7,12 @@ import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; import {PausableUntil} from "./utils/PausableUntil.sol"; +struct ValidatorData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; +} + interface IWithdrawalVault { function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) external payable; @@ -15,9 +21,7 @@ interface IWithdrawalVault { interface IStakingRouter { function onValidatorExitTriggered( - uint256 _stakingModuleId, - uint256 _nodeOperatorId, - bytes calldata _publicKey, + ValidatorData[] calldata validatorData, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external; @@ -69,12 +73,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil */ error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); - struct ValidatorData { - uint256 stakingModuleId; - uint256 nodeOperatorId; - bytes pubkey; - } - bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); bytes32 public constant ADD_FULL_WITHDRAWAL_REQUEST_ROLE = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); @@ -249,18 +247,11 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil uint256 exitType ) internal { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - ValidatorData calldata data; - for (uint256 i = 0; i < validatorsData.length; ++i) { - data = validatorsData[i]; - - stakingRouter.onValidatorExitTriggered( - data.stakingModuleId, - data.nodeOperatorId, - data.pubkey, - withdrawalRequestPaidFee, - exitType - ); - } + stakingRouter.onValidatorExitTriggered( + validatorsData, + withdrawalRequestPaidFee, + exitType + ); } function _refundFee(uint256 refund, address recipient) internal { diff --git a/test/0.8.9/contracts/StakingRouter_MockForTWG.sol b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol index 75bd9d4181..4c92e4b882 100644 --- a/test/0.8.9/contracts/StakingRouter_MockForTWG.sol +++ b/test/0.8.9/contracts/StakingRouter_MockForTWG.sol @@ -1,5 +1,11 @@ pragma solidity 0.8.9; +struct ValidatorData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; +} + contract StakingRouter__MockForTWG { error CustomRevertError(uint256 id, string reason); @@ -12,19 +18,18 @@ contract StakingRouter__MockForTWG { ); function onValidatorExitTriggered( - uint256 _stakingModuleId, - uint256 _nodeOperatorId, - bytes calldata _publicKey, + ValidatorData[] calldata validatorData, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external { - - emit Mock__onValidatorExitTriggered( - _stakingModuleId, - _nodeOperatorId, - _publicKey, - _withdrawalRequestPaidFee, - _exitType - ); + for (uint256 i = 0; i < validatorData.length; ++i) { + emit Mock__onValidatorExitTriggered( + validatorData[i].stakingModuleId, + validatorData[i].nodeOperatorId, + validatorData[i].pubkey, + _withdrawalRequestPaidFee, + _exitType + ); + } } } diff --git a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts index fde9ce92ca..270a578928 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.misc.test.ts @@ -6,7 +6,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { DepositContract__MockForBeaconChainDepositor, StakingRouter__Harness } from "typechain-types"; -import { certainAddress, ether, proxify } from "lib"; +import { certainAddress, ether, MAX_UINT256, proxify, randomString } from "lib"; import { Snapshot } from "test/suite"; From 2dfda2fd144898bcb951d81866321e50732ad2e2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 13:00:44 +0200 Subject: [PATCH 259/405] refactor: update exit delay reporting logic to return instead of revert on invalid conditions --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 15 ++++++-- test/0.4.24/nor/nor.exit.manager.test.ts | 37 ++++++++++--------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c97a321aae..fd8030c4bb 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1146,11 +1146,18 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 _eligibleToExitInSec ) external { _auth(STAKING_ROUTER_ROLE); - require(_publicKey.length == 48, "INVALID_PUBLIC_KEY"); - // Check if exit delay exceeds the threshold - require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); - require(_proofSlotTimestamp - _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "TOO_LATE_FOR_EXIT_DELAY_REPORT"); + if (_publicKey.length != 48) { + return; + } + + if (_eligibleToExitInSec < _exitDeadlineThreshold()) { + return; + } + + if (_proofSlotTimestamp - _eligibleToExitInSec < exitPenaltyCutoffTimestamp()) { + return; + } _markValidatorExitingKeyAsReported(_publicKey); diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index a288463a68..43b5150e72 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -145,12 +145,14 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); }); - it("reverts when public key is empty", async () => { - await expect( - nor - .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, "0x", eligibleToExitInSec), - ).to.be.revertedWith("INVALID_PUBLIC_KEY"); + it("return when public key is empty", async () => { + const tx = nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, "0x", eligibleToExitInSec) + + + await expect(tx).to.not.be.reverted; + await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); it("reverts when reporting the same validator key twice", async () => { @@ -308,17 +310,18 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { expect(result).to.be.true; }); - it("reverts reportValidatorExitDelay when _proofSlotTimestamp < cutoff", async () => { - await expect( - nor - .connect(stakingRouter) - .reportValidatorExitDelay( - firstNodeOperatorId, - cutoff + exitDeadlineThreshold - 1n, - testPublicKey, - eligibleToExitInSec, - ), - ).to.be.revertedWith("TOO_LATE_FOR_EXIT_DELAY_REPORT"); + it("return when _proofSlotTimestamp < cutoff", async () => { + const tx = nor + .connect(stakingRouter) + .reportValidatorExitDelay( + firstNodeOperatorId, + cutoff + exitDeadlineThreshold - 1n, + testPublicKey, + eligibleToExitInSec, + ) + + await expect(tx).to.not.be.reverted; + await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); it("emits event when reportValidatorExitDelay is called with _proofSlotTimestamp >= cutoff", async () => { From 82340e473962384df838a8898da4c5383d12920b Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 13:00:57 +0200 Subject: [PATCH 260/405] Revert "feat: ignore fails on exit delay report" This reverts commit 22635774623b904643e08c98eacbaf028ad6e357. --- .../0.8.25/ValidatorExitDelayVerifier.sol | 41 +------ test/0.8.25/contracts/StakingRouter_Mock.sol | 18 +-- .../0.8.25/validatorExitDelayVerifier.test.ts | 112 +----------------- .../validatorExitDelayVerifierHelpers.ts | 4 +- .../report-validator-exit-delay.ts | 50 ++------ 5 files changed, 18 insertions(+), 207 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index c0230b9dbb..095901715e 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -105,16 +105,6 @@ contract ValidatorExitDelayVerifier { uint256 eligibleExitRequestTimestamp ); error EmptyDeliveryHistory(); - error UnrecoverableModuleError(); - - event ReportValidatorExitDelayFailed( - uint256 moduleId, - uint256 nodeOpId, - bytes pubkey, - uint256 proofSlotTimestamp, - uint256 eligibleToExitInSec - ); - /** * @dev The previous and current forks can be essentially the same. @@ -202,7 +192,7 @@ contract ValidatorExitDelayVerifier { _verifyValidatorExitUnset(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); - _reportValidatorExitDelay(stakingRouter, moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); + stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); } } @@ -249,34 +239,7 @@ contract ValidatorExitDelayVerifier { _verifyValidatorExitUnset(oldBlock.header, witness, pubkey, valIndex); - _reportValidatorExitDelay(stakingRouter, moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); - } - } - - function _reportValidatorExitDelay( - IStakingRouter stakingRouter, - uint256 moduleId, - uint256 nodeOpId, - uint256 proofSlotTimestamp, - bytes memory pubkey, - uint256 eligibleToExitInSec - ) internal { - try - stakingRouter.reportValidatorExitDelay( - moduleId, - nodeOpId, - proofSlotTimestamp, - pubkey, - eligibleToExitInSec - ) {} - catch (bytes memory lowLevelRevertData) { - /// @dev This check is required to prevent incorrect gas estimation of the method. - /// Without it, Ethereum nodes that use binary search for gas estimation may - /// return an invalid value when the reportValidatorExitDelay() reverts because - /// of the "out of gas" error. Here we assume that the reportValidatorExitDelay() - /// method doesn't have reverts with empty error data except "out of gas". - if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); - emit ReportValidatorExitDelayFailed(moduleId, nodeOpId, pubkey, proofSlotTimestamp, eligibleToExitInSec); + stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); } } diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol index 78de436421..c9c610d073 100644 --- a/test/0.8.25/contracts/StakingRouter_Mock.sol +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -4,16 +4,8 @@ pragma solidity 0.8.25; import {IStakingRouter} from "contracts/0.8.25/interfaces/IStakingRouter.sol"; contract StakingRouter_Mock is IStakingRouter { - bool private onReportingValidatorExitDelayShouldRevert = false; - bool private onReportingValidatorExitDelayShouldRunOutOfGas = false; - - function mock__revertOnReportingValidatorExitDelay(bool shouldRevert, bool shouldRunOutOfGas) external { - onReportingValidatorExitDelayShouldRevert = shouldRevert; - onReportingValidatorExitDelayShouldRunOutOfGas = shouldRunOutOfGas; - } - // An event to track when reportValidatorExitDelay is called - event Mock_UnexitedValidatorReported( + event UnexitedValidatorReported( uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, @@ -28,14 +20,8 @@ contract StakingRouter_Mock is IStakingRouter { bytes calldata publicKey, uint256 secondsSinceEligibleExitRequest ) external { - require(!onReportingValidatorExitDelayShouldRevert, "revert reason"); - - if (onReportingValidatorExitDelayShouldRunOutOfGas) { - revert(); - } - // Emit an event so that testing frameworks can detect this call - emit Mock_UnexitedValidatorReported( + emit UnexitedValidatorReported( moduleId, nodeOperatorId, _proofSlotTimestamp, diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 689c0a61f4..1446aef40c 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -190,7 +190,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "Mock_UnexitedValidatorReported"); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); expect(events.length).to.equal(2); const firstEvent = events[0]; @@ -229,114 +229,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { ); }); - it("Reverts with `UnrecoverableModuleError` if the staking router hook fails without reason, e.g. ran out of gas", async () => { - const shouldRunOutOfGas = true; - - const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; - const veboExitRequestTimestamp = - GENESIS_TIME + - (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; - const moduleId = 1; - const nodeOpId = 2; - const pubkey = ACTIVE_VALIDATOR_PROOF.validator.pubkey; - const exitRequests: ExitRequest[] = [ - { - moduleId, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - await stakingRouter.mock__revertOnReportingValidatorExitDelay(false, shouldRunOutOfGas); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnrecoverableModuleError"); - - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "UnrecoverableModuleError"); - }); - - it("accepts a valid proof and does not revert if report delay failed", async () => { - const shouldRevert = true; - - const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; - const veboExitRequestTimestamp = - GENESIS_TIME + - (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; - const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; - - const moduleId = 1; - const nodeOpId = 2; - const pubkey = ACTIVE_VALIDATOR_PROOF.validator.pubkey; - const exitRequests: ExitRequest[] = [ - { - moduleId, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - await stakingRouter.mock__revertOnReportingValidatorExitDelay(shouldRevert, false); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ) - .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") - .withArgs( - moduleId, - nodeOpId, - pubkey, - proofSlotTimestamp, - intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT, - ); - - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ) - .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") - .withArgs( - moduleId, - nodeOpId, - pubkey, - proofSlotTimestamp, - intervalInSlotsBetweenProvableBlockAndExitRequest * SECONDS_PER_SLOT, - ); - }); - it("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { const activationEpochTimestamp = GENESIS_TIME + Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT; @@ -367,7 +259,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "Mock_UnexitedValidatorReported"); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); expect(events.length).to.equal(1); const event = events[0]; diff --git a/test/0.8.25/validatorExitDelayVerifierHelpers.ts b/test/0.8.25/validatorExitDelayVerifierHelpers.ts index a0d66e1531..eb9401b80c 100644 --- a/test/0.8.25/validatorExitDelayVerifierHelpers.ts +++ b/test/0.8.25/validatorExitDelayVerifierHelpers.ts @@ -42,10 +42,10 @@ export const encodeExitRequestsDataListWithFormat = (requests: ExitRequest[]) => }; const stakingRouterMockEventABI = [ - "event Mock_UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", + "event UnexitedValidatorReported(uint256 moduleId, uint256 nodeOperatorId, uint256 proofSlotTimestamp, bytes publicKey, uint256 secondsSinceEligibleExitRequest)", ]; const stakingRouterMockInterface = new ethers.Interface(stakingRouterMockEventABI); -type StakingRouterMockEvents = "Mock_UnexitedValidatorReported"; +type StakingRouterMockEvents = "UnexitedValidatorReported"; export function findStakingRouterMockEvents(receipt: ContractTransactionReceipt, event: StakingRouterMockEvents) { return findEventsWithInterfaces(receipt!, event, [stakingRouterMockInterface]); diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 7bc1609c97..ca9566e0ec 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -124,9 +124,7 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ) - .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") - .withArgs(moduleId, nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, proofSlotTimestamp, eligibleToExitInSec); + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); }); it("Should report validator exit delay historically", async () => { @@ -195,12 +193,10 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ) - .to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed") - .withArgs(moduleId, nodeOpId, ACTIVE_VALIDATOR_PROOF.validator.pubkey, proofSlotTimestamp, eligibleToExitInSec); + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); }); - it("Should ignore when validator reported multiple times in a single transaction", async () => { + it("Should revert when validator reported multiple times in a single transaction", async () => { const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; // Setup multiple exit requests with the same pubkey @@ -226,51 +222,25 @@ describe("Report Validator Exit Delay", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - // Second item should be ignored + const witnesses = nodeOpIds.map((_, index) => toValidatorWitness(ACTIVE_VALIDATOR_PROOF, index)); await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], + witnesses, encodedExitRequests, ), - ).to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed"); - }); - - it("Should ignore when validator reported multiple times historically in a single transaction", async () => { - const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; - - // Setup multiple exit requests with the same pubkey - const nodeOpIds = [1, 2]; - const exitRequests = nodeOpIds.map((nodeOpId) => ({ - moduleId, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - })); - - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - const currentBlockTimestamp = await getCurrentBlockTimestamp(); - const proofSlotTimestamp = - (await validatorExitDelayVerifier.GENESIS_TIME()) + BigInt(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * 12); - - // Set the block timestamp to 7 days before the proof time - await advanceChainTime(proofSlotTimestamp - currentBlockTimestamp - BigInt(3600 * 24 * 7)); - - await validatorsExitBusOracle.connect(vebReportSubmitter).submitExitRequestsHash(encodedExitRequestsHash); - await validatorsExitBusOracle.submitExitRequestsData(encodedExitRequests); + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - // Second item should be ignored await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], + witnesses, encodedExitRequests, ), - ).to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed"); + ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); }); it("Should revert when exit request hash is not submitted", async () => { @@ -380,7 +350,7 @@ describe("Report Validator Exit Delay", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); - it("Should emit `ReportValidatorExitDelayFailed` when reporting validator exit delay before exit deadline threshold", async () => { + it("Should revert when reporting validator exit delay before exit deadline threshold", async () => { const { nor, validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; const nodeOpId = 2; @@ -425,6 +395,6 @@ describe("Report Validator Exit Delay", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], encodedExitRequests, ), - ).to.emit(validatorExitDelayVerifier, "ReportValidatorExitDelayFailed"); + ).to.be.revertedWith("EXIT_DELAY_BELOW_THRESHOLD"); }); }); From 73caecf318743c6687d5a927863f8b7afd488908 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 13:09:12 +0200 Subject: [PATCH 261/405] test: update validator exit delay verification to expect no reversion and no event emission --- test/integration/report-validator-exit-delay.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index ca9566e0ec..44b0ffef5d 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -389,12 +389,13 @@ describe("Report Validator Exit Delay", () => { ), ).to.be.false; - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWith("EXIT_DELAY_BELOW_THRESHOLD"); + const tx = validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ) + + await expect(tx).to.not.be.reverted; + await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); }); From 821f0152336e8f6be2f73761371b7f68a75e73f3 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 13:31:05 +0200 Subject: [PATCH 262/405] refactor: add comment about UnrecoverableModuleError in onValidatorExitTriggered method --- contracts/0.8.9/StakingRouter.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 6442b309a5..03afbb03fc 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1507,6 +1507,12 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version _exitType ) {} catch (bytes memory lowLevelRevertData) { + /// @dev This check is required to prevent incorrect gas estimation of the method. + /// Without it, Ethereum nodes that use binary search for gas estimation may + /// return an invalid value when the onValidatorExitTriggered() + /// reverts because of the "out of gas" error. Here we assume that the + /// onValidatorExitTriggered() method doesn't have reverts with + /// empty error data except "out of gas". if (lowLevelRevertData.length == 0) revert UnrecoverableModuleError(); emit StakingModuleExitNotificationFailed(data.stakingModuleId, data.nodeOperatorId, data.pubkey); } From 0b4a52edb160cd37ff40ec535f12a27cf3443f44 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jun 2025 16:44:39 +0400 Subject: [PATCH 263/405] Update contracts/0.8.9/StakingRouter.sol Co-authored-by: Raman Siamionau <1590415904a@gmail.com> --- contracts/0.8.9/StakingRouter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 03afbb03fc..c40dddd027 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1479,7 +1479,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } /// @notice Handles the triggerable exit event for a set of validators. - /// @dev This function is called when validators are exited using triggerable exit requests on the Execution Layer (EL). + /// @dev This function is called when validators are exited using triggerable exit requests on the Execution Layer. /// @param validatorExitData An array of `ValidatorExitData` structs, each representing a validator /// for which a triggerable exit was requested. Each entry includes: /// - `stakingModuleId`: ID of the staking module. From aaf7fd5d471fa4c75a428b30177a875304a73690 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 14:46:04 +0200 Subject: [PATCH 264/405] Revert "refactor: update exit delay reporting logic to return instead of revert on invalid conditions" This reverts commit 2dfda2fd144898bcb951d81866321e50732ad2e2. --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 15 ++------ test/0.4.24/nor/nor.exit.manager.test.ts | 37 +++++++++---------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index fd8030c4bb..c97a321aae 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1146,18 +1146,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { uint256 _eligibleToExitInSec ) external { _auth(STAKING_ROUTER_ROLE); + require(_publicKey.length == 48, "INVALID_PUBLIC_KEY"); - if (_publicKey.length != 48) { - return; - } - - if (_eligibleToExitInSec < _exitDeadlineThreshold()) { - return; - } - - if (_proofSlotTimestamp - _eligibleToExitInSec < exitPenaltyCutoffTimestamp()) { - return; - } + // Check if exit delay exceeds the threshold + require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); + require(_proofSlotTimestamp - _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "TOO_LATE_FOR_EXIT_DELAY_REPORT"); _markValidatorExitingKeyAsReported(_publicKey); diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index 43b5150e72..a288463a68 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -145,14 +145,12 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, proofSlotTimestamp); }); - it("return when public key is empty", async () => { - const tx = nor - .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, "0x", eligibleToExitInSec) - - - await expect(tx).to.not.be.reverted; - await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); + it("reverts when public key is empty", async () => { + await expect( + nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, "0x", eligibleToExitInSec), + ).to.be.revertedWith("INVALID_PUBLIC_KEY"); }); it("reverts when reporting the same validator key twice", async () => { @@ -310,18 +308,17 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { expect(result).to.be.true; }); - it("return when _proofSlotTimestamp < cutoff", async () => { - const tx = nor - .connect(stakingRouter) - .reportValidatorExitDelay( - firstNodeOperatorId, - cutoff + exitDeadlineThreshold - 1n, - testPublicKey, - eligibleToExitInSec, - ) - - await expect(tx).to.not.be.reverted; - await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); + it("reverts reportValidatorExitDelay when _proofSlotTimestamp < cutoff", async () => { + await expect( + nor + .connect(stakingRouter) + .reportValidatorExitDelay( + firstNodeOperatorId, + cutoff + exitDeadlineThreshold - 1n, + testPublicKey, + eligibleToExitInSec, + ), + ).to.be.revertedWith("TOO_LATE_FOR_EXIT_DELAY_REPORT"); }); it("emits event when reportValidatorExitDelay is called with _proofSlotTimestamp >= cutoff", async () => { From 009b401528aed864c36fd00e7f6c3d739b383ff0 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 14:46:23 +0200 Subject: [PATCH 265/405] Revert "test: update validator exit delay verification to expect no reversion and no event emission" This reverts commit 73caecf318743c6687d5a927863f8b7afd488908. --- test/integration/report-validator-exit-delay.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index 44b0ffef5d..ca9566e0ec 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -389,13 +389,12 @@ describe("Report Validator Exit Delay", () => { ), ).to.be.false; - const tx = validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ) - - await expect(tx).to.not.be.reverted; - await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWith("EXIT_DELAY_BELOW_THRESHOLD"); }); }); From ab81a527831ae35e3d5aceef534bfe281a141632 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 14:51:23 +0200 Subject: [PATCH 266/405] refactor: streamline validator exit reporting logic --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 17 ++++++++--------- test/0.4.24/nor/nor.exit.manager.test.ts | 10 +++++----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c97a321aae..3418ec0ca5 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1046,14 +1046,6 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { return _validatorExitProcessedKeys[processedKeyHash]; } - function _markValidatorExitingKeyAsReported(bytes _publicKey) internal { - bytes32 processedKeyHash = keccak256(_publicKey); - // Require that key is currently NotProcessed - require(_validatorExitProcessedKeys[processedKeyHash] == false, - "VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); - _validatorExitProcessedKeys[processedKeyHash] = true; - } - /// @notice Returns the number of seconds after which a validator is considered late. /// @dev The operatorId argument is ignored and present only to comply with the IStakingModule interface. /// @return uint256 The exit deadline threshold in seconds for all node operators. @@ -1150,9 +1142,16 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { // Check if exit delay exceeds the threshold require(_eligibleToExitInSec >= _exitDeadlineThreshold(), "EXIT_DELAY_BELOW_THRESHOLD"); + // Check if the proof slot timestamp is within the allowed reporting window require(_proofSlotTimestamp - _eligibleToExitInSec >= exitPenaltyCutoffTimestamp(), "TOO_LATE_FOR_EXIT_DELAY_REPORT"); - _markValidatorExitingKeyAsReported(_publicKey); + bytes32 processedKeyHash = keccak256(_publicKey); + // Skip if key is already processed (i.e., not in NotProcessed state) + if (_validatorExitProcessedKeys[processedKeyHash]) { + return; + } + // Mark the validator exit key as processed + _validatorExitProcessedKeys[processedKeyHash] = true; emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index a288463a68..d5be10f3e0 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -157,12 +157,12 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { await nor .connect(stakingRouter) .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); + const tx = nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); - await expect( - nor - .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + await expect(tx).to.not.be.reverted; + await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); }); From c03fc32947a178b9d600a2f71869be0fd8019d80 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 6 Jun 2025 15:16:52 +0200 Subject: [PATCH 267/405] test: update validator exit delay verification to expect successful execution and no event emission --- .../report-validator-exit-delay.ts | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/test/integration/report-validator-exit-delay.ts b/test/integration/report-validator-exit-delay.ts index ca9566e0ec..57b6a71e32 100644 --- a/test/integration/report-validator-exit-delay.ts +++ b/test/integration/report-validator-exit-delay.ts @@ -118,13 +118,14 @@ describe("Report Validator Exit Delay", () => { ), ).to.be.false; - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + const tx = validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ) + + await expect(tx).to.not.be.reverted; + await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); it("Should report validator exit delay historically", async () => { @@ -186,18 +187,19 @@ describe("Report Validator Exit Delay", () => { ), ).to.be.false; - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + const tx = validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ) + + await expect(tx).to.not.be.reverted; + await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); it("Should revert when validator reported multiple times in a single transaction", async () => { - const { validatorsExitBusOracle, validatorExitDelayVerifier } = ctx.contracts; + const { validatorsExitBusOracle, validatorExitDelayVerifier, nor } = ctx.contracts; // Setup multiple exit requests with the same pubkey const nodeOpIds = [1, 2]; @@ -223,24 +225,26 @@ describe("Report Validator Exit Delay", () => { const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); const witnesses = nodeOpIds.map((_, index) => toValidatorWitness(ACTIVE_VALIDATOR_PROOF, index)); - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - witnesses, - encodedExitRequests, - ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + const tx = validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + witnesses, + encodedExitRequests, + ) + + await expect(tx).to.not.be.reverted; + await expect(tx).to.emit(nor, "ValidatorExitStatusUpdated"); const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), - toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), - witnesses, - encodedExitRequests, - ), - ).to.be.revertedWith("VALIDATOR_KEY_NOT_IN_REQUIRED_STATE"); + const tx2 = validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + witnesses, + encodedExitRequests, + ) + + await expect(tx2).to.not.be.reverted; + await expect(tx2).to.not.emit(nor, "ValidatorExitStatusUpdated"); }); it("Should revert when exit request hash is not submitted", async () => { From ac649170587618bc06ed6093bb73fea213f45efd Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 6 Jun 2025 17:36:23 +0400 Subject: [PATCH 268/405] fix: uint/intergation tests --- ...r-exit-bus-oracle.submitReportData.test.ts | 33 +++ ...dator-exit-bus-oracle.triggerExits.test.ts | 40 ++-- ...awalGateway.triggerFullWithdrawals.test.ts | 10 + test/deploy/validatorExitBusOracle.ts | 2 +- test/integration/trigger-full-withdrawals.ts | 17 +- .../validators-exit-bus-single-delivery.ts | 95 --------- ...ators-exit-bus-submit-and-trigger-exits.ts | 192 ++++++++++++++++++ .../validators-exit-bus-trigger-exits.ts | 139 ------------- 8 files changed, 269 insertions(+), 259 deletions(-) delete mode 100644 test/integration/validators-exit-bus-single-delivery.ts create mode 100644 test/integration/validators-exit-bus-submit-and-trigger-exits.ts delete mode 100644 test/integration/validators-exit-bus-trigger-exits.ts diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index 8438ef4db9..f4e9687577 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -913,4 +913,37 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); }, ); + + context("allow oracle to submit empty data", () => { + let originalState: string; + const time = 2000; + beforeEach(async () => { + originalState = await Snapshot.take(); + + await consensus.setTime(time); + }); + + afterEach(async () => await Snapshot.restore(originalState)); + + it("submit report pass", async () => { + const encodedEmptyRequestList = encodeExitRequestsDataList([]); + const exitHash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [encodedEmptyRequestList, DATA_FORMAT_LIST]), + ); + + await expect(oracle.connect(member1).getDeliveryTimestamp(exitHash)).to.be.revertedWithCustomError( + oracle, + "ExitHashNotSubmitted", + ); + + const { reportData } = await prepareReportAndSubmitHash([]); + const tx = await oracle.connect(member1).submitReportData(reportData, oracleVersion); + + await expect(tx).to.not.emit(oracle, "ValidatorExitRequest"); + await expect(tx).to.emit(oracle, "ExitDataProcessing").withArgs(exitHash); + + const timestamp = await oracle.connect(member1).getDeliveryTimestamp(exitHash); + expect(timestamp).to.be.equal(time); + }); + }); }); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index 64ad644e78..ccdf3b81d7 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -185,6 +185,16 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { .withArgs("msg.value"); }); + it("should revert with ZeroArgument error if exitRequestsData is empty", async () => { + await expect( + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [], ZERO_ADDRESS, { + value: 2, + }), + ) + .to.be.revertedWithCustomError(oracle, "ZeroArgument") + .withArgs("exitDataIndexes"); + }); + it("should refund fee to recipient address", async () => { const tx = await oracle.triggerExits( { data: reportFields.data, dataFormat: reportFields.dataFormat }, @@ -267,19 +277,14 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { it("should revert with an error if the key index array is not strictly increasing", async () => { await expect( - oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [1, 2, 2], ZERO_ADDRESS, { + oracle.triggerExits({ data: reportFields.data, dataFormat: reportFields.dataFormat }, [2, 1, 3], ZERO_ADDRESS, { value: 2, }), ).to.be.revertedWithCustomError(oracle, "InvalidExitDataIndexSortOrder"); }); }); - // the only difference in this checks, is that it is possible to get DeliveryWasNotStarted error because of partial delivery describe("Submit via trustfull method", () => { - const MAX_EXIT_REQUESTS_LIMIT = 3; - const EXITS_PER_FRAME = 1; - const FRAME_DURATION = 48; - const exitRequests = [ { moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }, { moduleId: 1, nodeOpId: 0, valIndex: 2, valPubkey: PUBKEYS[1] }, @@ -320,7 +325,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { await expect(submitTx).to.emit(oracle, "RequestsHashSubmitted").withArgs(exitRequestHash); }); - it("should revert if request was not started to deliver", async () => { + it("should revert if request was not delivered", async () => { await expect( oracle.triggerExits( { data: exitRequest.data, dataFormat: exitRequest.dataFormat }, @@ -331,15 +336,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { ).to.be.revertedWithCustomError(oracle, "RequestsNotDelivered"); }); - it("Should deliver request", async () => { - // set limit - const reportLimitRole = await oracle.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); - await oracle.grantRole(reportLimitRole, authorizedEntity); - - await oracle - .connect(authorizedEntity) - .setExitRequestLimit(MAX_EXIT_REQUESTS_LIMIT, EXITS_PER_FRAME, FRAME_DURATION); - + it("Should be executed without errors if request was previously delivered", async () => { const emitTx = await oracle.submitExitRequestsData(exitRequest); const timestamp = await oracle.getTime(); @@ -372,10 +369,13 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { exitRequests[2].valPubkey, timestamp, ); - }); - it("some time passes", async () => { - await consensus.advanceTimeBy(2 * 48); + await oracle.triggerExits( + { data: exitRequest.data, dataFormat: exitRequest.dataFormat }, + [0, 1, 2], + ZERO_ADDRESS, + { value: 4 }, + ); }); it("should revert with error if module id is equal to 0", async () => { @@ -404,7 +404,7 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { }); describe("Version changed", () => { - // version can be changed during deploy + // version should be changed during deploy // but we will change it via accessing storage const VALIDATORS: ExitRequest[] = [{ moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }]; diff --git a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts index 3c9c3a3c9a..66bafeda30 100644 --- a/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts +++ b/test/0.8.9/triggerableWithdrawalGateway.triggerFullWithdrawals.test.ts @@ -95,6 +95,16 @@ describe("TriggerableWithdrawalsGateway.sol:triggerFullWithdrawals", () => { .withArgs("msg.value"); }); + it("should revert with ZeroArgument error if requests count is zero", async () => { + await expect( + triggerableWithdrawalsGateway + .connect(authorizedEntity) + .triggerFullWithdrawals([], ZERO_ADDRESS, 0, { value: 10 }), + ) + .to.be.revertedWithCustomError(triggerableWithdrawalsGateway, "ZeroArgument") + .withArgs("validatorsData"); + }); + it("should revert if total fee value sent is insufficient to cover all provided TW requests ", async () => { const requests = createValidatorDataList(exitRequests); diff --git a/test/deploy/validatorExitBusOracle.ts b/test/deploy/validatorExitBusOracle.ts index ab8a16fd25..0cbced459d 100644 --- a/test/deploy/validatorExitBusOracle.ts +++ b/test/deploy/validatorExitBusOracle.ts @@ -67,7 +67,6 @@ export async function deployVEBO( await updateLidoLocatorImplementation(locatorAddr, { lido: await lido.getAddress(), accountingOracle: await ao.getAddress(), - triggerableWithdrawalsGateway, //: await lido.getAddress(), // await TriggerableWithdrawalsGateway.getAddress(), }); const oracleReportSanityChecker = await deployOracleReportSanityCheckerForExitBus(locatorAddr, admin); @@ -75,6 +74,7 @@ export async function deployVEBO( await updateLidoLocatorImplementation(locatorAddr, { validatorsExitBusOracle: await oracle.getAddress(), oracleReportSanityChecker: await oracleReportSanityChecker.getAddress(), + triggerableWithdrawalsGateway: await triggerableWithdrawalsGateway.getAddress(), }); await consensus.setTime(genesisTime + initialEpoch * slotsPerEpoch * secondsPerSlot); diff --git a/test/integration/trigger-full-withdrawals.ts b/test/integration/trigger-full-withdrawals.ts index e3fe849099..33fb33dd62 100644 --- a/test/integration/trigger-full-withdrawals.ts +++ b/test/integration/trigger-full-withdrawals.ts @@ -5,7 +5,7 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import { StakingRouter, TriggerableWithdrawalsGateway, WithdrawalVault } from "typechain-types"; +import { NodeOperatorsRegistry, StakingRouter, TriggerableWithdrawalsGateway, WithdrawalVault } from "typechain-types"; import { ether } from "lib"; import { getProtocolContext, ProtocolContext } from "lib/protocol"; @@ -19,6 +19,7 @@ describe("TriggerFullWithdrawals Integration", () => { let triggerableWithdrawalsGateway: TriggerableWithdrawalsGateway; let withdrawalVault: WithdrawalVault; let stakingRouter: StakingRouter; + let nor: NodeOperatorsRegistry; let authorizedEntity: HardhatEthersSigner; let stranger: HardhatEthersSigner; let admin: HardhatEthersSigner; @@ -46,6 +47,7 @@ describe("TriggerFullWithdrawals Integration", () => { withdrawalVault = ctx.contracts.withdrawalVault as WithdrawalVault; stakingRouter = ctx.contracts.stakingRouter as StakingRouter; triggerableWithdrawalsGateway = ctx.contracts.triggerableWithdrawalsGateway as TriggerableWithdrawalsGateway; + nor = ctx.contracts.nor as NodeOperatorsRegistry; // Take a snapshot to restore state after tests snapshot = await Snapshot.take(); @@ -147,6 +149,8 @@ describe("TriggerFullWithdrawals Integration", () => { .connect(authorizedEntity) .triggerFullWithdrawals(validatorData, refundRecipient.address, 0, { value: totalAmount }); await expect(tx).to.emit(withdrawalVault, "WithdrawalRequestAdded"); + // check notification of 1 module + await expect(tx).to.emit(nor, "ValidatorExitTriggered"); // Check refund was processed const balanceAfter = await ethers.provider.getBalance(refundRecipient.address); @@ -154,8 +158,8 @@ describe("TriggerFullWithdrawals Integration", () => { // Verify exit limits were consumed const exitLimitInfo = await triggerableWithdrawalsGateway.getExitRequestLimitFullInfo(); - const prevExitRequestsLimit = exitLimitInfo[3]; - expect(prevExitRequestsLimit).to.equal(100n - BigInt(validatorData.length)); + const currentExitRequestsLimit = exitLimitInfo[4]; // currentExitRequestsLimit + expect(currentExitRequestsLimit).to.equal(100n - BigInt(validatorData.length)); }); it("Should successfully trigger full withdrawals with fee refund to sender", async () => { @@ -175,10 +179,12 @@ describe("TriggerFullWithdrawals Integration", () => { .connect(authorizedEntity) .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalAmount }); await expect(tx).to.emit(withdrawalVault, "WithdrawalRequestAdded"); + // check notification of 1 module + await expect(tx).to.emit(nor, "ValidatorExitTriggered"); // Get gas costs const receipt = await tx.wait(); - const gasCost = receipt!.gasUsed * receipt!.gasPrice; + const gasCost = BigInt(receipt!.gasUsed * receipt!.gasPrice); // Check balance after (should be: initial - gas - totalFee) const balanceAfter = await ethers.provider.getBalance(authorizedEntity.address); @@ -229,5 +235,8 @@ describe("TriggerFullWithdrawals Integration", () => { .triggerFullWithdrawals(validatorData, ZeroAddress, 0, { value: totalFee }); await expect(tx).to.emit(withdrawalVault, "WithdrawalRequestAdded"); + + // check notification of 1 module + await expect(tx).to.emit(nor, "ValidatorExitTriggered"); }); }); diff --git a/test/integration/validators-exit-bus-single-delivery.ts b/test/integration/validators-exit-bus-single-delivery.ts deleted file mode 100644 index 219b5bbc1b..0000000000 --- a/test/integration/validators-exit-bus-single-delivery.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect } from "chai"; -import { ethers } from "hardhat"; - -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - -import { ValidatorsExitBusOracle } from "typechain-types"; - -import { de0x, ether, numberToHex } from "lib"; -import { getProtocolContext, ProtocolContext } from "lib/protocol"; - -import { Snapshot } from "test/suite"; - -interface ExitRequest { - moduleId: number; - nodeOpId: number; - valIndex: number; - valPubkey: string; -} - -const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; -}; - -const hashExitRequest = (request: { dataFormat: number; data: string }) => { - return ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), - ); -}; - -describe("ValidatorsExitBus integration", () => { - let ctx: ProtocolContext; - let snapshot: string; - - let veb: ValidatorsExitBusOracle; - let hashReporter: HardhatEthersSigner; - let resumer: HardhatEthersSigner; - let agent: HardhatEthersSigner; - - const moduleId = 1; - const nodeOpId = 2; - const valIndex = 3; - const pubkey = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - const exitRequestPacked = "0x" + encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey: pubkey }); - - before(async () => { - ctx = await getProtocolContext(); - veb = ctx.contracts.validatorsExitBusOracle; - - [hashReporter, resumer] = await ethers.getSigners(); - - agent = await ctx.getSigner("agent", ether("1")); - - // Grant role to submit exit hash - const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); - await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); - - if (await veb.isPaused()) { - const resumeRole = await veb.RESUME_ROLE(); - await veb.connect(agent).grantRole(resumeRole, resumer); - await veb.connect(resumer).resume(); - - expect(veb.isPaused()).to.be.false; - } - }); - - beforeEach(async () => (snapshot = await Snapshot.take())); - afterEach(async () => await Snapshot.restore(snapshot)); - - it("should submit hash and data, updating delivery history", async () => { - const dataFormat = 1; - - const exitRequest = { dataFormat, data: exitRequestPacked }; - - const exitRequestsHash: string = hashExitRequest(exitRequest); - - await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) - .to.emit(veb, "RequestsHashSubmitted") - .withArgs(exitRequestsHash); - - const tx = await veb.submitExitRequestsData(exitRequest); - const receipt = await tx.wait(); - const block = await receipt?.getBlock(); - const blockTimestamp = block!.timestamp; - - await expect(tx) - .to.emit(veb, "ValidatorExitRequest") - .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - - const timestamp = await veb.getDeliveryTimestamp(exitRequestsHash); - expect(timestamp).to.equal(blockTimestamp); - }); -}); diff --git a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts new file mode 100644 index 0000000000..ac24dc3ada --- /dev/null +++ b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts @@ -0,0 +1,192 @@ +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { NodeOperatorsRegistry, ValidatorsExitBusOracle, WithdrawalVault } from "typechain-types"; + +import { de0x, ether, numberToHex } from "lib"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { bailOnFailure, Snapshot } from "test/suite"; + +interface ExitRequest { + moduleId: number; + nodeOpId: number; + valIndex: number; + valPubkey: string; +} + +const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { + const pubkeyHex = de0x(valPubkey); + expect(pubkeyHex.length).to.equal(48 * 2); + return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; +}; + +const hashExitRequest = (request: { dataFormat: number; data: string }) => { + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), + ); +}; + +describe("ValidatorsExitBus integration", () => { + let ctx: ProtocolContext; + let snapshot: string; + + let veb: ValidatorsExitBusOracle; + let wv: WithdrawalVault; + let nor: NodeOperatorsRegistry; + + let hashReporter: HardhatEthersSigner; + let resumer: HardhatEthersSigner; + let agent: HardhatEthersSigner; + let stranger: HardhatEthersSigner; + let refundRecipient: HardhatEthersSigner; + + const dataFormat = 1; + const exitRequestsLength = 5; + + const validatorsExitRequests: ExitRequest[] = [ + { moduleId: 1, nodeOpId: 10, valIndex: 100, valPubkey: "0x" + "11".repeat(48) }, + { moduleId: 1, nodeOpId: 11, valIndex: 101, valPubkey: "0x" + "22".repeat(48) }, + { moduleId: 1, nodeOpId: 12, valIndex: 102, valPubkey: "0x" + "33".repeat(48) }, + { moduleId: 2, nodeOpId: 20, valIndex: 200, valPubkey: "0x" + "44".repeat(48) }, + { moduleId: 2, nodeOpId: 21, valIndex: 201, valPubkey: "0x" + "55".repeat(48) }, + ]; + + const multipleExitRequests = validatorsExitRequests.map((req) => "0x" + encodeExitRequestHex(req)); + const data = multipleExitRequests.reduce((acc, curr) => acc + curr.slice(2), "0x"); + const exitRequest = { dataFormat, data }; + const exitRequestsHash: string = hashExitRequest(exitRequest); + + before(async () => { + ctx = await getProtocolContext(); + veb = ctx.contracts.validatorsExitBusOracle; + wv = ctx.contracts.withdrawalVault; + nor = ctx.contracts.nor; + + [hashReporter, stranger, resumer, refundRecipient] = await ethers.getSigners(); + + agent = await ctx.getSigner("agent", ether("1")); + + // Grant role to submit exit hash + const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); + await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); + + const resumeRole = await veb.RESUME_ROLE(); + const pauseRole = await veb.PAUSE_ROLE(); + const exitRequestLimitManagerRole = await veb.EXIT_REQUEST_LIMIT_MANAGER_ROLE(); + await veb.connect(agent).grantRole(resumeRole, resumer); + await veb.connect(agent).grantRole(pauseRole, resumer); + await veb.connect(agent).grantRole(exitRequestLimitManagerRole, agent); + + if (await veb.isPaused()) { + await veb.connect(resumer).resume(); + + expect(veb.isPaused()).to.be.false; + } + }); + + before(async () => (snapshot = await Snapshot.take())); + beforeEach(bailOnFailure); + after(async () => await Snapshot.restore(snapshot)); + + it("check contract version", async () => {}); + + it("should revert when non-authorized entity tries to submit hash", async () => { + const SUBMIT_REPORT_HASH_ROLE = await veb.SUBMIT_REPORT_HASH_ROLE(); + const hasRole = await veb.hasRole(SUBMIT_REPORT_HASH_ROLE, stranger.address); + expect(hasRole).to.be.false; + + await expect(veb.connect(stranger).submitExitRequestsHash(exitRequestsHash)).to.revertedWithOZAccessControlError( + stranger.address, + await veb.SUBMIT_REPORT_HASH_ROLE(), + ); + }); + + it("should not alow to submit hash or report if veb is paused", async () => { + // pause + await veb.connect(resumer).pauseFor(60); + + // Verify contract is paused + expect(await veb.isPaused()).to.be.true; + + await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)).to.be.revertedWithCustomError( + veb, + "ResumedExpected", + ); + + await expect(veb.submitExitRequestsData(exitRequest)).to.be.revertedWithCustomError(veb, "ResumedExpected"); + }); + + it("should submit hash and data if veb is resumed", async () => { + // Configure exit requests limits + const MAX_LIMIT = 100; + await veb.connect(agent).setExitRequestLimit(MAX_LIMIT, 1, 48); + // Resume the contract + await veb.connect(resumer).resume(); + expect(await veb.isPaused()).to.be.false; + + await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) + .to.emit(veb, "RequestsHashSubmitted") + .withArgs(exitRequestsHash); + + const tx = await veb.submitExitRequestsData(exitRequest); + const receipt = await tx.wait(); + const block = await receipt?.getBlock(); + const blockTimestamp = block!.timestamp; + + for (const { moduleId, nodeOpId, valIndex, valPubkey } of validatorsExitRequests) { + await expect(tx) + .to.emit(veb, "ValidatorExitRequest") + .withArgs(moduleId, nodeOpId, valIndex, valPubkey, blockTimestamp); + } + await expect(tx).to.emit(veb, "ExitDataProcessing").withArgs(exitRequestsHash); + + const timestamp = await veb.getDeliveryTimestamp(exitRequestsHash); + expect(timestamp).to.equal(blockTimestamp); + + // check limit + const exitLimitInfo = await veb.getExitRequestLimitFullInfo(); + const currentExitRequestsLimit = exitLimitInfo[4]; + expect(currentExitRequestsLimit).to.equal(MAX_LIMIT - exitRequestsLength); + }); + + it("should trigger exits", async () => { + const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); + + const triggerExitsTx = await veb + .connect(refundRecipient) + .triggerExits(exitRequest, [0], ZeroAddress, { value: 10 }); + await expect(triggerExitsTx).to.emit(wv, "WithdrawalRequestAdded"); + + // check notification of 1 module + await expect(triggerExitsTx).to.emit(nor, "ValidatorExitTriggered"); + + const ethAfter = await ethers.provider.getBalance(refundRecipient.getAddress()); + + const fee = await wv.getWithdrawalRequestFee(); + + const txReceipt = await triggerExitsTx.wait(); + const gasUsed = BigInt(txReceipt!.gasUsed) * txReceipt!.gasPrice; + + expect(ethAfter).to.equal(ethBefore - gasUsed - fee); + }); + + it("should handle non-zero refundRecipient and refund the correct amount", async () => { + const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); + + const tx = await veb.triggerExits(exitRequest, [0], refundRecipient.getAddress(), { value: 10 }); + await expect(tx).to.emit(wv, "WithdrawalRequestAdded"); + // check notification of 1 module + await expect(tx).to.emit(nor, "ValidatorExitTriggered"); + + const fee = await wv.getWithdrawalRequestFee(); + + const ethAfter = await ethers.provider.getBalance(refundRecipient.getAddress()); + + // Should be increased by (10 - fee) + expect(ethAfter - ethBefore).to.equal(10n - fee); + }); +}); diff --git a/test/integration/validators-exit-bus-trigger-exits.ts b/test/integration/validators-exit-bus-trigger-exits.ts deleted file mode 100644 index e4332ef839..0000000000 --- a/test/integration/validators-exit-bus-trigger-exits.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { expect } from "chai"; -import { ZeroAddress } from "ethers"; -import { ethers } from "hardhat"; - -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - -import { ValidatorsExitBusOracle, WithdrawalVault } from "typechain-types"; - -import { de0x, ether, numberToHex } from "lib"; -import { getProtocolContext, ProtocolContext } from "lib/protocol"; - -import { Snapshot } from "test/suite"; - -interface ExitRequest { - moduleId: number; - nodeOpId: number; - valIndex: number; - valPubkey: string; -} - -const encodeExitRequestHex = ({ moduleId, nodeOpId, valIndex, valPubkey }: ExitRequest) => { - const pubkeyHex = de0x(valPubkey); - expect(pubkeyHex.length).to.equal(48 * 2); - return numberToHex(moduleId, 3) + numberToHex(nodeOpId, 5) + numberToHex(valIndex, 8) + pubkeyHex; -}; - -const hashExitRequest = (request: { dataFormat: number; data: string }) => { - return ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode(["bytes", "uint256"], [request.data, request.dataFormat]), - ); -}; - -describe("ValidatorsExitBus integration", () => { - let ctx: ProtocolContext; - let snapshot: string; - - let veb: ValidatorsExitBusOracle; - let wv: WithdrawalVault; - let hashReporter: HardhatEthersSigner; - let resumer: HardhatEthersSigner; - let agent: HardhatEthersSigner; - let refundRecipient: HardhatEthersSigner; - - const moduleId = 1; - const nodeOpId = 2; - const valIndex = 3; - const pubkey = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - const exitRequestPacked = "0x" + encodeExitRequestHex({ moduleId, nodeOpId, valIndex, valPubkey: pubkey }); - - before(async () => { - ctx = await getProtocolContext(); - veb = ctx.contracts.validatorsExitBusOracle; - wv = ctx.contracts.withdrawalVault; - - [hashReporter, resumer, refundRecipient] = await ethers.getSigners(); - - agent = await ctx.getSigner("agent", ether("1")); - - // Grant role to submit exit hash - const submitReportHashRole = await veb.SUBMIT_REPORT_HASH_ROLE(); - await veb.connect(agent).grantRole(submitReportHashRole, hashReporter); - - if (await veb.isPaused()) { - const resumeRole = await veb.RESUME_ROLE(); - await veb.connect(agent).grantRole(resumeRole, resumer); - await veb.connect(resumer).resume(); - - expect(veb.isPaused()).to.be.false; - } - }); - - beforeEach(async () => (snapshot = await Snapshot.take())); - afterEach(async () => await Snapshot.restore(snapshot)); - - it("should trigger exits", async () => { - const dataFormat = 1; - - const exitRequest = { dataFormat, data: exitRequestPacked }; - - const exitRequestsHash: string = hashExitRequest(exitRequest); - - await expect(veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash)) - .to.emit(veb, "RequestsHashSubmitted") - .withArgs(exitRequestsHash); - - const tx = await veb.submitExitRequestsData(exitRequest); - const receipt = await tx.wait(); - const block = await receipt?.getBlock(); - const blockTimestamp = block!.timestamp; - - await expect(tx) - .to.emit(veb, "ValidatorExitRequest") - .withArgs(moduleId, nodeOpId, valIndex, pubkey, blockTimestamp); - - const timestamp = await veb.getDeliveryTimestamp(exitRequestsHash); - expect(timestamp).to.equal(blockTimestamp); - - const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); - - const triggerExitsTx = await veb - .connect(refundRecipient) - .triggerExits(exitRequest, [0], ZeroAddress, { value: 10 }); - - await expect(triggerExitsTx).to.emit(wv, "WithdrawalRequestAdded"); - - const ethAfter = await ethers.provider.getBalance(refundRecipient.getAddress()); - - const fee = await wv.getWithdrawalRequestFee(); - - const txReceipt = await triggerExitsTx.wait(); - const gasUsed = BigInt(txReceipt!.gasUsed) * txReceipt!.gasPrice; - - expect(ethAfter).to.equal(ethBefore - gasUsed - fee); - }); - - it("should handle non-zero refundRecipient and refund the correct amount", async () => { - const dataFormat = 1; - - const exitRequest = { dataFormat, data: exitRequestPacked }; - - const exitRequestsHash = hashExitRequest(exitRequest); - - await veb.connect(hashReporter).submitExitRequestsHash(exitRequestsHash); - await veb.submitExitRequestsData(exitRequest); - - const ethBefore = await ethers.provider.getBalance(refundRecipient.getAddress()); - - const tx = await veb.triggerExits(exitRequest, [0], refundRecipient.getAddress(), { value: 10 }); - await expect(tx).to.emit(wv, "WithdrawalRequestAdded"); - - const fee = await wv.getWithdrawalRequestFee(); - - const ethAfter = await ethers.provider.getBalance(refundRecipient.getAddress()); - - // Should be increased by (10 - fee) - expect(ethAfter - ethBefore).to.equal(10n - fee); - }); -}); From a6134d0b902c57d55967803a430c9729a450f0bc Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 6 Jun 2025 18:15:13 +0400 Subject: [PATCH 269/405] fix: test --- .../integration/validators-exit-bus-submit-and-trigger-exits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts index ac24dc3ada..e462ab7da9 100644 --- a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts +++ b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts @@ -99,7 +99,7 @@ describe("ValidatorsExitBus integration", () => { const hasRole = await veb.hasRole(SUBMIT_REPORT_HASH_ROLE, stranger.address); expect(hasRole).to.be.false; - await expect(veb.connect(stranger).submitExitRequestsHash(exitRequestsHash)).to.revertedWithOZAccessControlError( + await expect(veb.connect(stranger).submitExitRequestsHash(exitRequestsHash)).to.be.revertedWithOZAccessControlError( stranger.address, await veb.SUBMIT_REPORT_HASH_ROLE(), ); From 1db6c8d1d84193986cdc3edce0a4c53455eb1959 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 6 Jun 2025 18:47:42 +0400 Subject: [PATCH 270/405] fix: role --- .../integration/validators-exit-bus-submit-and-trigger-exits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts index e462ab7da9..bfaa9b2bcd 100644 --- a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts +++ b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts @@ -101,7 +101,7 @@ describe("ValidatorsExitBus integration", () => { await expect(veb.connect(stranger).submitExitRequestsHash(exitRequestsHash)).to.be.revertedWithOZAccessControlError( stranger.address, - await veb.SUBMIT_REPORT_HASH_ROLE(), + SUBMIT_REPORT_HASH_ROLE, ); }); From 213bcfc2698876a13f7da08200ba9a0fb5f1dbdb Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 9 Jun 2025 14:20:18 +0400 Subject: [PATCH 271/405] fix: replace error: prefix in revertedWithOZAccessControlError with empty string --- test/hooks/assertion/revertedWithOZAccessControlError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hooks/assertion/revertedWithOZAccessControlError.ts b/test/hooks/assertion/revertedWithOZAccessControlError.ts index c30dfe2ee1..cf5590fd4c 100644 --- a/test/hooks/assertion/revertedWithOZAccessControlError.ts +++ b/test/hooks/assertion/revertedWithOZAccessControlError.ts @@ -25,7 +25,7 @@ Assertion.addMethod("revertedWithOZAccessControlError", async function (address: try { await ctx; } catch (error) { - const msg = (error as Error).message.toUpperCase(); + const msg = (error as Error).message.toUpperCase().replace(/^error:\s*/i, ""); const reason = `AccessControl: account ${address} is missing role ${role}`; expect(msg).to.equal( From 4bb4b333d541d0b6f4431ca9fc69d322fb09cdac Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Mon, 9 Jun 2025 12:41:12 +0200 Subject: [PATCH 272/405] refactor: swap IncorrectFee args in WithdrawalVaultEIP7002 --- contracts/0.8.9/WithdrawalVaultEIP7002.sol | 4 ++-- test/0.8.9/withdrawalVault/withdrawalVault.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVaultEIP7002.sol b/contracts/0.8.9/WithdrawalVaultEIP7002.sol index 970b54dfbd..d4449939eb 100644 --- a/contracts/0.8.9/WithdrawalVaultEIP7002.sol +++ b/contracts/0.8.9/WithdrawalVaultEIP7002.sol @@ -16,7 +16,7 @@ abstract contract WithdrawalVaultEIP7002 { error ArraysLengthMismatch(uint256 firstArrayLength, uint256 secondArrayLength); error FeeReadFailed(); error FeeInvalidData(); - error IncorrectFee(uint256 providedFee, uint256 requiredFee); + error IncorrectFee(uint256 requiredFee, uint256 providedFee); error RequestAdditionFailed(bytes callData); function _addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) internal { @@ -60,7 +60,7 @@ abstract contract WithdrawalVaultEIP7002 { function _checkFee(uint256 fee) internal view { if (msg.value != fee) { - revert IncorrectFee(msg.value, fee); + revert IncorrectFee(fee, msg.value); } } } diff --git a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts index b125bb4bc3..879db4184b 100644 --- a/test/0.8.9/withdrawalVault/withdrawalVault.test.ts +++ b/test/0.8.9/withdrawalVault/withdrawalVault.test.ts @@ -321,7 +321,7 @@ describe("WithdrawalVault.sol", () => { vault.connect(triggerableWithdrawalsGateway).addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") - .withArgs(0, 3n); + .withArgs(3n, 0); // 2. Should revert if fee is less than required const insufficientFee = 2n; @@ -331,7 +331,7 @@ describe("WithdrawalVault.sol", () => { .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: insufficientFee }), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") - .withArgs(2n, 3n); + .withArgs(3n, 2n); }); it("Should revert if pubkey is not 48 bytes", async function () { @@ -400,7 +400,7 @@ describe("WithdrawalVault.sol", () => { .addWithdrawalRequests(pubkeysHexArray, mixedWithdrawalAmounts, { value: withdrawalFee }), ) .to.be.revertedWithCustomError(vault, "IncorrectFee") - .withArgs(10n, 9n); + .withArgs(9n, 10n); }); ["0x", "0x01", "0x" + "0".repeat(61) + "1", "0x" + "0".repeat(65) + "1"].forEach((unexpectedFee) => { From 4bad6ccc1cfec164285c7786778a298385550648 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 9 Jun 2025 17:19:48 +0400 Subject: [PATCH 273/405] fix: spaces --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 24 ++++++++++--------- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 8 +++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index c838636e5b..278c216b9e 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -41,24 +41,36 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil * @param name Name of the argument that was zero */ error ZeroArgument(string name); + /** * @notice Thrown when attempting to set the admin address to zero */ error AdminCannotBeZero(); + /** * @notice Thrown when exit request has wrong length */ error InvalidRequestsDataLength(); + /** * @notice Thrown when a withdrawal fee insufficient * @param feeRequired Amount of fee required to cover withdrawal request * @param passedValue Amount of fee sent to cover withdrawal request */ error InsufficientFee(uint256 feeRequired, uint256 passedValue); + /** * @notice Thrown when a withdrawal fee refund failed */ error FeeRefundFailed(); + + /** + * @notice Thrown when remaining exit requests limit is not enough to cover sender requests + * @param requestsCount Amount of requests that were sent for processing + * @param remainingLimit Amount of requests that still can be processed at current day + */ + error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); + /** * @notice Emitted when limits configs are set. * @param maxExitRequestsLimit The maximum number of exit requests. @@ -66,12 +78,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); - /** - * @notice Thrown when remaining exit requests limit is not enough to cover sender requests - * @param requestsCount Amount of requests that were sent for processing - * @param remainingLimit Amount of requests that still can be processed at current day - */ - error ExitRequestsLimitExceeded(uint256 requestsCount, uint256 remainingLimit); bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); @@ -247,11 +253,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil uint256 exitType ) internal { IStakingRouter stakingRouter = IStakingRouter(LOCATOR.stakingRouter()); - stakingRouter.onValidatorExitTriggered( - validatorsData, - withdrawalRequestPaidFee, - exitType - ); + stakingRouter.onValidatorExitTriggered(validatorsData, withdrawalRequestPaidFee, exitType); } function _refundFee(uint256 refund, address recipient) internal { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 4354735a78..35b7bea125 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -105,13 +105,12 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V */ error TooManyExitRequestsInReport(uint256 requestsCount, uint256 maxRequestsPerReport); - /// @dev Events - /** * @notice Emitted when an entity with the SUBMIT_REPORT_HASH_ROLE role submits a hash of the exit requests data. * @param exitRequestsHash keccak256 hash of the encoded validators list */ event RequestsHashSubmitted(bytes32 exitRequestsHash); + /** * @notice Emitted when validator exit requested. * @param stakingModuleId Id of staking module. @@ -127,6 +126,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V bytes validatorPubkey, uint256 timestamp ); + /** * @notice Emitted when limits configs are set. * @param maxExitRequestsLimit The maximum number of exit requests. @@ -134,6 +134,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * @param frameDurationInSec The duration of each frame, in seconds, after which `exitsPerFrame` exits can be restored. */ event ExitRequestsLimitSet(uint256 maxExitRequestsLimit, uint256 exitsPerFrame, uint256 frameDurationInSec); + /** * @notice Emitted when exit requests were delivered * @param exitRequestsHash keccak256 hash of the encoded validators list @@ -569,7 +570,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V mapping(bytes32 => RequestStatus) storage requestStatusMap = _storageRequestStatus(); if (requestStatusMap[exitRequestsHash].deliveredExitDataTimestamp != 0) { - return; + return; } requestStatusMap[exitRequestsHash] = RequestStatus({ @@ -580,7 +581,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V emit RequestsHashSubmitted(exitRequestsHash); } - function _storeNewHashRequestStatus( bytes32 exitRequestsHash, uint32 contractVersion, From 29bbfd19971c82b8596279eefbd02552c1e0e746 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 9 Jun 2025 15:37:22 +0200 Subject: [PATCH 274/405] refactor: NOR, SR and AO refactoring --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 41 +++++++++---------- contracts/0.8.9/StakingRouter.sol | 22 +++++----- contracts/0.8.9/interfaces/IStakingModule.sol | 8 ++-- contracts/0.8.9/oracle/AccountingOracle.sol | 2 +- test/0.4.24/nor/nor.aux.test.ts | 4 +- 5 files changed, 38 insertions(+), 39 deletions(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index 3418ec0ca5..df5d755b82 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md @@ -210,7 +210,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { NodeOperatorSummary internal _nodeOperatorSummary; /// @dev Mapping of Node Operator exit delay keys - mapping(bytes32 => bool) internal _validatorExitProcessedKeys; + mapping(bytes32 => bool) internal _validatorProcessedLateKeys; // // METHODS @@ -255,12 +255,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _setExitDeadlineThreshold(_exitDeadlineThresholdInSeconds, 0); } - /// @notice A function to finalize upgrade to v2 (from v1). Can be called only once. + /// @notice A function to finalize upgrade to v2 (from v1). Removed and no longer used. /// For more details see https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md - /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L230 + /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L230 /// function finalizeUpgrade_v2(address _locator, bytes32 _type, uint256 _stuckPenaltyDelay) external - /// @notice A function to finalize upgrade to v3 (from v2). Can be called only once. + /// @notice A function to finalize upgrade to v3 (from v2). Removed and no longer used. /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.4.24/nos/NodeOperatorsRegistry.sol#L298 /// function finalizeUpgrade_v3() external @@ -540,15 +540,12 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { _updateRewardDistributionState(RewardDistributionState.ReadyForDistribution); } - /// @notice [DEPRECATED] `_stuckValidatorsCount` is ignored. /// @notice Unsafely updates the number of validators in the EXITED/STUCK states for node operator with given id. /// @param _nodeOperatorId Id of the node operator /// @param _exitedValidatorsCount New number of EXITED validators for the node operator - /// @dev _stuckValidatorsCount [DEPRECATED] Ignored. function unsafeUpdateValidatorsCount( uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount, - uint256 /* _stuckValidatorsCount */ + uint256 _exitedValidatorsCount ) external { _onlyExistedNodeOperator(_nodeOperatorId); _auth(STAKING_ROUTER_ROLE); @@ -1038,15 +1035,15 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { } /// @notice Returns true if the given validator public key has already been reported as exiting. - /// @dev The function hashes the input public key using keccak256 and checks if it exists in the _validatorExitProcessedKeys mapping. + /// @dev The function hashes the input public key using keccak256 and checks if it exists in the _validatorProcessedLateKeys mapping. /// @param _publicKey The BLS public key of the validator (serialized as bytes). /// @return True if the validator exit for the provided key has been reported, false otherwise. function isValidatorExitingKeyReported(bytes _publicKey) public view returns (bool) { bytes32 processedKeyHash = keccak256(_publicKey); - return _validatorExitProcessedKeys[processedKeyHash]; + return _validatorProcessedLateKeys[processedKeyHash]; } - /// @notice Returns the number of seconds after which a validator is considered late. + /// @notice Returns the number of seconds after which a validator is considered late for specified node operator. /// @dev The operatorId argument is ignored and present only to comply with the IStakingModule interface. /// @return uint256 The exit deadline threshold in seconds for all node operators. function exitDeadlineThreshold(uint256 /* operatorId */) public view returns (uint256) { @@ -1066,17 +1063,17 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @notice Sets the validator exit deadline threshold and the reporting window for late exits. /// @dev Updates the cutoff timestamp before which a validator that was requested to exit cannot be reported as late. /// @param _threshold Number of seconds a validator has to exit after becoming eligible. - /// @param _reportingWindow Additional number of seconds during which a late exit can still be reported. - function setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) external { + /// @param _lateReportingWindow Additional number of seconds during which a late exit can still be reported. + function setExitDeadlineThreshold(uint256 _threshold, uint256 _lateReportingWindow) external { _auth(MANAGE_NODE_OPERATOR_ROLE); - _setExitDeadlineThreshold(_threshold, _reportingWindow); + _setExitDeadlineThreshold(_threshold, _lateReportingWindow); } - function _setExitDeadlineThreshold(uint256 _threshold, uint256 _reportingWindow) internal { + function _setExitDeadlineThreshold(uint256 _threshold, uint256 _lateReportingWindow) internal { require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); // Set the cutoff timestamp to the current time minus the threshold and reportingWindow period - uint256 currentCutoffTimestamp = block.timestamp - _threshold - _reportingWindow; + uint256 currentCutoffTimestamp = block.timestamp - _threshold - _lateReportingWindow; require(exitPenaltyCutoffTimestamp() <= currentCutoffTimestamp, "INVALID_EXIT_PENALTY_CUTOFF_TIMESTAMP"); Packed64x4.Packed memory stats = Packed64x4.Packed(0); @@ -1084,11 +1081,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { stats.set(EXIT_PENALTY_CUTOFF_TIMESTAMP_OFFSET, currentCutoffTimestamp); EXIT_DELAY_STATS.setStorageUint256(stats.v); - emit ExitDeadlineThresholdChanged(_threshold, _reportingWindow); + emit ExitDeadlineThresholdChanged(_threshold, _lateReportingWindow); } /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. - /// @dev This function is called by the StakingRouter when a validator is exited using the triggerable + /// @dev This function is called by the StakingRouter when a validator is triggered to exit using the triggerable /// exit request on the Execution Layer (EL). /// @param _nodeOperatorId The ID of the node operator. /// @param _publicKey The public key of the validator being reported. @@ -1130,7 +1127,7 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered. /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. /// @param _publicKey The public key of the validator being reported. - /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. + /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit after request but has not exited. function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, @@ -1147,11 +1144,11 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { bytes32 processedKeyHash = keccak256(_publicKey); // Skip if key is already processed (i.e., not in NotProcessed state) - if (_validatorExitProcessedKeys[processedKeyHash]) { + if (_validatorProcessedLateKeys[processedKeyHash]) { return; } // Mark the validator exit key as processed - _validatorExitProcessedKeys[processedKeyHash] = true; + _validatorProcessedLateKeys[processedKeyHash] = true; emit ValidatorExitStatusUpdated(_nodeOperatorId, _publicKey, _eligibleToExitInSec, _proofSlotTimestamp); } diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index c40dddd027..6a4fdb8565 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 /* See contracts/COMPILERS.md */ @@ -195,9 +195,9 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version revert DirectETHTransfer(); } - /// @notice Finalizes upgrade to v2 (from v1). Can be called only once. + /// @notice A function to finalize upgrade to v2 (from v1). Removed and no longer used. /// @dev https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-10.md - /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.8.9/StakingRouter.sol#L190 + /// See historical usage in commit: https://github.com/lidofinance/core/blob/c19480aa3366b26aa6eac17f85a6efae8b9f4f72/contracts/0.8.9/StakingRouter.sol#L190 // function finalizeUpgrade_v2( // uint256[] memory _priorityExitShareThresholds, // uint256[] memory _maxDepositsPerBlock, @@ -438,11 +438,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// finish until the new oracle reporting frame is started, in which case staking router will emit a warning /// event `StakingModuleExitedValidatorsIncompleteReporting` when the first data submission phase is performed /// for a new reporting frame. This condition will result in the staking module having an incomplete data about - /// the exited and maybe stuck validator counts during the whole reporting frame. Handling this condition is + /// the exited validator counts during the whole reporting frame. Handling this condition is /// the responsibility of each staking module. /// - /// 4. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the stuck - /// and exited validator counts per node operator for the current reporting frame, the oracle calls + /// 4. When the second reporting phase is finished, i.e. when the oracle submitted the complete data on the exited + /// validator counts per node operator for the current reporting frame, the oracle calls /// `StakingRouter.onValidatorsCountsByNodeOperatorReportingFinished` which, in turn, calls /// `IStakingModule.onExitedAndStuckValidatorsCountsUpdated` on all modules. /// @@ -547,7 +547,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// after applying the corrections. /// @param _correction See the docs for the `ValidatorsCountsCorrection` struct. /// - /// @dev Reverts if the current numbers of exited and stuck validators of the module and node operator + /// @dev Reverts if the current numbers of exited validators of the module and node operator /// don't match the supplied expected current values. /// /// @dev The function is restricted to the `UNSAFE_SET_EXITED_VALIDATORS_ROLE` role. @@ -614,11 +614,11 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version } } - /// @notice Finalizes the reporting of the exited and stuck validators counts for the current + /// @notice Finalizes the reporting of the exited validators counts for the current /// reporting frame. /// /// @dev Called by the oracle when the second phase of data reporting finishes, i.e. when the - /// oracle submitted the complete data on the stuck and exited validator counts per node operator + /// oracle submitted the complete data on the exited validator counts per node operator /// for the current reporting frame. See the docs for `updateExitedValidatorsCountByStakingModule` /// for the description of the overall update process. /// @@ -762,6 +762,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 targetValidatorsCount; /// @notice The number of validators with an expired request to exit time. + /// @dev [deprecated] Stuck key processing has been removed, this field is no longer used. uint256 stuckValidatorsCount; /// @notice The number of validators that can't be withdrawn, but deposit costs were @@ -769,6 +770,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 refundedValidatorsCount; /// @notice A time when the penalty for stuck validators stops applying to node operator rewards. + /// @dev [deprecated] Stuck key processing has been removed, this field is no longer used. uint256 stuckPenaltyEndTimestamp; /// @notice The total number of validators in the EXITED state on the Consensus Layer. @@ -1459,7 +1461,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @param _nodeOperatorId The ID of the node operator whose validator status is being delivered. /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. /// @param _publicKey The public key of the validator being reported. - /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. + /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit after request but has not exited. function reportValidatorExitDelay( uint256 _stakingModuleId, uint256 _nodeOperatorId, diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index def30ea303..be04b2fd1d 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; @@ -22,7 +22,7 @@ interface IStakingModule { /// @param _nodeOperatorId The ID of the node operator whose validator's status is being delivered. /// @param _proofSlotTimestamp The timestamp (slot time) when the validator was last known to be in an active ongoing state. /// @param _publicKey The public key of the validator being reported. - /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit but has not exited. + /// @param _eligibleToExitInSec The duration (in seconds) indicating how long the validator has been eligible to exit after request but has not exited. function reportValidatorExitDelay( uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, @@ -31,7 +31,7 @@ interface IStakingModule { ) external; /// @notice Handles the triggerable exit event for a validator belonging to a specific node operator. - /// @dev This function is called by the StakingRouter when a validator is exited using the triggerable + /// @dev This function is called by the StakingRouter when a validator is triggered to exit using the triggerable /// exit request on the Execution Layer (EL). /// @param _nodeOperatorId The ID of the node operator. /// @param _publicKey The public key of the validator being reported. @@ -58,7 +58,7 @@ interface IStakingModule { uint256 _eligibleToExitInSec ) external view returns (bool); - /// @notice Returns the number of seconds after which a validator is considered late. + /// @notice Returns the number of seconds after which a validator is considered late for specified node operator. /// @param _nodeOperatorId The ID of the node operator. /// @return The exit deadline threshold in seconds. function exitDeadlineThreshold(uint256 _nodeOperatorId) external view returns (uint256); diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 5801ff0e12..4273b017f8 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index df103ff93a..90f9673c9f 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -150,11 +150,11 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { }); it("Reverts if no such an operator exists", async () => { - await expect(nor.unsafeUpdateValidatorsCount(3n, 0n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); + await expect(nor.unsafeUpdateValidatorsCount(3n, 0n)).to.be.revertedWith("OUT_OF_RANGE"); }); it("Reverts if has not STAKING_ROUTER_ROLE assigned", async () => { - await expect(nor.connect(stranger).unsafeUpdateValidatorsCount(firstNodeOperatorId, 0n, 0n)).to.be.revertedWith( + await expect(nor.connect(stranger).unsafeUpdateValidatorsCount(firstNodeOperatorId, 0n)).to.be.revertedWith( "APP_AUTH_FAILED", ); }); From a826afe7f1713b335f3b5c368b5a2310d8e83793 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 9 Jun 2025 15:52:11 +0200 Subject: [PATCH 275/405] fix: update copyright year to 2025 in ValidatorsExitBusOracle.sol --- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index f2b17163e6..6675c595b5 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; From cea798f6e14a9b35ae8497701364fdcae5243b93 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 10 Jun 2025 16:11:40 +0200 Subject: [PATCH 276/405] feat: full unit tests coverage for NOR --- .../NodeOperatorsRegistry__Harness.sol | 9 +++ test/0.4.24/nor/nor.aux.test.ts | 30 ++++++++ test/0.4.24/nor/nor.exit.manager.test.ts | 44 ++++++++--- .../0.4.24/nor/nor.initialize.upgrade.test.ts | 76 +++++++++++++++++++ test/0.4.24/nor/nor.management.flow.test.ts | 37 +++++++++ 5 files changed, 184 insertions(+), 12 deletions(-) diff --git a/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol b/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol index e0977e690f..457b1448e2 100644 --- a/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol +++ b/test/0.4.24/contracts/NodeOperatorsRegistry__Harness.sol @@ -15,6 +15,15 @@ contract NodeOperatorsRegistry__Harness is NodeOperatorsRegistry { initialized(); } + function harness__initializeWithLocator( + uint256 _initialVersion, + address _locator + ) external { + _setContractVersion(_initialVersion); + LIDO_LOCATOR_POSITION.setStorageAddress(_locator); + initialized(); + } + function harness__setDepositedSigningKeysCount(uint256 _nodeOperatorId, uint256 _depositedSigningKeysCount) public { _onlyExistedNodeOperator(_nodeOperatorId); // NodeOperator storage nodeOperator = _nodeOperators[_nodeOperatorId]; diff --git a/test/0.4.24/nor/nor.aux.test.ts b/test/0.4.24/nor/nor.aux.test.ts index 90f9673c9f..7410dcfb3c 100644 --- a/test/0.4.24/nor/nor.aux.test.ts +++ b/test/0.4.24/nor/nor.aux.test.ts @@ -158,6 +158,36 @@ describe("NodeOperatorsRegistry.sol:auxiliary", () => { "APP_AUTH_FAILED", ); }); + + it("Can change exited keys arbitrary (even decreasing exited)", async () => { + const nonce = await nor.getNonce(); + + const beforeNOSummary = await nor.getNodeOperatorSummary(firstNodeOperatorId); + expect(beforeNOSummary.stuckValidatorsCount).to.equal(0n); + expect(beforeNOSummary.totalExitedValidators).to.equal(1n); + + await expect(nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperatorId, 3n)) + .to.emit(nor, "ExitedSigningKeysCountChanged") + .withArgs(firstNodeOperatorId, 3n) + .to.emit(nor, "KeysOpIndexSet") + .withArgs(nonce + 1n) + .to.emit(nor, "NonceChanged") + .withArgs(nonce + 1n); + + const middleNOSummary = await nor.getNodeOperatorSummary(firstNodeOperatorId); + expect(middleNOSummary.totalExitedValidators).to.equal(3n); + + await expect(nor.connect(stakingRouter).unsafeUpdateValidatorsCount(firstNodeOperatorId, 1n)) + .to.emit(nor, "ExitedSigningKeysCountChanged") + .withArgs(firstNodeOperatorId, 1n) + .to.emit(nor, "KeysOpIndexSet") + .withArgs(nonce + 2n) + .to.emit(nor, "NonceChanged") + .withArgs(nonce + 2n); + + const lastNOSummary = await nor.getNodeOperatorSummary(firstNodeOperatorId); + expect(lastNOSummary.totalExitedValidators).to.equal(1n); + }); }); context("onWithdrawalCredentialsChanged", () => { diff --git a/test/0.4.24/nor/nor.exit.manager.test.ts b/test/0.4.24/nor/nor.exit.manager.test.ts index d5be10f3e0..79b75733c8 100644 --- a/test/0.4.24/nor/nor.exit.manager.test.ts +++ b/test/0.4.24/nor/nor.exit.manager.test.ts @@ -120,6 +120,19 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { afterEach(async () => (originalState = await Snapshot.refresh(originalState))); + context("backward compatibility test", () => { + it("isOperatorPenalized", async () => { + expect(await nor.isOperatorPenalized(firstNodeOperatorId)).to.be.false; + }); + + it("isOperatorPenaltyCleared", async () => { + expect(await nor.isOperatorPenaltyCleared(firstNodeOperatorId)).to.be.true; + }); + it("getStuckPenaltyDelay", async () => { + expect(await nor.getStuckPenaltyDelay()).to.be.equal(0n); + }); + }); + context("reportValidatorExitDelay", () => { it("reverts when called by sender without STAKING_ROUTER_ROLE", async () => { expect(await acl["hasPermission(address,address,bytes32)"](stranger, nor, await nor.STAKING_ROUTER_ROLE())).to.be @@ -157,9 +170,9 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { await nor .connect(stakingRouter) .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); - const tx = nor - .connect(stakingRouter) - .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); + const tx = nor + .connect(stakingRouter) + .reportValidatorExitDelay(firstNodeOperatorId, proofSlotTimestamp, testPublicKey, eligibleToExitInSec); await expect(tx).to.not.be.reverted; await expect(tx).to.not.emit(nor, "ValidatorExitStatusUpdated"); @@ -246,15 +259,19 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { }); context("exitPenaltyCutoffTimestamp", () => { - const reportingWindow = 3600n; // 1 hour let cutoff: bigint; beforeEach(async () => { - await deployer.provider.send("hardhat_mine", [`0x${(BigInt(await deployer.provider.getBlockNumber()) + 3000n).toString(16)}`, 12000]); + await deployer.provider.send("hardhat_mine", [ + `0x${(BigInt(await deployer.provider.getBlockNumber()) + 3000n).toString(16)}`, + 12000, + ]); - const tx = await nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(exitDeadlineThreshold, reportingWindow); + const tx = await nor + .connect(nodeOperatorsManager) + .setExitDeadlineThreshold(exitDeadlineThreshold, reportingWindow); // Fetch actual cutoff timestamp from the contract cutoff = BigInt(await nor.exitPenaltyCutoffTimestamp()); @@ -269,12 +286,7 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { it("reverts oldCutoffTimestamp <= currentCutoffTimestamp", async () => { await expect( - nor - .connect(nodeOperatorsManager) - .setExitDeadlineThreshold( - eligibleToExitInSec, - eligibleToExitInSec + 100_000n, - ), + nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(eligibleToExitInSec, eligibleToExitInSec + 100_000n), ).to.be.revertedWith("INVALID_EXIT_PENALTY_CUTOFF_TIMESTAMP"); }); @@ -334,6 +346,14 @@ describe("NodeOperatorsRegistry.sol:ExitManager", () => { ) .to.emit(nor, "ValidatorExitStatusUpdated") .withArgs(firstNodeOperatorId, testPublicKey, eligibleToExitInSec, cutoff + exitDeadlineThreshold); + + const result = await nor.isValidatorExitDelayPenaltyApplicable( + firstNodeOperatorId, + cutoff + exitDeadlineThreshold, + testPublicKey, + eligibleToExitInSec, + ); + expect(result).to.be.false; }); }); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index 232738f367..d5466c6216 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -126,4 +126,80 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { expect(await nor.getType()).to.equal(moduleType); }); }); + + context("finalizeUpgrade_v4()", () => { + let preInitState: string; + beforeEach(async () => { + locator = await deployLidoLocator({ lido: lido }); + preInitState = await Snapshot.take(); + await nor.harness__initializeWithLocator(2n, locator.getAddress()); + }); + + it("Reverts if contract is not initialized", async () => { + await Snapshot.restore(preInitState); // Restore to uninitialized state + await expect(nor.finalizeUpgrade_v4(86400n)).to.be.revertedWith("CONTRACT_NOT_INITIALIZED"); + }); + + it("Reverts if contract version is not 3", async () => { + // Version is currently 2 from harness__initialize(2n) + await expect(nor.finalizeUpgrade_v4(86400n)).to.be.revertedWith("UNEXPECTED_CONTRACT_VERSION"); + }); + + it("Successfully upgrades from v3 to v4", async () => { + // First upgrade to v3 + await nor.harness__setBaseVersion(3n); + + // Get burner address from locator + const burnerAddress = await locator.burner(); + + // Set initial allowance to a non-zero value to verify it gets reset + await lido.connect(deployer).approve(burnerAddress, 100); + expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.be.eq(0); + + // Perform the upgrade to v4 + await expect(nor.finalizeUpgrade_v4(86400n)) + .to.emit(nor, "ContractVersionSet") + .withArgs(4n) + .and.to.emit(nor, "ExitDeadlineThresholdChanged") + .withArgs(86400n, 0n); + + // Verify contract version updated to 4 + expect(await nor.getContractVersion()).to.equal(4n); + + // Verify allowance reset to 0 + expect(await lido.allowance(await nor.getAddress(), burnerAddress)).to.equal(0n); + + // Verify exit deadline threshold was set correctly + expect(await nor.exitDeadlineThreshold(0)).to.equal(86400n); + }); + + it("Works with different exit deadline threshold values", async () => { + // Upgrade to v3 first + await nor.harness__setBaseVersion(3n); + + const customThreshold = 172800n; // 2 days in seconds + await nor.finalizeUpgrade_v4(customThreshold); + + expect(await nor.exitDeadlineThreshold(0)).to.equal(customThreshold); + }); + + it("Calls _initialize_v4 with correct parameters", async () => { + // Upgrade to v3 first + await nor.harness__setBaseVersion(3n); + + // Mock the _initialize_v4 function to track calls + // This is a simplified approach since we can't easily mock internal functions + // We'll verify through events and state changes instead + + await nor.finalizeUpgrade_v4(86400n); + + // Verify expected state changes from _initialize_v4 + expect(await nor.getContractVersion()).to.equal(4n); + expect(await nor.exitDeadlineThreshold(0)).to.equal(86400n); + + // Verify exit penalty cutoff timestamp is set correctly (this is done in _setExitDeadlineThreshold) + const currentTimestamp = await time.latest(); + expect(await nor.exitPenaltyCutoffTimestamp()).to.be.lte(currentTimestamp); + }); + }); }); diff --git a/test/0.4.24/nor/nor.management.flow.test.ts b/test/0.4.24/nor/nor.management.flow.test.ts index 2943730683..3c1622a8fe 100644 --- a/test/0.4.24/nor/nor.management.flow.test.ts +++ b/test/0.4.24/nor/nor.management.flow.test.ts @@ -692,6 +692,43 @@ describe("NodeOperatorsRegistry.sol:management", () => { .and.to.emit(nor, "RewardsDistributed") .withArgs(await user2.getAddress(), ether("7")); }); + + it("distribute with stopped works", async () => { + const totalRewardShares = ether("10"); + + await lido.setTotalPooledEther(ether("100")); + await lido.mintShares(await nor.getAddress(), totalRewardShares); + + // before + // operatorId | Total | Deposited | Exited | Active (deposited-exited) + // 0 3 3 0 3 + // 1 7 7 0 7 + // 2 0 0 0 0 + // ----------------------------------------------------------------------------- + // total 3 10 10 0 10 + // + // perValidatorShare 10*10^18 / 10 = 10^18 + + // update [operator, exited, stuck] + await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(0, 1); + await nor.connect(stakingRouter).unsafeUpdateValidatorsCount(1, 1); + + // after + // operatorId | Total | Deposited | Exited | Stuck | Active (deposited-exited) + // 0 3 3 1 0 2 + // 1 7 7 1 0 6 + // 2 0 0 0 0 0 + // ----------------------------------------------------------------------------- + // total 3 10 10 2 0 8 + // + // perValidatorShare 10*10^18 / 8 = 1250000000000000000 == 1.25 * 10^18 + + await expect(nor.distributeReward()) + .to.emit(nor, "RewardsDistributed") + .withArgs(await user1.getAddress(), ether(2 * 1.25 + "")) + .and.to.emit(nor, "RewardsDistributed") + .withArgs(await user2.getAddress(), ether(6 * 1.25 + "")); + }); }); context("getNodeOperatorIds", () => { From 0f47ad08e7780e649399b520e7ce92ab23774ac4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 10 Jun 2025 18:35:29 +0200 Subject: [PATCH 277/405] feat: add mock staking module and tests for tw functionality --- ...gModule__MockForTriggerableWithdrawals.sol | 147 +++++++++++++ .../stakingRouter/stakingRouter.exit.test.ts | 207 ++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol create mode 100644 test/0.8.9/stakingRouter/stakingRouter.exit.test.ts diff --git a/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol b/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol new file mode 100644 index 0000000000..b1a3fdf905 --- /dev/null +++ b/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.9; + +import {IStakingModule} from "contracts/0.8.9/interfaces/IStakingModule.sol"; + +contract StakingModule__MockForTriggerableWithdrawals is IStakingModule { + uint256 private _nonce; + uint256[] private _nodeOperatorIds; + bool private _onValidatorExitTriggeredResponse = true; + string private _revertReason = "Mock revert"; + bool private _revertWithEmptyReason = false; + + // State control functions + function setOnValidatorExitTriggeredResponse(bool response) external { + _onValidatorExitTriggeredResponse = response; + } + + function setRevertReason(string memory reason) external { + _revertReason = reason; + } + + function setRevertWithEmptyReason(bool value) external { + _revertWithEmptyReason = value; + } + + // Additional required implementations + function exitDeadlineThreshold(uint256) external pure override returns (uint256) { + return 7 days; // Default value for testing + } + + function getType() external pure override returns (bytes32) { + return keccak256("MOCK_STAKING_MODULE"); + } + + function isValidatorExitDelayPenaltyApplicable( + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes calldata _publicKey, + uint256 _eligibleToExitInSec + ) external pure override returns (bool) { + return false; // Default value for testing + } + + // IStakingModule implementations + function obtainDepositData(uint256 count, bytes calldata) external pure override returns (bytes memory publicKeys, bytes memory signatures) { + publicKeys = new bytes(count * 48); + signatures = new bytes(count * 96); + return (publicKeys, signatures); + } + + function onWithdrawalCredentialsChanged() external pure override { + return; + } + + function onRewardsMinted(uint256) external pure override { + return; + } + + function getNonce() external view override returns (uint256) { + return _nonce; + } + + function getStakingModuleSummary() external pure override returns (uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount) { + return (0, 0, 0); + } + + function getNodeOperatorSummary(uint256) external pure override returns ( + uint256 targetLimitMode, + uint256 targetValidatorsCount, + uint256 stuckValidatorsCount, + uint256 refundedValidatorsCount, + uint256 stuckPenaltyEndTimestamp, + uint256 totalExitedValidators, + uint256 totalDepositedValidators, + uint256 depositableValidatorsCount + ) { + return (0, 0, 0, 0, 0, 0, 0, 0); + } + + function getNodeOperatorsCount() external view override returns (uint256) { + return 1; + } + + function getActiveNodeOperatorsCount() external view override returns (uint256) { + return 1; + } + + function getNodeOperatorIds(uint256, uint256) external view override returns (uint256[] memory) { + return _nodeOperatorIds; + } + + function getNodeOperatorIsActive(uint256) external pure override returns (bool) { + return true; + } + + function updateTargetValidatorsLimits(uint256, uint256, uint256) external pure override { + return; + } + + function updateRefundedValidatorsCount(uint256, uint256) external pure override { + return; + } + + function updateExitedValidatorsCount(bytes calldata, bytes calldata) external pure override { + return; + } + + function onExitedAndStuckValidatorsCountsUpdated() external pure override { + return; + } + + function decreaseVettedSigningKeysCount(bytes calldata, bytes calldata) external pure override { + return; + } + + function unsafeUpdateValidatorsCount(uint256, uint256) external pure override { + return; + } + + // The functions we are testing + function reportValidatorExitDelay( + uint256, + uint256, + bytes calldata, + uint256 + ) external pure override { + return; + } + + function onValidatorExitTriggered( + uint256, + bytes calldata, + uint256, + uint256 + ) external view override { + if (!_onValidatorExitTriggeredResponse) { + if (_revertWithEmptyReason) { + assembly { + revert(0, 0) + } + } + revert(_revertReason); + } + } +} diff --git a/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts b/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts new file mode 100644 index 0000000000..d8f849cc03 --- /dev/null +++ b/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts @@ -0,0 +1,207 @@ +import { expect } from "chai"; +import { hexlify, randomBytes, ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { + DepositContract__MockForBeaconChainDepositor, + StakingModule__MockForTriggerableWithdrawals, + StakingRouter__Harness, +} from "typechain-types"; + +import { certainAddress, ether, proxify, randomString } from "lib"; + +import { Snapshot } from "test/suite"; + +describe("StakingRouter.sol:exit", () => { + let deployer: HardhatEthersSigner; + let proxyAdmin: HardhatEthersSigner; + let stakingRouterAdmin: HardhatEthersSigner; + let user: HardhatEthersSigner; + let reporter: HardhatEthersSigner; + + let depositContract: DepositContract__MockForBeaconChainDepositor; + let stakingRouter: StakingRouter__Harness; + let stakingModule: StakingModule__MockForTriggerableWithdrawals; + + let originalState: string; + + const lido = certainAddress("test:staking-router:lido"); + const withdrawalCredentials = hexlify(randomBytes(32)); + const STAKE_SHARE_LIMIT = 1_00n; + const PRIORITY_EXIT_SHARE_THRESHOLD = STAKE_SHARE_LIMIT; + const MODULE_FEE = 5_00n; + const TREASURY_FEE = 5_00n; + const MAX_DEPOSITS_PER_BLOCK = 150n; + const MIN_DEPOSIT_BLOCK_DISTANCE = 25n; + const STAKING_MODULE_ID = 1n; + const NODE_OPERATOR_ID = 1n; + + before(async () => { + [deployer, proxyAdmin, stakingRouterAdmin, user, reporter] = await ethers.getSigners(); + + depositContract = await ethers.deployContract("DepositContract__MockForBeaconChainDepositor", deployer); + const allocLib = await ethers.deployContract("MinFirstAllocationStrategy", deployer); + const stakingRouterFactory = await ethers.getContractFactory("StakingRouter__Harness", { + libraries: { + ["contracts/common/lib/MinFirstAllocationStrategy.sol:MinFirstAllocationStrategy"]: await allocLib.getAddress(), + }, + }); + + const impl = await stakingRouterFactory.connect(deployer).deploy(depositContract); + [stakingRouter] = await proxify({ impl, admin: proxyAdmin, caller: user }); + + // Initialize StakingRouter + await stakingRouter.initialize(stakingRouterAdmin.address, lido, withdrawalCredentials); + + // Deploy mock staking module + stakingModule = await ethers.deployContract("StakingModule__MockForTriggerableWithdrawals", deployer); + + // Grant roles to admin + await stakingRouter + .connect(stakingRouterAdmin) + .grantRole(await stakingRouter.STAKING_MODULE_MANAGE_ROLE(), stakingRouterAdmin); + + // Add staking module + await stakingRouter + .connect(stakingRouterAdmin) + .addStakingModule( + randomString(8), + await stakingModule.getAddress(), + STAKE_SHARE_LIMIT, + PRIORITY_EXIT_SHARE_THRESHOLD, + MODULE_FEE, + TREASURY_FEE, + MAX_DEPOSITS_PER_BLOCK, + MIN_DEPOSIT_BLOCK_DISTANCE, + ); + + // Grant necessary roles to reporter + await stakingRouter + .connect(stakingRouterAdmin) + .grantRole(await stakingRouter.REPORT_VALIDATOR_EXITING_STATUS_ROLE(), reporter); + + await stakingRouter + .connect(stakingRouterAdmin) + .grantRole(await stakingRouter.REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE(), reporter); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + + afterEach(async () => await Snapshot.restore(originalState)); + + context("reportValidatorExitDelay", () => { + const proofSlotTimestamp = Math.floor(Date.now() / 1000); + const eligibleToExitInSec = 86400; // 1 day + const publicKey = hexlify(randomBytes(48)); + + it("calls reportValidatorExitDelay on the staking module", async () => { + await expect( + stakingModule.reportValidatorExitDelay(NODE_OPERATOR_ID, proofSlotTimestamp, publicKey, eligibleToExitInSec), + ).to.not.be.reverted; + + await expect( + stakingRouter + .connect(reporter) + .reportValidatorExitDelay( + STAKING_MODULE_ID, + NODE_OPERATOR_ID, + proofSlotTimestamp, + publicKey, + eligibleToExitInSec, + ), + ).to.not.be.reverted; + }); + + it("reverts when called by unauthorized user", async () => { + await expect( + stakingRouter + .connect(user) + .reportValidatorExitDelay( + STAKING_MODULE_ID, + NODE_OPERATOR_ID, + proofSlotTimestamp, + publicKey, + eligibleToExitInSec, + ), + ).to.be.revertedWith( + `AccessControl: account ${user.address.toLowerCase()} is missing role ${await stakingRouter.REPORT_VALIDATOR_EXITING_STATUS_ROLE()}`, + ); + }); + }); + + context("onValidatorExitTriggered", () => { + const withdrawalRequestPaidFee = ether("0.01"); + const exitType = 1n; + const publicKey = hexlify(randomBytes(48)); + + it("calls onValidatorExitTriggered on the staking module for each validator", async () => { + const validatorExitData = [ + { + stakingModuleId: STAKING_MODULE_ID, + nodeOperatorId: NODE_OPERATOR_ID, + pubkey: publicKey, + }, + ]; + + await stakingModule.setOnValidatorExitTriggeredResponse(true); + + await expect( + stakingRouter.connect(reporter).onValidatorExitTriggered(validatorExitData, withdrawalRequestPaidFee, exitType), + ).to.not.be.reverted; + }); + + it("emits StakingModuleExitNotificationFailed when staking module reverts", async () => { + const validatorExitData = [ + { + stakingModuleId: STAKING_MODULE_ID, + nodeOperatorId: NODE_OPERATOR_ID, + pubkey: publicKey, + }, + ]; + + await stakingModule.setOnValidatorExitTriggeredResponse(false); + await stakingModule.setRevertReason("Test revert reason"); + + await expect( + stakingRouter.connect(reporter).onValidatorExitTriggered(validatorExitData, withdrawalRequestPaidFee, exitType), + ) + .to.emit(stakingRouter, "StakingModuleExitNotificationFailed") + .withArgs(STAKING_MODULE_ID, NODE_OPERATOR_ID, publicKey); + }); + + it("reverts with UnrecoverableModuleError when staking module reverts with empty reason", async () => { + const validatorExitData = [ + { + stakingModuleId: STAKING_MODULE_ID, + nodeOperatorId: NODE_OPERATOR_ID, + pubkey: publicKey, + }, + ]; + + await stakingModule.setOnValidatorExitTriggeredResponse(false); + await stakingModule.setRevertWithEmptyReason(true); + + await expect( + stakingRouter.connect(reporter).onValidatorExitTriggered(validatorExitData, withdrawalRequestPaidFee, exitType), + ).to.be.revertedWithCustomError(stakingRouter, "UnrecoverableModuleError"); + }); + + it("reverts when called by unauthorized user", async () => { + const validatorExitData = [ + { + stakingModuleId: STAKING_MODULE_ID, + nodeOperatorId: NODE_OPERATOR_ID, + pubkey: publicKey, + }, + ]; + + await expect( + stakingRouter.connect(user).onValidatorExitTriggered(validatorExitData, withdrawalRequestPaidFee, exitType), + ).to.be.revertedWith( + `AccessControl: account ${user.address.toLowerCase()} is missing role ${await stakingRouter.REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE()}`, + ); + }); + }); +}); From 91fb537b704eb964119026df459ca0d3d776ae1d Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 10 Jun 2025 18:53:06 +0200 Subject: [PATCH 278/405] feat: upgrade tests for AccountingOracle --- .../contracts/AccountingOracle__Harness.sol | 4 ++ .../oracle/accountingOracle.upgrade.test.ts | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/0.8.9/oracle/accountingOracle.upgrade.test.ts diff --git a/test/0.8.9/contracts/AccountingOracle__Harness.sol b/test/0.8.9/contracts/AccountingOracle__Harness.sol index aa8f0a415f..ac0f98e129 100644 --- a/test/0.8.9/contracts/AccountingOracle__Harness.sol +++ b/test/0.8.9/contracts/AccountingOracle__Harness.sol @@ -24,6 +24,10 @@ contract AccountingOracle__Harness is AccountingOracle, ITimeProvider { CONTRACT_VERSION_POSITION.setStorageUint256(0); } + function setContractVersion(uint256 version) external { + CONTRACT_VERSION_POSITION.setStorageUint256(version); + } + function getTime() external view returns (uint256) { return _getTime(); } diff --git a/test/0.8.9/oracle/accountingOracle.upgrade.test.ts b/test/0.8.9/oracle/accountingOracle.upgrade.test.ts new file mode 100644 index 0000000000..037ca0fcbc --- /dev/null +++ b/test/0.8.9/oracle/accountingOracle.upgrade.test.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { AccountingOracle__Harness } from "typechain-types"; + +import { deployAndConfigureAccountingOracle } from "test/deploy"; + +describe("AccountingOracle.sol:upgrade", () => { + context("finalizeUpgrade_v2", () => { + let admin: HardhatEthersSigner; + let oracle: AccountingOracle__Harness; + const NEW_CONSENSUS_VERSION = 42n; // Just a test value + + beforeEach(async () => { + [admin] = await ethers.getSigners(); + const deployed = await deployAndConfigureAccountingOracle(admin.address); + oracle = deployed.oracle; + await oracle.setContractVersion(1); // Set initial contract version to 1 + }); + + it("successfully updates contract and consensus versions", async () => { + // Get initial versions + const initialContractVersion = await oracle.getContractVersion(); + const initialConsensusVersion = await oracle.getConsensusVersion(); + + // Call finalizeUpgrade_v2 + await oracle.connect(admin).finalizeUpgrade_v2(NEW_CONSENSUS_VERSION); + + // Verify contract version updated to 2 + const newContractVersion = await oracle.getContractVersion(); + expect(newContractVersion).to.equal(2); + expect(newContractVersion).to.not.equal(initialContractVersion); + + // Verify consensus version updated to the provided value + const newConsensusVersion = await oracle.getConsensusVersion(); + expect(newConsensusVersion).to.equal(NEW_CONSENSUS_VERSION); + expect(newConsensusVersion).to.not.equal(initialConsensusVersion); + }); + }); +}); From 2fa8edb2cc7dd5b0c2fe1352ea5a9464f9e57241 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 10 Jun 2025 19:19:59 +0200 Subject: [PATCH 279/405] fix: remove unused import ZeroAddress --- test/0.8.9/stakingRouter/stakingRouter.exit.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts b/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts index d8f849cc03..bf5e78656d 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.exit.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { hexlify, randomBytes, ZeroAddress } from "ethers"; +import { hexlify, randomBytes } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; From 81f1ddf652040a74e7cb1e5f731231c7e8416973 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 10 Jun 2025 20:10:41 +0200 Subject: [PATCH 280/405] feat: reimplement GIndex test in ts --- test/0.8.25/contracts/GIndex.test.sol | 72 +++++ test/0.8.9/lib/GIndex.test.ts | 445 ++++++++++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 test/0.8.25/contracts/GIndex.test.sol create mode 100644 test/0.8.9/lib/GIndex.test.ts diff --git a/test/0.8.25/contracts/GIndex.test.sol b/test/0.8.25/contracts/GIndex.test.sol new file mode 100644 index 0000000000..28659dfa34 --- /dev/null +++ b/test/0.8.25/contracts/GIndex.test.sol @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.25; + +import {GIndex, pack, IndexOutOfRange, fls} from "contracts/0.8.25/lib/GIndex.sol"; + +/** + * @dev Test contract for GIndex library in TypeScript tests + */ +contract GIndex__Test { + function wrap(bytes32 value) external pure returns (GIndex) { + return GIndex.wrap(value); + } + + function unwrap(GIndex gIndex) external pure returns (bytes32) { + return gIndex.unwrap(); + } + + function pack(uint248 index, uint8 pow) external pure returns (GIndex) { + return pack(index, pow); + } + + function isRoot(GIndex gIndex) external pure returns (bool) { + return gIndex.isRoot(); + } + + function isParentOf(GIndex lhs, GIndex rhs) external pure returns (bool) { + return lhs.isParentOf(rhs); + } + + function index(GIndex gIndex) external pure returns (uint256) { + return gIndex.index(); + } + + function width(GIndex gIndex) external pure returns (uint256) { + return gIndex.width(); + } + + function concat(GIndex lhs, GIndex rhs) external pure returns (GIndex) { + return lhs.concat(rhs); + } + + function shr(GIndex self, uint256 n) external pure returns (GIndex) { + return self.shr(n); + } + + function shl(GIndex self, uint256 n) external pure returns (GIndex) { + return self.shl(n); + } + + function fls(uint256 x) external pure returns (uint256) { + return fls(x); + } +} + +/** + * @dev Library wrapper for testing error cases + */ +contract GIndexLibrary__Test { + function concat(GIndex lhs, GIndex rhs) public returns (GIndex) { + return lhs.concat(rhs); + } + + function shr(GIndex self, uint256 n) public returns (GIndex) { + return self.shr(n); + } + + function shl(GIndex self, uint256 n) public returns (GIndex) { + return self.shl(n); + } +} diff --git a/test/0.8.9/lib/GIndex.test.ts b/test/0.8.9/lib/GIndex.test.ts new file mode 100644 index 0000000000..b3ddda5d9c --- /dev/null +++ b/test/0.8.9/lib/GIndex.test.ts @@ -0,0 +1,445 @@ +import { expect } from "chai"; +import { BigNumberish, BytesLike, concat, hexlify, randomBytes, ZeroHash, zeroPadValue } from "ethers"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { GIndex__Test, GIndexLibrary__Test } from "typechain-types"; + +import { Snapshot } from "test/suite"; + +/** + * Wrapper for the GIndex operations to match the Solidity test + */ +class GIndexWrapper { + constructor(private contract: GIndex__Test) {} + + async wrap(value: BytesLike): Promise { + return await this.contract.wrap(value); + } + + async unwrap(gIndex: string): Promise { + return await this.contract.unwrap(gIndex); + } + + async pack(index: BigNumberish, pow: BigNumberish): Promise { + return await this.contract.pack(index, pow); + } + + async isRoot(gIndex: string): Promise { + return await this.contract.isRoot(gIndex); + } + + async isParentOf(lhs: string, rhs: string): Promise { + return await this.contract.isParentOf(lhs, rhs); + } + + async index(gIndex: string): Promise { + return await this.contract.index(gIndex); + } + + async width(gIndex: string): Promise { + return await this.contract.width(gIndex); + } + + async concat(lhs: string, rhs: string): Promise { + return await this.contract.concat(lhs, rhs); + } + + async shr(self: string, n: BigNumberish): Promise { + return await this.contract.shr(self, n); + } + + async shl(self: string, n: BigNumberish): Promise { + return await this.contract.shl(self, n); + } + + async fls(x: BigNumberish): Promise { + return await this.contract.fls(x); + } +} + +describe("GIndex", () => { + let owner: HardhatEthersSigner; + let originalState: string; + + let gIndexTest: GIndex__Test; + let library: GIndexLibrary__Test; + let gIndex: GIndexWrapper; + + let ZERO: string; + let ROOT: string; + let MAX: string; + + before(async () => { + [owner] = await ethers.getSigners(); + + // Deploy the test contracts + gIndexTest = await ethers.deployContract("GIndex__Test"); + library = await ethers.deployContract("GIndexLibrary__Test"); + + gIndex = new GIndexWrapper(gIndexTest); + + // Initialize constants + ZERO = await gIndex.wrap(ZeroHash); + ROOT = await gIndex.wrap("0x0000000000000000000000000000000000000000000000000000000000000100"); + MAX = await gIndex.wrap("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + }); + + beforeEach(async () => (originalState = await Snapshot.take())); + afterEach(async () => await Snapshot.restore(originalState)); + + it("test_pack", async () => { + const gI = await gIndex.pack("0x7b426f79504c6a8e9d31415b722f696e705c8a3d9f41", 42); + + expect(await gIndex.unwrap(gI)).to.equal( + "0x0000000000000000007b426f79504c6a8e9d31415b722f696e705c8a3d9f412a", + "Invalid gindex encoded" + ); + + expect(await gIndex.unwrap(MAX)).to.equal(ethers.MaxUint256, "Invalid gindex encoded"); + }); + + it("test_isRootTrue", async () => { + expect(await gIndex.isRoot(ROOT)).to.be.true; + }); + + it("test_isRootFalse", async () => { + expect(await gIndex.isRoot(await gIndex.pack(0, 0))).to.be.false; + expect(await gIndex.isRoot(await gIndex.pack(42, 0))).to.be.false; + expect(await gIndex.isRoot(await gIndex.pack(42, 4))).to.be.false; + expect(await gIndex.isRoot(await gIndex.pack(2048, 4))).to.be.false; + + const maxUint248 = BigInt(2) ** BigInt(248) - BigInt(1); + expect(await gIndex.isRoot(await gIndex.pack(maxUint248, 255))).to.be.false; + }); + + it("test_isParentOf_Truthy", async () => { + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2048, 0))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2049, 0))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2048, 0))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2049, 0))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2048, 9))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2049, 9))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(4094, 0))).to.be.true; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(4098, 0))).to.be.true; + }); + + it("testFuzz_ROOT_isParentOfAnyChild", async () => { + for (let i = 0; i < 20; i++) { + const randomIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)) + BigInt(2); + const randomGIndex = await gIndex.wrap(zeroPadValue(ethers.toBeHex(randomIndex), 32)); + + expect(await gIndex.isParentOf(ROOT, randomGIndex)).to.be.true; + } + }); + + it("testFuzz_isParentOf_LessThanAnchor", async () => { + for (let i = 0; i < 10; i++) { + // Create two random indices where lhs > rhs + const lhsIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)) + BigInt(100); + const rhsIndex = lhsIndex - BigInt(1); + + const lhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(lhsIndex), 32)); + const rhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(rhsIndex), 32)); + + expect(await gIndex.isParentOf(lhs, rhs)).to.be.false; + } + }); + + it("test_isParentOf_OffTheBranch", async () => { + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2050, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2051, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2047, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2046, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2050, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2051, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2047, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2046, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2050, 9))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2051, 9))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2047, 9))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2046, 9))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(2048, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(2049, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1023, 9), await gIndex.pack(2048, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1023, 9), await gIndex.pack(2049, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(4098, 0))).to.be.false; + expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(4094, 0))).to.be.false; + }); + + it("test_concat", async () => { + expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(2, 99), await gIndex.pack(3, 99)))).to.equal( + await gIndex.unwrap(await gIndex.pack(5, 99)) + ); + expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(31, 99), await gIndex.pack(3, 99)))).to.equal( + await gIndex.unwrap(await gIndex.pack(63, 99)) + ); + expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(31, 99), await gIndex.pack(6, 99)))).to.equal( + await gIndex.unwrap(await gIndex.pack(126, 99)) + ); + + expect( + await gIndex.unwrap( + await gIndex.concat( + await gIndex.concat( + await gIndex.concat(ROOT, await gIndex.pack(2, 1)), + await gIndex.pack(5, 1) + ), + await gIndex.pack(9, 1) + ) + ) + ).to.equal(await gIndex.unwrap(await gIndex.pack(73, 1))); + + expect( + await gIndex.unwrap( + await gIndex.concat( + await gIndex.concat( + await gIndex.concat(ROOT, await gIndex.pack(2, 9)), + await gIndex.pack(5, 1) + ), + await gIndex.pack(9, 4) + ) + ) + ).to.equal(await gIndex.unwrap(await gIndex.pack(73, 4))); + + expect(await gIndex.unwrap(await gIndex.concat(ROOT, MAX))).to.equal(await gIndex.unwrap(MAX)); + }); + + it("test_concat_RevertsIfZeroGIndex", async () => { + await expect(library.concat(ZERO, await gIndex.pack(1024, 1))).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.concat(await gIndex.pack(1024, 1), ZERO)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + }); + + it("test_concat_BigIndicesBorderCases", async () => { + await expect(await library.concat( + await gIndex.pack(2n ** 9n, 0), + await gIndex.pack(2n ** 238n, 0) + )).to.not.be.reverted; + + await expect(await library.concat( + await gIndex.pack(2n ** 47n, 0), + await gIndex.pack(2n ** 200n, 0) + )).to.not.be.reverted; + + await expect(library.concat( + await gIndex.pack(2n ** 199n, 0), + await gIndex.pack(2n ** 48n, 0) + )).to.not.be.reverted; + }); + + it("test_concat_RevertsIfTooBigIndices", async () => { + await expect(library.concat(MAX, MAX)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + + await expect(library.concat( + await gIndex.pack(2n ** 48n, 0), + await gIndex.pack(2n ** 200n, 0) + )).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + + await expect(library.concat( + await gIndex.pack(2n ** 200n, 0), + await gIndex.pack(2n ** 48n, 0) + )).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + }); + + it("testFuzz_concat_WithRoot", async () => { + for (let i = 0; i < 10; i++) { + const randomIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)) + BigInt(1); + const randomGIndex = await gIndex.wrap(zeroPadValue(ethers.toBeHex(randomIndex), 32)); + + expect(await gIndex.unwrap(await gIndex.concat(ROOT, randomGIndex))).to.equal( + await gIndex.unwrap(randomGIndex), + "`concat` with a root should return right-hand side value" + ); + } + }); + + it("testFuzz_concat_isParentOf", async () => { + for (let i = 0; i < 10; i++) { + // Create two random indices + const lhsIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(100)) + BigInt(1); + let rhsIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(100)) + BigInt(1); + + // Make sure rhs is not 1 (root) + if (rhsIndex <= 1n) { + rhsIndex = 2n; + } + + const lhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(lhsIndex), 32)); + const rhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(rhsIndex), 32)); + + // Skip if the concatenation would overflow + const lhsBits = await gIndex.fls(lhsIndex); + const rhsBits = await gIndex.fls(rhsIndex); + + if (lhsBits + 1n + rhsBits >= 248n) { + continue; + } + + const concatenated = await gIndex.concat(lhs, rhs); + + // Verify lhs is parent of lhs.concat(rhs) + expect(await gIndex.isParentOf(lhs, concatenated)).to.be.true; + + // Verify lhs.concat(rhs) is NOT parent of lhs or rhs + expect(await gIndex.isParentOf(concatenated, lhs)).to.be.false; + expect(await gIndex.isParentOf(concatenated, rhs)).to.be.false; + } + }); + + it("testFuzz_unpack", async () => { + for (let i = 0; i < 20; i++) { + const index = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)); + const pow = BigInt(ethers.hexlify(randomBytes(1))) % 256n; + + const packed = await gIndex.pack(index, pow); + + expect(await gIndex.index(packed)).to.equal(index); + expect(await gIndex.width(packed)).to.equal(2n ** pow); + } + }); + + it("test_shr", async () => { + let gI = await gIndex.pack(1024, 4); + expect(await gIndex.unwrap(await gIndex.shr(gI, 0))).to.equal(await gIndex.unwrap(await gIndex.pack(1024, 4))); + expect(await gIndex.unwrap(await gIndex.shr(gI, 1))).to.equal(await gIndex.unwrap(await gIndex.pack(1025, 4))); + expect(await gIndex.unwrap(await gIndex.shr(gI, 15))).to.equal(await gIndex.unwrap(await gIndex.pack(1039, 4))); + + gI = await gIndex.pack(1031, 4); + expect(await gIndex.unwrap(await gIndex.shr(gI, 0))).to.equal(await gIndex.unwrap(await gIndex.pack(1031, 4))); + expect(await gIndex.unwrap(await gIndex.shr(gI, 1))).to.equal(await gIndex.unwrap(await gIndex.pack(1032, 4))); + expect(await gIndex.unwrap(await gIndex.shr(gI, 8))).to.equal(await gIndex.unwrap(await gIndex.pack(1039, 4))); + + gI = await gIndex.pack(2049, 4); + expect(await gIndex.unwrap(await gIndex.shr(gI, 0))).to.equal(await gIndex.unwrap(await gIndex.pack(2049, 4))); + expect(await gIndex.unwrap(await gIndex.shr(gI, 1))).to.equal(await gIndex.unwrap(await gIndex.pack(2050, 4))); + expect(await gIndex.unwrap(await gIndex.shr(gI, 14))).to.equal(await gIndex.unwrap(await gIndex.pack(2063, 4))); + }); + + it("test_shr_AfterConcat", async () => { + const gIParent = await gIndex.pack(5, 4); + + let gI = await gIndex.pack(1024, 4); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5120, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5121, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 15))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5135, 4))); + + gI = await gIndex.pack(1031, 4); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5127, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5128, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 8))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5135, 4))); + + gI = await gIndex.pack(2049, 4); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))) + .to.equal(await gIndex.unwrap(await gIndex.pack(10241, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))) + .to.equal(await gIndex.unwrap(await gIndex.pack(10242, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 14))) + .to.equal(await gIndex.unwrap(await gIndex.pack(10255, 4))); + }); + + it("test_shr_OffTheWidth", async () => { + await expect(library.shr(ROOT, 1)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.pack(1024, 4), 16)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.pack(1031, 4), 9)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.pack(1023, 4), 1)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + }); + + it("test_shr_OffTheWidth_AfterConcat", async () => { + const gIParent = await gIndex.pack(154, 4); + + await expect(library.shr(await gIndex.concat(gIParent, ROOT), 1)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.concat(gIParent, await gIndex.pack(1024, 4)), 16)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.concat(gIParent, await gIndex.pack(1031, 4)), 9)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.concat(gIParent, await gIndex.pack(1023, 4)), 1)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + }); + + it("test_shl", async () => { + let gI = await gIndex.pack(1023, 4); + expect(await gIndex.unwrap(await gIndex.shl(gI, 0))).to.equal(await gIndex.unwrap(await gIndex.pack(1023, 4))); + expect(await gIndex.unwrap(await gIndex.shl(gI, 1))).to.equal(await gIndex.unwrap(await gIndex.pack(1022, 4))); + expect(await gIndex.unwrap(await gIndex.shl(gI, 15))).to.equal(await gIndex.unwrap(await gIndex.pack(1008, 4))); + + gI = await gIndex.pack(1031, 4); + expect(await gIndex.unwrap(await gIndex.shl(gI, 0))).to.equal(await gIndex.unwrap(await gIndex.pack(1031, 4))); + expect(await gIndex.unwrap(await gIndex.shl(gI, 1))).to.equal(await gIndex.unwrap(await gIndex.pack(1030, 4))); + expect(await gIndex.unwrap(await gIndex.shl(gI, 7))).to.equal(await gIndex.unwrap(await gIndex.pack(1024, 4))); + + gI = await gIndex.pack(2063, 4); + expect(await gIndex.unwrap(await gIndex.shl(gI, 0))).to.equal(await gIndex.unwrap(await gIndex.pack(2063, 4))); + expect(await gIndex.unwrap(await gIndex.shl(gI, 1))).to.equal(await gIndex.unwrap(await gIndex.pack(2062, 4))); + expect(await gIndex.unwrap(await gIndex.shl(gI, 15))).to.equal(await gIndex.unwrap(await gIndex.pack(2048, 4))); + }); + + it("test_shl_AfterConcat", async () => { + const gIParent = await gIndex.pack(5, 4); + + let gI = await gIndex.pack(1023, 4); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))) + .to.equal(await gIndex.unwrap(await gIndex.pack(3071, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))) + .to.equal(await gIndex.unwrap(await gIndex.pack(3070, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 15))) + .to.equal(await gIndex.unwrap(await gIndex.pack(3056, 4))); + + gI = await gIndex.pack(1031, 4); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5127, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5126, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 7))) + .to.equal(await gIndex.unwrap(await gIndex.pack(5120, 4))); + + gI = await gIndex.pack(2063, 4); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))) + .to.equal(await gIndex.unwrap(await gIndex.pack(10255, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))) + .to.equal(await gIndex.unwrap(await gIndex.pack(10254, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 15))) + .to.equal(await gIndex.unwrap(await gIndex.pack(10240, 4))); + }); + + it("test_shl_OffTheWidth", async () => { + await expect(library.shl(ROOT, 1)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.pack(1024, 4), 1)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.pack(1031, 4), 9)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.pack(1023, 4), 16)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + }); + + it("test_shl_OffTheWidth_AfterConcat", async () => { + const gIParent = await gIndex.pack(154, 4); + + await expect(library.shl(await gIndex.concat(gIParent, ROOT), 1)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.concat(gIParent, await gIndex.pack(1024, 4)), 1)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.concat(gIParent, await gIndex.pack(1031, 4)), 9)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.concat(gIParent, await gIndex.pack(1023, 4)), 16)) + .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + }); + + it("test_fls", async () => { + for (let i = 1; i < 255; i++) { + expect(await gIndex.fls((1n << BigInt(i)) - 1n)).to.equal(BigInt(i - 1)); + expect(await gIndex.fls(1n << BigInt(i))).to.equal(BigInt(i)); + expect(await gIndex.fls((1n << BigInt(i)) + 1n)).to.equal(BigInt(i)); + } + + expect(await gIndex.fls(3n)).to.equal(1n); // 0011 + expect(await gIndex.fls(7n)).to.equal(2n); // 0111 + expect(await gIndex.fls(10n)).to.equal(3n); // 1010 + expect(await gIndex.fls(300n)).to.equal(8n); // 0001 0010 1100 + expect(await gIndex.fls(0n)).to.equal(256n); + }); +}); From c16db3b143ca5f0a7037e482484a7459cb254e28 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 10 Jun 2025 22:00:22 +0200 Subject: [PATCH 281/405] refactor: clean up GIndex test imports and improve readability --- test/0.8.9/lib/GIndex.test.ts | 228 ++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 107 deletions(-) diff --git a/test/0.8.9/lib/GIndex.test.ts b/test/0.8.9/lib/GIndex.test.ts index b3ddda5d9c..192b69b72d 100644 --- a/test/0.8.9/lib/GIndex.test.ts +++ b/test/0.8.9/lib/GIndex.test.ts @@ -1,9 +1,7 @@ import { expect } from "chai"; -import { BigNumberish, BytesLike, concat, hexlify, randomBytes, ZeroHash, zeroPadValue } from "ethers"; +import { BigNumberish, BytesLike, randomBytes, ZeroHash, zeroPadValue } from "ethers"; import { ethers } from "hardhat"; -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; - import { GIndex__Test, GIndexLibrary__Test } from "typechain-types"; import { Snapshot } from "test/suite"; @@ -60,7 +58,6 @@ class GIndexWrapper { } describe("GIndex", () => { - let owner: HardhatEthersSigner; let originalState: string; let gIndexTest: GIndex__Test; @@ -72,8 +69,6 @@ describe("GIndex", () => { let MAX: string; before(async () => { - [owner] = await ethers.getSigners(); - // Deploy the test contracts gIndexTest = await ethers.deployContract("GIndex__Test"); library = await ethers.deployContract("GIndexLibrary__Test"); @@ -94,7 +89,7 @@ describe("GIndex", () => { expect(await gIndex.unwrap(gI)).to.equal( "0x0000000000000000007b426f79504c6a8e9d31415b722f696e705c8a3d9f412a", - "Invalid gindex encoded" + "Invalid gindex encoded", ); expect(await gIndex.unwrap(MAX)).to.equal(ethers.MaxUint256, "Invalid gindex encoded"); @@ -127,7 +122,7 @@ describe("GIndex", () => { it("testFuzz_ROOT_isParentOfAnyChild", async () => { for (let i = 0; i < 20; i++) { - const randomIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)) + BigInt(2); + const randomIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240)) + BigInt(2); const randomGIndex = await gIndex.wrap(zeroPadValue(ethers.toBeHex(randomIndex), 32)); expect(await gIndex.isParentOf(ROOT, randomGIndex)).to.be.true; @@ -137,7 +132,7 @@ describe("GIndex", () => { it("testFuzz_isParentOf_LessThanAnchor", async () => { for (let i = 0; i < 10; i++) { // Create two random indices where lhs > rhs - const lhsIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)) + BigInt(100); + const lhsIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240)) + BigInt(100); const rhsIndex = lhsIndex - BigInt(1); const lhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(lhsIndex), 32)); @@ -170,86 +165,77 @@ describe("GIndex", () => { it("test_concat", async () => { expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(2, 99), await gIndex.pack(3, 99)))).to.equal( - await gIndex.unwrap(await gIndex.pack(5, 99)) + await gIndex.unwrap(await gIndex.pack(5, 99)), ); expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(31, 99), await gIndex.pack(3, 99)))).to.equal( - await gIndex.unwrap(await gIndex.pack(63, 99)) + await gIndex.unwrap(await gIndex.pack(63, 99)), ); expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(31, 99), await gIndex.pack(6, 99)))).to.equal( - await gIndex.unwrap(await gIndex.pack(126, 99)) + await gIndex.unwrap(await gIndex.pack(126, 99)), ); expect( await gIndex.unwrap( await gIndex.concat( - await gIndex.concat( - await gIndex.concat(ROOT, await gIndex.pack(2, 1)), - await gIndex.pack(5, 1) - ), - await gIndex.pack(9, 1) - ) - ) + await gIndex.concat(await gIndex.concat(ROOT, await gIndex.pack(2, 1)), await gIndex.pack(5, 1)), + await gIndex.pack(9, 1), + ), + ), ).to.equal(await gIndex.unwrap(await gIndex.pack(73, 1))); expect( await gIndex.unwrap( await gIndex.concat( - await gIndex.concat( - await gIndex.concat(ROOT, await gIndex.pack(2, 9)), - await gIndex.pack(5, 1) - ), - await gIndex.pack(9, 4) - ) - ) + await gIndex.concat(await gIndex.concat(ROOT, await gIndex.pack(2, 9)), await gIndex.pack(5, 1)), + await gIndex.pack(9, 4), + ), + ), ).to.equal(await gIndex.unwrap(await gIndex.pack(73, 4))); expect(await gIndex.unwrap(await gIndex.concat(ROOT, MAX))).to.equal(await gIndex.unwrap(MAX)); }); it("test_concat_RevertsIfZeroGIndex", async () => { - await expect(library.concat(ZERO, await gIndex.pack(1024, 1))).to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.concat(await gIndex.pack(1024, 1), ZERO)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.concat(ZERO, await gIndex.pack(1024, 1))).to.be.revertedWithCustomError( + library, + "IndexOutOfRange", + ); + await expect(library.concat(await gIndex.pack(1024, 1), ZERO)).to.be.revertedWithCustomError( + library, + "IndexOutOfRange", + ); }); it("test_concat_BigIndicesBorderCases", async () => { - await expect(await library.concat( - await gIndex.pack(2n ** 9n, 0), - await gIndex.pack(2n ** 238n, 0) - )).to.not.be.reverted; - - await expect(await library.concat( - await gIndex.pack(2n ** 47n, 0), - await gIndex.pack(2n ** 200n, 0) - )).to.not.be.reverted; - - await expect(library.concat( - await gIndex.pack(2n ** 199n, 0), - await gIndex.pack(2n ** 48n, 0) - )).to.not.be.reverted; + await expect(await library.concat(await gIndex.pack(2n ** 9n, 0), await gIndex.pack(2n ** 238n, 0))).to.not.be + .reverted; + + await expect(await library.concat(await gIndex.pack(2n ** 47n, 0), await gIndex.pack(2n ** 200n, 0))).to.not.be + .reverted; + + await expect(library.concat(await gIndex.pack(2n ** 199n, 0), await gIndex.pack(2n ** 48n, 0))).to.not.be.reverted; }); it("test_concat_RevertsIfTooBigIndices", async () => { await expect(library.concat(MAX, MAX)).to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.concat( - await gIndex.pack(2n ** 48n, 0), - await gIndex.pack(2n ** 200n, 0) - )).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect( + library.concat(await gIndex.pack(2n ** 48n, 0), await gIndex.pack(2n ** 200n, 0)), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.concat( - await gIndex.pack(2n ** 200n, 0), - await gIndex.pack(2n ** 48n, 0) - )).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect( + library.concat(await gIndex.pack(2n ** 200n, 0), await gIndex.pack(2n ** 48n, 0)), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); }); it("testFuzz_concat_WithRoot", async () => { for (let i = 0; i < 10; i++) { - const randomIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)) + BigInt(1); + const randomIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240)) + BigInt(1); const randomGIndex = await gIndex.wrap(zeroPadValue(ethers.toBeHex(randomIndex), 32)); expect(await gIndex.unwrap(await gIndex.concat(ROOT, randomGIndex))).to.equal( await gIndex.unwrap(randomGIndex), - "`concat` with a root should return right-hand side value" + "`concat` with a root should return right-hand side value", ); } }); @@ -257,8 +243,8 @@ describe("GIndex", () => { it("testFuzz_concat_isParentOf", async () => { for (let i = 0; i < 10; i++) { // Create two random indices - const lhsIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(100)) + BigInt(1); - let rhsIndex = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(100)) + BigInt(1); + const lhsIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(100)) + BigInt(1); + let rhsIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(100)) + BigInt(1); // Make sure rhs is not 1 (root) if (rhsIndex <= 1n) { @@ -289,7 +275,7 @@ describe("GIndex", () => { it("testFuzz_unpack", async () => { for (let i = 0; i < 20; i++) { - const index = BigInt(ethers.hexlify(randomBytes(30))) % (BigInt(2) ** BigInt(240)); + const index = BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240); const pow = BigInt(ethers.hexlify(randomBytes(1))) % 256n; const packed = await gIndex.pack(index, pow); @@ -320,28 +306,37 @@ describe("GIndex", () => { const gIParent = await gIndex.pack(5, 4); let gI = await gIndex.pack(1024, 4); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5120, 4))); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5121, 4))); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 15))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5135, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))).to.equal( + await gIndex.unwrap(await gIndex.pack(5120, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))).to.equal( + await gIndex.unwrap(await gIndex.pack(5121, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 15))).to.equal( + await gIndex.unwrap(await gIndex.pack(5135, 4)), + ); gI = await gIndex.pack(1031, 4); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5127, 4))); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5128, 4))); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 8))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5135, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))).to.equal( + await gIndex.unwrap(await gIndex.pack(5127, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))).to.equal( + await gIndex.unwrap(await gIndex.pack(5128, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 8))).to.equal( + await gIndex.unwrap(await gIndex.pack(5135, 4)), + ); gI = await gIndex.pack(2049, 4); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))) - .to.equal(await gIndex.unwrap(await gIndex.pack(10241, 4))); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))) - .to.equal(await gIndex.unwrap(await gIndex.pack(10242, 4))); - expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 14))) - .to.equal(await gIndex.unwrap(await gIndex.pack(10255, 4))); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 0))).to.equal( + await gIndex.unwrap(await gIndex.pack(10241, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 1))).to.equal( + await gIndex.unwrap(await gIndex.pack(10242, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shr(await gIndex.concat(gIParent, gI), 14))).to.equal( + await gIndex.unwrap(await gIndex.pack(10255, 4)), + ); }); it("test_shr_OffTheWidth", async () => { @@ -354,14 +349,19 @@ describe("GIndex", () => { it("test_shr_OffTheWidth_AfterConcat", async () => { const gIParent = await gIndex.pack(154, 4); - await expect(library.shr(await gIndex.concat(gIParent, ROOT), 1)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.shr(await gIndex.concat(gIParent, await gIndex.pack(1024, 4)), 16)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.shr(await gIndex.concat(gIParent, await gIndex.pack(1031, 4)), 9)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.shr(await gIndex.concat(gIParent, await gIndex.pack(1023, 4)), 1)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shr(await gIndex.concat(gIParent, ROOT), 1)).to.be.revertedWithCustomError( + library, + "IndexOutOfRange", + ); + await expect( + library.shr(await gIndex.concat(gIParent, await gIndex.pack(1024, 4)), 16), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect( + library.shr(await gIndex.concat(gIParent, await gIndex.pack(1031, 4)), 9), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect( + library.shr(await gIndex.concat(gIParent, await gIndex.pack(1023, 4)), 1), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); }); it("test_shl", async () => { @@ -385,28 +385,37 @@ describe("GIndex", () => { const gIParent = await gIndex.pack(5, 4); let gI = await gIndex.pack(1023, 4); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))) - .to.equal(await gIndex.unwrap(await gIndex.pack(3071, 4))); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))) - .to.equal(await gIndex.unwrap(await gIndex.pack(3070, 4))); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 15))) - .to.equal(await gIndex.unwrap(await gIndex.pack(3056, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))).to.equal( + await gIndex.unwrap(await gIndex.pack(3071, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))).to.equal( + await gIndex.unwrap(await gIndex.pack(3070, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 15))).to.equal( + await gIndex.unwrap(await gIndex.pack(3056, 4)), + ); gI = await gIndex.pack(1031, 4); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5127, 4))); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5126, 4))); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 7))) - .to.equal(await gIndex.unwrap(await gIndex.pack(5120, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))).to.equal( + await gIndex.unwrap(await gIndex.pack(5127, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))).to.equal( + await gIndex.unwrap(await gIndex.pack(5126, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 7))).to.equal( + await gIndex.unwrap(await gIndex.pack(5120, 4)), + ); gI = await gIndex.pack(2063, 4); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))) - .to.equal(await gIndex.unwrap(await gIndex.pack(10255, 4))); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))) - .to.equal(await gIndex.unwrap(await gIndex.pack(10254, 4))); - expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 15))) - .to.equal(await gIndex.unwrap(await gIndex.pack(10240, 4))); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 0))).to.equal( + await gIndex.unwrap(await gIndex.pack(10255, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 1))).to.equal( + await gIndex.unwrap(await gIndex.pack(10254, 4)), + ); + expect(await gIndex.unwrap(await gIndex.shl(await gIndex.concat(gIParent, gI), 15))).to.equal( + await gIndex.unwrap(await gIndex.pack(10240, 4)), + ); }); it("test_shl_OffTheWidth", async () => { @@ -419,14 +428,19 @@ describe("GIndex", () => { it("test_shl_OffTheWidth_AfterConcat", async () => { const gIParent = await gIndex.pack(154, 4); - await expect(library.shl(await gIndex.concat(gIParent, ROOT), 1)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.shl(await gIndex.concat(gIParent, await gIndex.pack(1024, 4)), 1)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.shl(await gIndex.concat(gIParent, await gIndex.pack(1031, 4)), 9)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); - await expect(library.shl(await gIndex.concat(gIParent, await gIndex.pack(1023, 4)), 16)) - .to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect(library.shl(await gIndex.concat(gIParent, ROOT), 1)).to.be.revertedWithCustomError( + library, + "IndexOutOfRange", + ); + await expect( + library.shl(await gIndex.concat(gIParent, await gIndex.pack(1024, 4)), 1), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect( + library.shl(await gIndex.concat(gIParent, await gIndex.pack(1031, 4)), 9), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); + await expect( + library.shl(await gIndex.concat(gIParent, await gIndex.pack(1023, 4)), 16), + ).to.be.revertedWithCustomError(library, "IndexOutOfRange"); }); it("test_fls", async () => { From 7856addc660b087fdd68ec28bbfd4948194c18c4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 11 Jun 2025 09:42:27 +0200 Subject: [PATCH 282/405] refactor: rename utils contract for GIndex --- .../{GIndex.test.sol => GIndex__Harness.sol} | 6 +++--- test/0.8.9/lib/GIndex.test.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) rename test/0.8.25/contracts/{GIndex.test.sol => GIndex__Harness.sol} (94%) diff --git a/test/0.8.25/contracts/GIndex.test.sol b/test/0.8.25/contracts/GIndex__Harness.sol similarity index 94% rename from test/0.8.25/contracts/GIndex.test.sol rename to test/0.8.25/contracts/GIndex__Harness.sol index 28659dfa34..85ca8bb3b9 100644 --- a/test/0.8.25/contracts/GIndex.test.sol +++ b/test/0.8.25/contracts/GIndex__Harness.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.25; @@ -8,7 +8,7 @@ import {GIndex, pack, IndexOutOfRange, fls} from "contracts/0.8.25/lib/GIndex.so /** * @dev Test contract for GIndex library in TypeScript tests */ -contract GIndex__Test { +contract GIndex__Harness { function wrap(bytes32 value) external pure returns (GIndex) { return GIndex.wrap(value); } @@ -57,7 +57,7 @@ contract GIndex__Test { /** * @dev Library wrapper for testing error cases */ -contract GIndexLibrary__Test { +contract GIndexLibrary__Harness { function concat(GIndex lhs, GIndex rhs) public returns (GIndex) { return lhs.concat(rhs); } diff --git a/test/0.8.9/lib/GIndex.test.ts b/test/0.8.9/lib/GIndex.test.ts index 192b69b72d..ec86337a70 100644 --- a/test/0.8.9/lib/GIndex.test.ts +++ b/test/0.8.9/lib/GIndex.test.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { BigNumberish, BytesLike, randomBytes, ZeroHash, zeroPadValue } from "ethers"; import { ethers } from "hardhat"; -import { GIndex__Test, GIndexLibrary__Test } from "typechain-types"; +import { GIndex__Harness, GIndexLibrary__Harness } from "typechain-types"; import { Snapshot } from "test/suite"; @@ -10,7 +10,7 @@ import { Snapshot } from "test/suite"; * Wrapper for the GIndex operations to match the Solidity test */ class GIndexWrapper { - constructor(private contract: GIndex__Test) {} + constructor(private contract: GIndex__Harness) {} async wrap(value: BytesLike): Promise { return await this.contract.wrap(value); @@ -60,8 +60,8 @@ class GIndexWrapper { describe("GIndex", () => { let originalState: string; - let gIndexTest: GIndex__Test; - let library: GIndexLibrary__Test; + let gIndexTest: GIndex__Harness; + let library: GIndexLibrary__Harness; let gIndex: GIndexWrapper; let ZERO: string; @@ -70,8 +70,8 @@ describe("GIndex", () => { before(async () => { // Deploy the test contracts - gIndexTest = await ethers.deployContract("GIndex__Test"); - library = await ethers.deployContract("GIndexLibrary__Test"); + gIndexTest = await ethers.deployContract("GIndex__Harness"); + library = await ethers.deployContract("GIndexLibrary__Harness"); gIndex = new GIndexWrapper(gIndexTest); From bf0bb4dd179d42ca21306a86b9c08de0dddb47e6 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 11 Jun 2025 10:10:12 +0200 Subject: [PATCH 283/405] Update contracts/0.8.25/lib/SSZ.sol Co-authored-by: Eugene Mamin --- contracts/0.8.25/lib/SSZ.sol | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.25/lib/SSZ.sol b/contracts/0.8.25/lib/SSZ.sol index 48e637b1b3..40d5ca1908 100644 --- a/contracts/0.8.25/lib/SSZ.sol +++ b/contracts/0.8.25/lib/SSZ.sol @@ -205,15 +205,17 @@ library SSZ { // Store elements to hash contiguously in scratch space. // Scratch space is 64 bytes (0x00 - 0x3f) and both elements are 32 bytes. mstore(scratch, leaf) + // load next proof from calldata to the scratch space at 0x00 or 0x20 + // xor() acts as if mstore(xor(scratch, 0x20), calldataload(offset)) // Call sha256 precompile. let result := staticcall( gas(), - 0x02, - 0x00, - 0x40, - 0x00, - 0x20 + 0x02, // SHA-256 precompile + 0x00, // input from scratch space from 0x00 + 0x40, // length is 2 leafs of 32 bytes each + 0x00, // output back to scratch space at 0x00 + 0x20 // length of the output is 32 bytes ) if iszero(result) { From 27f6906becf89476cf9783a18233ceff8406b814 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 11 Jun 2025 12:13:29 +0200 Subject: [PATCH 284/405] feat: move BeaconTypes, SSZ and GIndex to common folder Change solidity version to ^0.8.25 --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 6 +++--- contracts/{0.8.25 => common}/lib/BeaconTypes.sol | 2 +- contracts/{0.8.25 => common}/lib/GIndex.sol | 2 +- contracts/{0.8.25 => common}/lib/SSZ.sol | 2 +- test/0.8.25/contracts/GIndex__Harness.sol | 2 +- test/0.8.25/lib/GIndex.t.sol | 4 ++-- test/0.8.25/lib/SSZ.t.sol | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) rename contracts/{0.8.25 => common}/lib/BeaconTypes.sol (94%) rename contracts/{0.8.25 => common}/lib/GIndex.sol (99%) rename contracts/{0.8.25 => common}/lib/SSZ.sol (99%) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 095901715e..85cfb9765c 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -3,9 +3,9 @@ pragma solidity 0.8.25; -import {BeaconBlockHeader, Validator} from "./lib/BeaconTypes.sol"; -import {GIndex} from "./lib/GIndex.sol"; -import {SSZ} from "./lib/SSZ.sol"; +import {BeaconBlockHeader, Validator} from "../common/lib/BeaconTypes.sol"; +import {GIndex} from "../common/lib/GIndex.sol"; +import {SSZ} from "../common/lib/SSZ.sol"; import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; import {IValidatorsExitBus} from "./interfaces/IValidatorsExitBus.sol"; import {IStakingRouter} from "./interfaces/IStakingRouter.sol"; diff --git a/contracts/0.8.25/lib/BeaconTypes.sol b/contracts/common/lib/BeaconTypes.sol similarity index 94% rename from contracts/0.8.25/lib/BeaconTypes.sol rename to contracts/common/lib/BeaconTypes.sol index cb48f54b41..89fe84f3e9 100644 --- a/contracts/0.8.25/lib/BeaconTypes.sol +++ b/contracts/common/lib/BeaconTypes.sol @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.25; +pragma solidity ^0.8.25; struct Validator { bytes pubkey; diff --git a/contracts/0.8.25/lib/GIndex.sol b/contracts/common/lib/GIndex.sol similarity index 99% rename from contracts/0.8.25/lib/GIndex.sol rename to contracts/common/lib/GIndex.sol index 996bb053bc..85252824f0 100644 --- a/contracts/0.8.25/lib/GIndex.sol +++ b/contracts/common/lib/GIndex.sol @@ -6,7 +6,7 @@ original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/GIndex.sol */ -pragma solidity 0.8.25; +pragma solidity ^0.8.25; type GIndex is bytes32; diff --git a/contracts/0.8.25/lib/SSZ.sol b/contracts/common/lib/SSZ.sol similarity index 99% rename from contracts/0.8.25/lib/SSZ.sol rename to contracts/common/lib/SSZ.sol index 48e637b1b3..8e1441d22d 100644 --- a/contracts/0.8.25/lib/SSZ.sol +++ b/contracts/common/lib/SSZ.sol @@ -6,7 +6,7 @@ original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol */ -pragma solidity 0.8.25; +pragma solidity ^0.8.25; import {BeaconBlockHeader, Validator} from "./BeaconTypes.sol"; import {GIndex} from "./GIndex.sol"; diff --git a/test/0.8.25/contracts/GIndex__Harness.sol b/test/0.8.25/contracts/GIndex__Harness.sol index 85ca8bb3b9..3fde63ead9 100644 --- a/test/0.8.25/contracts/GIndex__Harness.sol +++ b/test/0.8.25/contracts/GIndex__Harness.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.25; -import {GIndex, pack, IndexOutOfRange, fls} from "contracts/0.8.25/lib/GIndex.sol"; +import {GIndex, pack, IndexOutOfRange, fls} from "contracts/common/lib/GIndex.sol"; /** * @dev Test contract for GIndex library in TypeScript tests diff --git a/test/0.8.25/lib/GIndex.t.sol b/test/0.8.25/lib/GIndex.t.sol index 4da9a01da1..d4d8d56fd0 100644 --- a/test/0.8.25/lib/GIndex.t.sol +++ b/test/0.8.25/lib/GIndex.t.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.25; import {Test} from "forge-std/Test.sol"; -import {GIndex, pack, IndexOutOfRange, fls} from "../../../contracts/0.8.25/lib/GIndex.sol"; -import {SSZ} from "../../../contracts/0.8.25/lib/SSZ.sol"; +import {GIndex, pack, IndexOutOfRange, fls} from "../../../contracts/common/lib/GIndex.sol"; +import {SSZ} from "../../../contracts/common/lib/SSZ.sol"; // Wrap the library internal methods to make an actual call to them. // Supposed to be used with `expectRevert` cheatcode. diff --git a/test/0.8.25/lib/SSZ.t.sol b/test/0.8.25/lib/SSZ.t.sol index 647eee0c04..6bcb11070e 100644 --- a/test/0.8.25/lib/SSZ.t.sol +++ b/test/0.8.25/lib/SSZ.t.sol @@ -4,10 +4,10 @@ pragma solidity 0.8.25; import {Test} from "forge-std/Test.sol"; -import {BeaconBlockHeader, Validator} from "../../../contracts/0.8.25/lib/BeaconTypes.sol"; -import {GIndex, pack} from "../../../contracts/0.8.25/lib/GIndex.sol"; +import {BeaconBlockHeader, Validator} from "../../../contracts/common/lib/BeaconTypes.sol"; +import {GIndex, pack} from "../../../contracts/common/lib/GIndex.sol"; import {Utilities} from "../contracts/Utilities.sol"; -import {SSZ} from "../../../contracts/0.8.25/lib/SSZ.sol"; +import {SSZ} from "../../../contracts/common/lib/SSZ.sol"; // Wrap the library internal methods to make an actual call to them. // Supposed to be used with `expectRevert` cheatcode and to pass From fe7df7014670174d260c72d6cad2c170690b4d11 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 11 Jun 2025 12:19:29 +0200 Subject: [PATCH 285/405] Update contracts/common/lib/BeaconTypes.sol Co-authored-by: Yuri Tkachenko --- contracts/common/lib/BeaconTypes.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/common/lib/BeaconTypes.sol b/contracts/common/lib/BeaconTypes.sol index 89fe84f3e9..d0b67459df 100644 --- a/contracts/common/lib/BeaconTypes.sol +++ b/contracts/common/lib/BeaconTypes.sol @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version pragma solidity ^0.8.25; struct Validator { From 3c399268f673bdcc81c62bcb8e74ad9dca904e1f Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 11 Jun 2025 12:25:18 +0200 Subject: [PATCH 286/405] fix: add solhint-disable --- contracts/common/lib/GIndex.sol | 2 ++ contracts/common/lib/SSZ.sol | 2 ++ 2 files changed, 4 insertions(+) diff --git a/contracts/common/lib/GIndex.sol b/contracts/common/lib/GIndex.sol index 85252824f0..66a3874922 100644 --- a/contracts/common/lib/GIndex.sol +++ b/contracts/common/lib/GIndex.sol @@ -6,6 +6,8 @@ original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/GIndex.sol */ +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version pragma solidity ^0.8.25; type GIndex is bytes32; diff --git a/contracts/common/lib/SSZ.sol b/contracts/common/lib/SSZ.sol index 8e1441d22d..d5814c0350 100644 --- a/contracts/common/lib/SSZ.sol +++ b/contracts/common/lib/SSZ.sol @@ -6,6 +6,8 @@ original: https://github.com/lidofinance/community-staking-module/blob/7071c2096983a7780a5f147963aaa5405c0badb1/src/lib/SSZ.sol */ +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version pragma solidity ^0.8.25; import {BeaconBlockHeader, Validator} from "./BeaconTypes.sol"; From b64b1c196b08d4346c2cb4435cffe2b38a97af9c Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 11 Jun 2025 16:42:05 +0200 Subject: [PATCH 287/405] feat: add withdrawal vault integration tests Test addWithdrawalRequests method --- lib/eips/eip7002.ts | 32 ++++++- test/0.8.9/withdrawalVault/eip7002Mock.ts | 4 +- test/0.8.9/withdrawalVault/utils.ts | 1 - ...ult-add-withdrawal-requests.integration.ts | 95 +++++++++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 test/integration/withdrawal-vault-add-withdrawal-requests.integration.ts diff --git a/lib/eips/eip7002.ts b/lib/eips/eip7002.ts index c201b0da17..7de3987354 100644 --- a/lib/eips/eip7002.ts +++ b/lib/eips/eip7002.ts @@ -26,14 +26,44 @@ export const ensureEIP7002WithdrawalRequestContractPresent = async (): Promise => { +export const readWithdrawalRequests = async (): Promise => { const sysAddress = await impersonate("0xfffffffffffffffffffffffffffffffffffffffe", 999999999999999999999999999n); + // Use a call to get the return data (simulate the transaction) + const callResult: string = await ethers.provider.call({ + to: EIP7002_ADDRESS, + from: await sysAddress.getAddress(), + value: 0, + data: "0x", + }); + + // Send a transaction await sysAddress.sendTransaction({ to: EIP7002_ADDRESS, value: 0, }); + + if (!callResult || callResult === "0x") return []; + + const buf = Buffer.from(callResult.slice(2), "hex"); + const REQUEST_SIZE = 76; + const requests: EIP7002WithdrawalRequest[] = []; + for (let i = 0; i + REQUEST_SIZE <= buf.length; i += REQUEST_SIZE) { + const chunk = buf.subarray(i, i + REQUEST_SIZE); + const address = "0x" + chunk.subarray(0, 20).toString("hex"); + const pubkey = "0x" + chunk.subarray(20, 68).toString("hex"); + const amount = chunk.readBigUInt64LE(68); + requests.push({ address, pubkey, amount }); + } + + return requests; }; diff --git a/test/0.8.9/withdrawalVault/eip7002Mock.ts b/test/0.8.9/withdrawalVault/eip7002Mock.ts index 7a1093ade8..09037bd6d2 100644 --- a/test/0.8.9/withdrawalVault/eip7002Mock.ts +++ b/test/0.8.9/withdrawalVault/eip7002Mock.ts @@ -25,7 +25,9 @@ export const deployEIP7002WithdrawalRequestContractMock = async ( }; export function encodeEIP7002Payload(pubkey: string, amount: bigint): string { - return `0x${pubkey}${amount.toString(16).padStart(16, "0")}`; + // remove 0x prefix if it exists + const pubkeyWithoutPrefix = pubkey.startsWith("0x") ? pubkey.slice(2) : pubkey; + return `0x${pubkeyWithoutPrefix}${amount.toString(16).padStart(16, "0")}`; } export function findEIP7002MockEvents(receipt: ContractTransactionReceipt) { diff --git a/test/0.8.9/withdrawalVault/utils.ts b/test/0.8.9/withdrawalVault/utils.ts index db5ffa740e..968e73df9b 100644 --- a/test/0.8.9/withdrawalVault/utils.ts +++ b/test/0.8.9/withdrawalVault/utils.ts @@ -28,7 +28,6 @@ export function generateWithdrawalRequestPayload(numberOfRequests: number) { } return { - pubkeysHexString: `0x${pubkeys.join("")}`, pubkeysHexArray: pubkeys.map((pk) => `0x${pk}`), pubkeys, fullWithdrawalAmounts, diff --git a/test/integration/withdrawal-vault-add-withdrawal-requests.integration.ts b/test/integration/withdrawal-vault-add-withdrawal-requests.integration.ts new file mode 100644 index 0000000000..888c428eae --- /dev/null +++ b/test/integration/withdrawal-vault-add-withdrawal-requests.integration.ts @@ -0,0 +1,95 @@ +// ToDo: add integration tests for the withdrawal vault +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; + +import { WithdrawalVault } from "typechain-types"; + +import { ether, readWithdrawalRequests } from "lib"; +import { impersonate } from "lib/account"; +import { getProtocolContext, ProtocolContext } from "lib/protocol"; + +import { encodeEIP7002Payload } from "test/0.8.9/withdrawalVault/eip7002Mock"; +import { Snapshot } from "test/suite"; + +describe("WithdrawalVault: addWithdrawalRequests Integration", () => { + let ctx: ProtocolContext; + let snapshot: string; + let withdrawalVault: WithdrawalVault; + let withdrawalVaultAddress: string; + let stranger: Awaited>[number]; + let gateway: HardhatEthersSigner; + + // Example 48-byte pubkeys + const PUBKEYS = ["0x" + "aa".repeat(48), "0x" + "bb".repeat(48)]; + const AMOUNTS = [0n, 123456n]; + + before(async () => { + snapshot = await Snapshot.take(); + + ctx = await getProtocolContext(); + [, stranger] = await ethers.getSigners(); + withdrawalVault = ctx.contracts.withdrawalVault; + withdrawalVaultAddress = await withdrawalVault.getAddress(); + gateway = await impersonate(await ctx.contracts.triggerableWithdrawalsGateway.getAddress(), ether("100")); + }); + + after(async () => await Snapshot.restore(snapshot)); + + it("should revert if called by non-gateway", async () => { + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + await expect( + withdrawalVault + .connect(stranger) + .addWithdrawalRequests(PUBKEYS, AMOUNTS, { value: withdrawalFee * BigInt(PUBKEYS.length) }), + ).to.be.revertedWithCustomError(withdrawalVault, "NotTriggerableWithdrawalsGateway"); + }); + + it("should revert on empty pubkeys array", async () => { + await expect( + withdrawalVault.connect(gateway).addWithdrawalRequests([], [], { value: 0 }), + ).to.be.revertedWithCustomError(withdrawalVault, "ZeroArgument"); + }); + + it("should revert on mismatched pubkeys/amounts length", async () => { + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + await expect( + withdrawalVault.connect(gateway).addWithdrawalRequests([PUBKEYS[0]], AMOUNTS, { value: withdrawalFee }), + ).to.be.revertedWithCustomError(withdrawalVault, "ArraysLengthMismatch"); + }); + + it("should revert on incorrect fee", async () => { + await expect( + withdrawalVault.connect(gateway).addWithdrawalRequests(PUBKEYS, AMOUNTS, { value: 0 }), + ).to.be.revertedWithCustomError(withdrawalVault, "IncorrectFee"); + }); + + it("should emit WithdrawalRequestAdded for each request", async () => { + //Clear any existing withdrawal requests before adding new ones + while ((await readWithdrawalRequests()).length > 0) { + /* empty */ + } + + const withdrawalFee = await withdrawalVault.getWithdrawalRequestFee(); + const totalFee = withdrawalFee * BigInt(PUBKEYS.length); + await expect(withdrawalVault.connect(gateway).addWithdrawalRequests(PUBKEYS, AMOUNTS, { value: totalFee })) + .to.emit(withdrawalVault, "WithdrawalRequestAdded") + .withArgs(encodeEIP7002Payload(PUBKEYS[0], AMOUNTS[0])) + .and.to.emit(withdrawalVault, "WithdrawalRequestAdded") + .withArgs(encodeEIP7002Payload(PUBKEYS[1], AMOUNTS[1])); + + const requests = await readWithdrawalRequests(); + expect(requests.length).to.equal(PUBKEYS.length); + + expect(requests[0].address.toLocaleLowerCase()).to.equal(withdrawalVaultAddress.toLocaleLowerCase()); + expect(requests[0].pubkey).to.equal(PUBKEYS[0]); + expect(requests[0].amount).to.equal(AMOUNTS[0]); + + expect(requests[1].address.toLocaleLowerCase()).to.equal(withdrawalVaultAddress.toLocaleLowerCase()); + expect(requests[1].pubkey).to.equal(PUBKEYS[1]); + expect(requests[1].amount).to.equal(AMOUNTS[1]); + + expect((await readWithdrawalRequests()).length).to.equal(0); + }); +}); From 6db5b9003201236528c01ecfccaa4537c5bc43cb Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Fri, 13 Jun 2025 11:12:29 +0200 Subject: [PATCH 288/405] feat: use via-ir for solidity 0.8.25 --- hardhat.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index 2badba3cac..65ca4fa882 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -153,6 +153,7 @@ const config: HardhatUserConfig = { enabled: true, runs: 200, }, + viaIR: true, evmVersion: "cancun", }, }, From 4c1730d7bb4be6b32d001ffa75d88c54a3f1dadc Mon Sep 17 00:00:00 2001 From: Eugene Mamin Date: Fri, 13 Jun 2025 14:27:09 +0300 Subject: [PATCH 289/405] try out heap tweak with IR --- .github/workflows/coverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ad2939d467..06b27b3133 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,6 +9,8 @@ jobs: coverage: name: Hardhat runs-on: ubuntu-latest + env: + NODE_OPTIONS: --max_old_space_size=4096 permissions: contents: write From 9a18a594aa5f8252e35cba34d24528a4e78a74ab Mon Sep 17 00:00:00 2001 From: Eugene Mamin Date: Fri, 13 Jun 2025 14:39:19 +0300 Subject: [PATCH 290/405] Update coverage.yml --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 06b27b3133..13be41217f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,7 +10,7 @@ jobs: name: Hardhat runs-on: ubuntu-latest env: - NODE_OPTIONS: --max_old_space_size=4096 + NODE_OPTIONS: --max_old_space_size=6400 permissions: contents: write From 1053ef68c2f60ef140910475a4b4fa327dc75ef4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 14:10:28 +0200 Subject: [PATCH 291/405] feat: deploy configuration for holesky testnet --- hardhat.config.ts | 2 +- scripts/deploy-tw.sh | 6 ++---- scripts/triggerable-withdrawals/tw-deploy.ts | 21 ++++++++++---------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 2badba3cac..ac658ebef1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -60,7 +60,7 @@ const config: HardhatUserConfig = { "holesky": { url: process.env.HOLESKY_RPC_URL || RPC_URL, chainId: 17000, - // accounts: loadAccounts("holesky"), + accounts: loadAccounts("holesky"), }, "hoodi": { url: process.env.HOLESKY_RPC_URL || RPC_URL, diff --git a/scripts/deploy-tw.sh b/scripts/deploy-tw.sh index 3a86a9f7f8..13e21b6113 100755 --- a/scripts/deploy-tw.sh +++ b/scripts/deploy-tw.sh @@ -2,10 +2,8 @@ set -e +u set -o pipefail -export NETWORK=hoodi +export NETWORK=holesky export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise -export SLOTS_PER_EPOCH=32 -export GENESIS_TIME=1639659600 # just some time # export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" # export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" @@ -13,7 +11,7 @@ export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of defau export GAS_PRIORITY_FEE=1 export GAS_MAX_FEE=100 -export NETWORK_STATE_FILE="deployed-hoodi.json" +export NETWORK_STATE_FILE="deployed-holesky.json" # export NETWORK_STATE_DEFAULTS_FILE="scripts/scratch/deployed-testnet-defaults.json" diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index d7bbb19c71..0bfed65253 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -21,13 +21,12 @@ function getEnvVariable(name: string, defaultValue?: string) { } } -// Must comply with the specification -// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 -const SECONDS_PER_SLOT = 12; - -// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis -// and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb -const genesisTime = parseInt(getEnvVariable("GENESIS_TIME")); +type ChainSpec = { + slotsPerEpoch: number; + secondsPerSlot: number; + genesisTime: number; + depositContractAddress: string; +}; async function main() { const deployer = ethers.getAddress(getEnvVariable("DEPLOYER")); @@ -38,7 +37,7 @@ async function main() { const state = readNetworkState(); persistNetworkState(state); - const chainSpec = state[Sk.chainSpec]; + const chainSpec = state[Sk.chainSpec] as ChainSpec; log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); @@ -52,7 +51,7 @@ async function main() { // Deploy ValidatorExitBusOracle // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator - const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; + const validatorsExitBusOracleArgs = [chainSpec.secondsPerSlot, chainSpec.genesisTime, locator.address]; const validatorsExitBusOracle = await deployImplementation( Sk.validatorsExitBusOracle, @@ -116,7 +115,7 @@ async function main() { 1, // uint64 pivotSlot, 32, // uint32 slotsPerEpoch, 12, // uint32 secondsPerSlot, - genesisTime, // uint64 genesisTime, + chainSpec.genesisTime, // uint64 genesisTime, 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds ]; @@ -162,7 +161,7 @@ async function main() { log(`Configuration for voting script:`); log(` LIDO_LOCATOR_IMPL = "${lidoLocator.address}" -ACCOUNTING_ORACLE = "${accountingOracle.address}" +ACCOUNTING_ORACLE_IMPL = "${accountingOracle.address}" VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}" WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" From 61a6c163f442ffdc0b6b5a351ac640fe27ce188e Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 14:14:17 +0200 Subject: [PATCH 292/405] feat: refactor deployment script for triggerable withdrawals --- scripts/triggerable-withdrawals/tw-deploy.ts | 219 +++++++++++-------- 1 file changed, 123 insertions(+), 96 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 0bfed65253..006b5cc1d9 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -8,126 +8,147 @@ import { cy, deployImplementation, loadContract, log, persistNetworkState, readN dotenv.config({ path: join(__dirname, "../../.env") }); -function getEnvVariable(name: string, defaultValue?: string) { - const value = process.env[name]; - if (value === undefined) { - if (defaultValue === undefined) { - throw new Error(`Env variable ${name} must be set`); - } - return defaultValue; - } else { - log(`Using env variable ${name}=${value}`); - return value; - } +//-------------------------------------------------------------------------- +// Constants +//-------------------------------------------------------------------------- + +// Consensus‑spec constants +const SECONDS_PER_SLOT = 12; +const SLOTS_PER_EPOCH = 32; +const SHARD_COMMITTEE_PERIOD_SLOTS = 2 ** 8 * SLOTS_PER_EPOCH; // 8192 + +// G‑indices (phase0 spec) +const VALIDATOR_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; +const VALIDATOR_CURR_GINDEX = VALIDATOR_PREV_GINDEX; +const HISTORICAL_SUMMARIES_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; +const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; + +// TriggerableWithdrawalsGateway params +const TRIGGERABLE_WITHDRAWALS_GAS_LIMIT = 13_000; +const TRIGGERABLE_WITHDRAWALS_MIN_PRIORITY_FEE = 1; // wei +const TRIGGERABLE_WITHDRAWALS_MAX_VALIDATORS = 48; + +//-------------------------------------------------------------------------- +// Helpers +//-------------------------------------------------------------------------- + +function requireEnv(variable: string): string { + const value = process.env[variable]; + if (!value) throw new Error(`Environment variable ${variable} is not set`); + log(`Using env variable ${variable}=${value}`); + return value; } -type ChainSpec = { - slotsPerEpoch: number; - secondsPerSlot: number; - genesisTime: number; - depositContractAddress: string; -}; +//-------------------------------------------------------------------------- +// Main +//-------------------------------------------------------------------------- -async function main() { - const deployer = ethers.getAddress(getEnvVariable("DEPLOYER")); - const chainId = (await ethers.provider.getNetwork()).chainId; +async function main(): Promise { + // ----------------------------------------------------------------------- + // Environment & chain context + // ----------------------------------------------------------------------- + const deployer = ethers.getAddress(requireEnv("DEPLOYER")); + const genesisTime = parseInt(requireEnv("GENESIS_TIME"), 10); - log(cy(`Deploy of contracts on chain ${chainId}`)); + const { chainId } = await ethers.provider.getNetwork(); + log(cy(`Deploying contracts on chain ${chainId}`)); + // ----------------------------------------------------------------------- + // State & configuration + // ----------------------------------------------------------------------- const state = readNetworkState(); persistNetworkState(state); - const chainSpec = state[Sk.chainSpec] as ChainSpec; - + const chainSpec = state[Sk.chainSpec]; log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); const agent = state["app:aragon-agent"].proxy.address; log(`Using agent: ${agent}`); - // Read contracts addresses from config - const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); - const LIDO_PROXY = await locator.lido(); - const TREASURY_PROXY = await locator.treasury(); + const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); - // Deploy ValidatorExitBusOracle - // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator - const validatorsExitBusOracleArgs = [chainSpec.secondsPerSlot, chainSpec.genesisTime, locator.address]; + // ----------------------------------------------------------------------- + // Deployments + // ----------------------------------------------------------------------- + // 1. ValidatorsExitBusOracle const validatorsExitBusOracle = await deployImplementation( Sk.validatorsExitBusOracle, "ValidatorsExitBusOracle", deployer, - validatorsExitBusOracleArgs, + [SECONDS_PER_SLOT, genesisTime, locator.address], ); - log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle.address}`); - log.emptyLine(); + log.success(`ValidatorsExitBusOracle: ${validatorsExitBusOracle.address}`); + // 2. TriggerableWithdrawalsGateway const triggerableWithdrawalsGateway = await deployImplementation( Sk.triggerableWithdrawalsGateway, "TriggerableWithdrawalsGateway", deployer, - [agent, locator.address, 13000, 1, 48], + [ + agent, + locator.address, + TRIGGERABLE_WITHDRAWALS_GAS_LIMIT, + TRIGGERABLE_WITHDRAWALS_MIN_PRIORITY_FEE, + TRIGGERABLE_WITHDRAWALS_MAX_VALIDATORS, + ], ); - log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); - log.emptyLine(); - - const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY, triggerableWithdrawalsGateway.address]; + log.success(`TriggerableWithdrawalsGateway: ${triggerableWithdrawalsGateway.address}`); - const withdrawalVault = await deployImplementation( - Sk.withdrawalVault, - "WithdrawalVault", - deployer, - withdrawalVaultArgs, - ); - log.success(`WithdrawalVault address implementation: ${withdrawalVault.address}`); + // 3. WithdrawalVault + const withdrawalVault = await deployImplementation(Sk.withdrawalVault, "WithdrawalVault", deployer, [ + await locator.lido(), + await locator.treasury(), + triggerableWithdrawalsGateway.address, + ]); + log.success(`WithdrawalVault: ${withdrawalVault.address}`); + // ----------------------------------------------------------------------- + // Shared libraries + // ----------------------------------------------------------------------- const minFirstAllocationStrategyAddress = state[Sk.minFirstAllocationStrategy].address; const libraries = { MinFirstAllocationStrategy: minFirstAllocationStrategyAddress, - }; + } as const; - const DEPOSIT_CONTRACT_ADDRESS = state[Sk.chainSpec].depositContract; - log(`Deposit contract address: ${DEPOSIT_CONTRACT_ADDRESS}`); - const stakingRouterAddress = await deployImplementation( + // 4. StakingRouter + const stakingRouter = await deployImplementation( Sk.stakingRouter, "StakingRouter", deployer, - [DEPOSIT_CONTRACT_ADDRESS], + [chainSpec.depositContractAddress], { libraries }, ); + log.success(`StakingRouter: ${stakingRouter.address}`); - log(`StakingRouter implementation address: ${stakingRouterAddress.address}`); - - const NOR = await deployImplementation(Sk.appNodeOperatorsRegistry, "NodeOperatorsRegistry", deployer, [], { + // 5. NodeOperatorsRegistry + const nor = await deployImplementation(Sk.appNodeOperatorsRegistry, "NodeOperatorsRegistry", deployer, [], { libraries, }); + log.success(`NodeOperatorsRegistry: ${nor.address}`); - log.success(`NOR implementation address: ${NOR.address}`); - log.emptyLine(); - - const validatorExitDelayVerifierArgs = [ - locator.address, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, - 1, // uint64 firstSupportedSlot, - 1, // uint64 pivotSlot, - 32, // uint32 slotsPerEpoch, - 12, // uint32 secondsPerSlot, - chainSpec.genesisTime, // uint64 genesisTime, - 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds - ]; - + // 6. ValidatorExitDelayVerifier const validatorExitDelayVerifier = await deployImplementation( Sk.validatorExitDelayVerifier, "ValidatorExitDelayVerifier", deployer, - validatorExitDelayVerifierArgs, + [ + locator.address, + VALIDATOR_PREV_GINDEX, + VALIDATOR_CURR_GINDEX, + HISTORICAL_SUMMARIES_PREV_GINDEX, + HISTORICAL_SUMMARIES_CURR_GINDEX, + 1, // firstSupportedSlot + 1, // pivotSlot + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + genesisTime, + SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT, // seconds + ], ); - log.success(`ValidatorExitDelayVerifier implementation address: ${validatorExitDelayVerifier.address}`); - log.emptyLine(); + log.success(`ValidatorExitDelayVerifier: ${validatorExitDelayVerifier.address}`); + // 7. AccountingOracle const accountingOracle = await deployImplementation(Sk.accountingOracle, "AccountingOracle", deployer, [ locator.address, await locator.lido(), @@ -135,8 +156,11 @@ async function main() { Number(chainSpec.secondsPerSlot), Number(chainSpec.genesisTime), ]); + log.success(`AccountingOracle: ${accountingOracle.address}`); - // fetch contract addresses that will not changed + // ----------------------------------------------------------------------- + // New LidoLocator (all addresses consolidated) + // ----------------------------------------------------------------------- const locatorConfig = [ await locator.accountingOracle(), await locator.depositSecurityModule(), @@ -146,34 +170,37 @@ async function main() { await locator.oracleReportSanityChecker(), await locator.postTokenRebaseReceiver(), await locator.burner(), - await locator.stakingRouter(), + stakingRouter.address, await locator.treasury(), - await locator.validatorsExitBusOracle(), + validatorsExitBusOracle.address, await locator.withdrawalQueue(), - await locator.withdrawalVault(), + withdrawalVault.address, await locator.oracleDaemonConfig(), validatorExitDelayVerifier.address, triggerableWithdrawalsGateway.address, ]; - const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); - - log(`Configuration for voting script:`); - log(` -LIDO_LOCATOR_IMPL = "${lidoLocator.address}" -ACCOUNTING_ORACLE_IMPL = "${accountingOracle.address}" -VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}" -WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" -STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" -NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" -VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" -TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" -`); + const newLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); + log.success(`LidoLocator: ${newLocator.address}`); + + // ----------------------------------------------------------------------- + // Governance summary + // ----------------------------------------------------------------------- + log.emptyLine(); + log(`Configuration for governance script:`); + log.emptyLine(); + log(`LIDO_LOCATOR_IMPL = "${newLocator.address}"`); + log(`ACCOUNTING_ORACLE_IMPL = "${accountingOracle.address}"`); + log(`VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}"`); + log(`WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}"`); + log(`STAKING_ROUTER_IMPL = "${stakingRouter.address}"`); + log(`NODE_OPERATORS_REGISTRY_IMPL = "${nor.address}"`); + log(`VALIDATOR_EXIT_DELAY_VERIFIER_IMPL = "${validatorExitDelayVerifier.address}"`); + log(`TRIGGERABLE_WITHDRAWALS_GATEWAY_IMPL = "${triggerableWithdrawalsGateway.address}"\n`); + log.emptyLine(); } -main() - .then(() => process.exit(0)) - .catch((error) => { - log.error(error); - process.exit(1); - }); +main().catch((error) => { + log.error(error); + process.exitCode = 1; +}); From 9374f78b573a282f353f688ae4eb6e7bb99ff973 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 14:18:16 +0200 Subject: [PATCH 293/405] fix: locator config in tw deploy script --- scripts/triggerable-withdrawals/tw-deploy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 006b5cc1d9..04553c05fa 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -170,11 +170,11 @@ async function main(): Promise { await locator.oracleReportSanityChecker(), await locator.postTokenRebaseReceiver(), await locator.burner(), - stakingRouter.address, + await locator.stakingRouter(), await locator.treasury(), - validatorsExitBusOracle.address, + await locator.validatorsExitBusOracle(), await locator.withdrawalQueue(), - withdrawalVault.address, + await locator.withdrawalVault(), await locator.oracleDaemonConfig(), validatorExitDelayVerifier.address, triggerableWithdrawalsGateway.address, From c0d35b168109732be1fd85a20d321295975bcfff Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 15:34:53 +0200 Subject: [PATCH 294/405] refactor: move consensus-spec constants to chainSpec for better configuration management --- scripts/triggerable-withdrawals/tw-deploy.ts | 57 ++++++++++---------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 04553c05fa..3c551e2084 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -8,26 +8,6 @@ import { cy, deployImplementation, loadContract, log, persistNetworkState, readN dotenv.config({ path: join(__dirname, "../../.env") }); -//-------------------------------------------------------------------------- -// Constants -//-------------------------------------------------------------------------- - -// Consensus‑spec constants -const SECONDS_PER_SLOT = 12; -const SLOTS_PER_EPOCH = 32; -const SHARD_COMMITTEE_PERIOD_SLOTS = 2 ** 8 * SLOTS_PER_EPOCH; // 8192 - -// G‑indices (phase0 spec) -const VALIDATOR_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; -const VALIDATOR_CURR_GINDEX = VALIDATOR_PREV_GINDEX; -const HISTORICAL_SUMMARIES_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; -const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; - -// TriggerableWithdrawalsGateway params -const TRIGGERABLE_WITHDRAWALS_GAS_LIMIT = 13_000; -const TRIGGERABLE_WITHDRAWALS_MIN_PRIORITY_FEE = 1; // wei -const TRIGGERABLE_WITHDRAWALS_MAX_VALIDATORS = 48; - //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- @@ -48,7 +28,6 @@ async function main(): Promise { // Environment & chain context // ----------------------------------------------------------------------- const deployer = ethers.getAddress(requireEnv("DEPLOYER")); - const genesisTime = parseInt(requireEnv("GENESIS_TIME"), 10); const { chainId } = await ethers.provider.getNetwork(); log(cy(`Deploying contracts on chain ${chainId}`)); @@ -59,9 +38,33 @@ async function main(): Promise { const state = readNetworkState(); persistNetworkState(state); - const chainSpec = state[Sk.chainSpec]; + const chainSpec = state[Sk.chainSpec] as { + slotsPerEpoch: number; + secondsPerSlot: number; + genesisTime: number; + depositContractAddress: string; + }; + log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); + // Consensus‑spec constants + const SECONDS_PER_SLOT = chainSpec.secondsPerSlot; + const SLOTS_PER_EPOCH = chainSpec.slotsPerEpoch; + const GENESIS_TIME = chainSpec.genesisTime; + const DEPOSIT_CONTRACT_ADDRESS = chainSpec.depositContractAddress; + const SHARD_COMMITTEE_PERIOD_SLOTS = 2 ** 8 * SLOTS_PER_EPOCH; // 8192 + + // G‑indices (phase0 spec) + const VALIDATOR_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const VALIDATOR_CURR_GINDEX = VALIDATOR_PREV_GINDEX; + const HISTORICAL_SUMMARIES_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; + const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; + + // TriggerableWithdrawalsGateway params + const TRIGGERABLE_WITHDRAWALS_GAS_LIMIT = 13_000; + const TRIGGERABLE_WITHDRAWALS_MIN_PRIORITY_FEE = 1; // wei + const TRIGGERABLE_WITHDRAWALS_MAX_VALIDATORS = 48; + const agent = state["app:aragon-agent"].proxy.address; log(`Using agent: ${agent}`); @@ -76,7 +79,7 @@ async function main(): Promise { Sk.validatorsExitBusOracle, "ValidatorsExitBusOracle", deployer, - [SECONDS_PER_SLOT, genesisTime, locator.address], + [SECONDS_PER_SLOT, GENESIS_TIME, locator.address], ); log.success(`ValidatorsExitBusOracle: ${validatorsExitBusOracle.address}`); @@ -116,7 +119,7 @@ async function main(): Promise { Sk.stakingRouter, "StakingRouter", deployer, - [chainSpec.depositContractAddress], + [DEPOSIT_CONTRACT_ADDRESS], { libraries }, ); log.success(`StakingRouter: ${stakingRouter.address}`); @@ -142,7 +145,7 @@ async function main(): Promise { 1, // pivotSlot SLOTS_PER_EPOCH, SECONDS_PER_SLOT, - genesisTime, + GENESIS_TIME, SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT, // seconds ], ); @@ -153,8 +156,8 @@ async function main(): Promise { locator.address, await locator.lido(), await locator.legacyOracle(), - Number(chainSpec.secondsPerSlot), - Number(chainSpec.genesisTime), + SECONDS_PER_SLOT, + GENESIS_TIME, ]); log.success(`AccountingOracle: ${accountingOracle.address}`); From 3bbd295ae9d8cf557cdc6f0bfb773b722c6b77b8 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 15:36:55 +0200 Subject: [PATCH 295/405] refactor: update triggerable withdrawals parameters --- scripts/triggerable-withdrawals/tw-deploy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 3c551e2084..9336b86286 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -61,9 +61,9 @@ async function main(): Promise { const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; // TriggerableWithdrawalsGateway params - const TRIGGERABLE_WITHDRAWALS_GAS_LIMIT = 13_000; - const TRIGGERABLE_WITHDRAWALS_MIN_PRIORITY_FEE = 1; // wei - const TRIGGERABLE_WITHDRAWALS_MAX_VALIDATORS = 48; + const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 13_000; + const TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME = 1; + const TRIGGERABLE_WITHDRAWALS_FRAME_DURATION = 48; const agent = state["app:aragon-agent"].proxy.address; log(`Using agent: ${agent}`); @@ -91,9 +91,9 @@ async function main(): Promise { [ agent, locator.address, - TRIGGERABLE_WITHDRAWALS_GAS_LIMIT, - TRIGGERABLE_WITHDRAWALS_MIN_PRIORITY_FEE, - TRIGGERABLE_WITHDRAWALS_MAX_VALIDATORS, + TRIGGERABLE_WITHDRAWALS_MAX_LIMIT, + TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME, + TRIGGERABLE_WITHDRAWALS_FRAME_DURATION, ], ); log.success(`TriggerableWithdrawalsGateway: ${triggerableWithdrawalsGateway.address}`); From 8079bc96951c16e803b7240479b3ea8358c44362 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 15:44:08 +0200 Subject: [PATCH 296/405] refactor: remove unused .env.sample and tw-verify.ts files --- scripts/triggerable-withdrawals/.env.sample | 9 -- scripts/triggerable-withdrawals/tw-verify.ts | 92 -------------------- 2 files changed, 101 deletions(-) delete mode 100644 scripts/triggerable-withdrawals/.env.sample delete mode 100644 scripts/triggerable-withdrawals/tw-verify.ts diff --git a/scripts/triggerable-withdrawals/.env.sample b/scripts/triggerable-withdrawals/.env.sample deleted file mode 100644 index 8157a2159d..0000000000 --- a/scripts/triggerable-withdrawals/.env.sample +++ /dev/null @@ -1,9 +0,0 @@ -# Deployer -DEPLOYER= -DEPLOYER_PRIVATE_KEY= -# Chain config -RPC_URL= -NETWORK= -# Deploy transactions gas -GAS_PRIORITY_FEE= -GAS_MAX_FEE= diff --git a/scripts/triggerable-withdrawals/tw-verify.ts b/scripts/triggerable-withdrawals/tw-verify.ts deleted file mode 100644 index fae50f443b..0000000000 --- a/scripts/triggerable-withdrawals/tw-verify.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as dotenv from "dotenv"; -import { ethers, run } from "hardhat"; -import { join } from "path"; - -import { LidoLocator } from "typechain-types"; - -import { cy, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; - -dotenv.config({ path: join(__dirname, "../../.env") }); - -function getEnvVariable(name: string, defaultValue?: string) { - const value = process.env[name]; - if (value === undefined) { - if (defaultValue === undefined) { - throw new Error(`Env variable ${name} must be set`); - } - return defaultValue; - } else { - log(`Using env variable ${name}=${value}`); - return value; - } -} - -// Must comply with the specification -// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 -const SECONDS_PER_SLOT = 12; - -// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis -// and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb -const genesisTime = parseInt(getEnvVariable("GENESIS_TIME")); - -async function main() { - const chainId = (await ethers.provider.getNetwork()).chainId; - - log(cy(`Deploy of contracts on chain ${chainId}`)); - - const state = readNetworkState(); - persistNetworkState(state); - - // Read contracts addresses from config - const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); - - const LIDO_PROXY = await locator.lido(); - const TREASURY_PROXY = await locator.treasury(); - - const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; - const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY]; - const validatorExitDelayVerifierArgs = [ - locator.address, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, - 1, // uint64 firstSupportedSlot, - 1, // uint64 pivotSlot, - 32, // uint32 slotsPerEpoch, - 12, // uint32 secondsPerSlot, - genesisTime, // uint64 genesisTime, - 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds - ]; - - await run("verify:verify", { - address: state[Sk.withdrawalVault].implementation.address, - constructorArguments: withdrawalVaultArgs, - contract: "contracts/0.8.9/WithdrawalVault.sol:WithdrawalVault", - }); - - await run("verify:verify", { - address: state[Sk.validatorsExitBusOracle].implementation.address, - constructorArguments: validatorsExitBusOracleArgs, - contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", - }); - - await run("verify:verify", { - address: state[Sk.validatorExitDelayVerifier].implementation.address, - constructorArguments: validatorExitDelayVerifierArgs, - contract: "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol:ValidatorsExitBusOracle", - }); - - await run("verify:verify", { - address: state[Sk.lidoLocator].implementation.address, - constructorArguments: [], // TBD - contract: "contracts/0.8.9/LidoLocator.sol:LidoLocator", - }); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - log.error(error); - process.exit(1); - }); From b7a11efee2ca89e23e795f326be3c8b54c527e54 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 16:05:00 +0200 Subject: [PATCH 297/405] refactor: remove deprecated refunded validators count functionality --- contracts/0.8.9/StakingRouter.sol | 20 ++---------------- contracts/0.8.9/interfaces/IStakingModule.sol | 5 ----- .../StakingModule__MockForStakingRouter.sol | 4 ---- ...gModule__MockForTriggerableWithdrawals.sol | 4 ---- .../stakingRouter.module-sync.test.ts | 21 +------------------ 5 files changed, 3 insertions(+), 51 deletions(-) diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 6a4fdb8565..a669e45654 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -364,22 +364,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ); } - /// @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. - /// @param _nodeOperatorId Id of the node operator. - /// @param _refundedValidatorsCount New number of refunded validators of the node operator. - /// @dev The function is restricted to the `STAKING_MODULE_MANAGE_ROLE` role. - function updateRefundedValidatorsCount( - uint256 _stakingModuleId, - uint256 _nodeOperatorId, - uint256 _refundedValidatorsCount - ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { - _getIStakingModuleById(_stakingModuleId).updateRefundedValidatorsCount( - _nodeOperatorId, _refundedValidatorsCount - ); - } - /// @notice Reports the minted rewards to the staking modules with the specified ids. /// @param _stakingModuleIds Ids of the staking modules. /// @param _totalShares Total shares minted for the staking modules. @@ -767,6 +751,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version /// @notice The number of validators that can't be withdrawn, but deposit costs were /// compensated to the Lido by the node operator. + /// @dev [deprecated] Refunded validators processing has been removed, this field is no longer used. uint256 refundedValidatorsCount; /// @notice A time when the penalty for stuck validators stops applying to node operator rewards. @@ -819,7 +804,7 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version uint256 targetLimitMode, uint256 targetValidatorsCount, /* uint256 stuckValidatorsCount */, - uint256 refundedValidatorsCount, + /* uint256 refundedValidatorsCount */, /* uint256 stuckPenaltyEndTimestamp */, uint256 totalExitedValidators, uint256 totalDepositedValidators, @@ -827,7 +812,6 @@ contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Version ) = stakingModule.getNodeOperatorSummary(_nodeOperatorId); summary.targetLimitMode = targetLimitMode; summary.targetValidatorsCount = targetValidatorsCount; - summary.refundedValidatorsCount = refundedValidatorsCount; summary.totalExitedValidators = totalExitedValidators; summary.totalDepositedValidators = totalDepositedValidators; summary.depositableValidatorsCount = depositableValidatorsCount; diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/0.8.9/interfaces/IStakingModule.sol index be04b2fd1d..6ab6f14f5b 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/0.8.9/interfaces/IStakingModule.sol @@ -160,11 +160,6 @@ interface IStakingModule { bytes calldata _exitedValidatorsCounts ) external; - /// @notice Updates the number of the refunded validators for node operator with the given id - /// @param _nodeOperatorId Id of the node operator - /// @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 _targetLimitMode target limit mode diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index 0115d48f38..673dbeea28 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -194,10 +194,6 @@ contract StakingModule__MockForStakingRouter is IStakingModule { emit Mock__ExitedValidatorsCountUpdated(_nodeOperatorIds, _stuckValidatorsCounts); } - function updateRefundedValidatorsCount(uint256 _nodeOperatorId, uint256 _refundedValidatorsCount) external { - emit Mock__RefundedValidatorsCountUpdated(_nodeOperatorId, _refundedValidatorsCount); - } - function updateTargetValidatorsLimits( uint256 _nodeOperatorId, uint256 _targetLimitMode, diff --git a/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol b/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol index b1a3fdf905..95e09ef5a9 100644 --- a/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol +++ b/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol @@ -99,10 +99,6 @@ contract StakingModule__MockForTriggerableWithdrawals is IStakingModule { return; } - function updateRefundedValidatorsCount(uint256, uint256) external pure override { - return; - } - function updateExitedValidatorsCount(bytes calldata, bytes calldata) external pure override { return; } diff --git a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts index eed62c8654..85a4a3015d 100644 --- a/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts +++ b/test/0.8.9/stakingRouter/stakingRouter.module-sync.test.ts @@ -127,7 +127,7 @@ describe("StakingRouter.sol:module-sync", () => { 1, // targetLimitMode 100n, // targetValidatorsCount 0n, // stuckValidatorsCount - 5n, // refundedValidatorsCount + 0n, // refundedValidatorsCount 0n, // stuckPenaltyEndTimestamp 50, // totalExitedValidators 1000n, // totalDepositedValidators @@ -364,25 +364,6 @@ describe("StakingRouter.sol:module-sync", () => { }); }); - context("updateRefundedValidatorsCount", () => { - const NODE_OPERATOR_ID = 0n; - const REFUNDED_VALIDATORS_COUNT = 10n; - - it("Reverts if the caller does not have the role", async () => { - await expect( - stakingRouter - .connect(user) - .updateRefundedValidatorsCount(moduleId, NODE_OPERATOR_ID, REFUNDED_VALIDATORS_COUNT), - ).to.be.revertedWithOZAccessControlError(user.address, await stakingRouter.STAKING_MODULE_MANAGE_ROLE()); - }); - - it("Redirects the call to the staking module", async () => { - await expect(stakingRouter.updateRefundedValidatorsCount(moduleId, NODE_OPERATOR_ID, REFUNDED_VALIDATORS_COUNT)) - .to.emit(stakingModule, "Mock__RefundedValidatorsCountUpdated") - .withArgs(NODE_OPERATOR_ID, REFUNDED_VALIDATORS_COUNT); - }); - }); - context("reportRewardsMinted", () => { it("Reverts if the caller does not have the role", async () => { await expect( From 09003797cd034375bc26b5e8ba252c93377d0f6d Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 15 Jun 2025 11:41:34 +0200 Subject: [PATCH 298/405] fix: contracts verification --- scripts/deploy-tw.sh | 2 +- tasks/verify-contracts.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/deploy-tw.sh b/scripts/deploy-tw.sh index 13e21b6113..bd34264cc5 100755 --- a/scripts/deploy-tw.sh +++ b/scripts/deploy-tw.sh @@ -7,7 +7,7 @@ export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value s # export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" # export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" -export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of default mnemonic "test test ..." +export DEPLOYER=${DEPLOYER:="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} # first acc of default mnemonic "test test ..." export GAS_PRIORITY_FEE=1 export GAS_MAX_FEE=100 diff --git a/tasks/verify-contracts.ts b/tasks/verify-contracts.ts index 3aa99009c2..cab49d23bd 100644 --- a/tasks/verify-contracts.ts +++ b/tasks/verify-contracts.ts @@ -99,6 +99,8 @@ async function verifyContract(contract: DeployedContract, hre: HardhatRuntimeEnv function getDeployedContract(contract: Contract): DeployedContract[] { if ("proxy" in contract && "implementation" in contract) { return [contract.proxy, contract.implementation]; + } else if ("implementation" in contract) { + return [contract.implementation as DeployedContract]; } else if ("contract" in contract && "address" in contract && "constructorArgs" in contract) { return [contract]; } From 8c5f90d8bbf6a3341a174eaa0123fbfe7ecb2cfe Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Mon, 16 Jun 2025 15:19:33 +0300 Subject: [PATCH 299/405] feat(deploy-holesky): deploy contracts on holesky --- deployed-holesky.json | 132 +++++++++++++++---- scripts/triggerable-withdrawals/tw-deploy.ts | 2 +- 2 files changed, 111 insertions(+), 23 deletions(-) diff --git a/deployed-holesky.json b/deployed-holesky.json index 61174a4e0a..a51d66362c 100644 --- a/deployed-holesky.json +++ b/deployed-holesky.json @@ -14,7 +14,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x748CE008ac6b15634ceD5a6083796f75695052a2", + "address": "0xCA2689BE9b3Fc8a02F61f7CC3a7d0968119c53b5", "constructorArgs": [ "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", @@ -133,7 +133,7 @@ "app:node-operators-registry": { "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x605A3AFadF35A8a8fa4f4Cd4fe34a09Bbcea7718", + "address": "0x834aa47DCd21A32845099a78B4aBb17A7f0bD503", "constructorArgs": [] }, "aragonApp": { @@ -368,12 +368,16 @@ "implementation": { "contract": "@aragon/os/contracts/kernel/Kernel.sol", "address": "0x34c0cbf9836FD945423bD3d2d72880da9d068E5F", - "constructorArgs": [true] + "constructorArgs": [ + true + ] }, "proxy": { "address": "0x3b03f75Ec541Ca11a223bB58621A3146246E1644", "contract": "@aragon/os/contracts/kernel/KernelProxy.sol", - "constructorArgs": ["0x34c0cbf9836FD945423bD3d2d72880da9d068E5F"] + "constructorArgs": [ + "0x34c0cbf9836FD945423bD3d2d72880da9d068E5F" + ] } }, "aragonEnsLabelName": "aragonpm", @@ -465,7 +469,9 @@ "eip712StETH": { "contract": "contracts/0.8.9/EIP712StETH.sol", "address": "0xE154732c5Eab277fd88a9fF6Bdff7805eD97BCB1", - "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034"] + "constructorArgs": [ + "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034" + ] }, "ensAddress": "0x4327d1Fc6E5fa0326CCAE737F67C066c50BcC258", "ensFactoryAddress": "0xADba3e3122F2Da8F7B07723a3e1F1cEDe3fe8d7d", @@ -476,7 +482,10 @@ "executionLayerRewardsVault": { "contract": "contracts/0.8.9/LidoExecutionLayerRewardsVault.sol", "address": "0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8", - "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d"] + "constructorArgs": [ + "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", + "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d" + ] }, "gateSeal": { "factoryAddress": "0x1134F7077055b0B3559BE52AfeF9aA22A0E1eEC2", @@ -559,7 +568,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0xab89ED3D8f31bcF8BB7De53F02084d1e6F043D34", + "address": "0xa437ab5614033d071493C88Fd351aFEbc802521f", "constructorArgs": [ [ "0x4E97A3972ce8511D87F334dA17a2C332542a5246", @@ -567,7 +576,7 @@ "0xE73a3602b99f1f913e72F8bdcBC235e206794Ac8", "0x072f72BE3AcFE2c52715829F2CD9061A6C8fF019", "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", - "0xd7C870777e08325Ad0A3A85F41E66E7D84B63E4f", + "0x80D1B1fF6E84134404abA18A628347960c38ccA7", "0x072f72BE3AcFE2c52715829F2CD9061A6C8fF019", "0x4E46BD7147ccf666E1d73A3A456fC7a68de82eCA", "0xd6EbF043D30A7fe46D1Db32BA90a0A51207FE229", @@ -575,7 +584,9 @@ "0xffDDF7025410412deaa05E3E1cE68FE53208afcb", "0xc7cc160b58F8Bb0baC94b80847E2CF2800565C50", "0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9", - "0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7" + "0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7", + "0x9c5da60e54fcae8592132Fc9a67511e686b52BE8", + "0x4FD4113f2B92856B59BC3be77f2943B7F4eaa9a5" ] ] } @@ -613,7 +624,10 @@ "oracleDaemonConfig": { "contract": "contracts/0.8.9/OracleDaemonConfig.sol", "address": "0xC01fC1F2787687Bc656EAc0356ba9Db6e6b7afb7", - "constructorArgs": ["0x22896Bfc68814BFD855b1a167255eE497006e730", []], + "constructorArgs": [ + "0x22896Bfc68814BFD855b1a167255eE497006e730", + [] + ], "deployParameters": { "NORMALIZED_CL_REWARD_PER_EPOCH": 64, "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000, @@ -643,11 +657,34 @@ "constructorArgs": [ "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d", - [9000, 500, 1000, 50, 600, 8, 62, 7680, 750000, 43200], - [[], [], [], [], [], [], [], [], [], [], []] + [ + 9000, + 500, + 1000, + 50, + 600, + 8, + 62, + 7680, + 750000, + 43200 + ], + [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ] ] }, - "scratchDeployGasUsed": "20484707", + "scratchDeployGasUsed": "65513521", "stakingRouter": { "proxy": { "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", @@ -660,8 +697,42 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x9b5890E950E3Df487Bb64E0A6743cdE791139152", - "constructorArgs": ["0x4242424242424242424242424242424242424242"] + "address": "0xE6E775C6AdF8753588237b1De32f61937bC54341", + "constructorArgs": [ + "0x4242424242424242424242424242424242424242" + ] + } + }, + "triggerableWithdrawalsGateway": { + "implementation": { + "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", + "address": "0x4FD4113f2B92856B59BC3be77f2943B7F4eaa9a5", + "constructorArgs": [ + "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d", + "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", + 11200, + 1, + 48 + ] + } + }, + "validatorExitDelayVerifier": { + "implementation": { + "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", + "address": "0x9c5da60e54fcae8592132Fc9a67511e686b52BE8", + "constructorArgs": [ + "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", + "0x0000000000000000000000000000000000000000000000000096000000000028", + "0x0000000000000000000000000000000000000000000000000096000000000028", + "0x0000000000000000000000000000000000000000000000000000000000005b00", + "0x0000000000000000000000000000000000000000000000000000000000005b00", + 1, + 1, + 32, + 12, + 1695902400, + 98304 + ] } }, "validatorsExitBusOracle": { @@ -679,8 +750,12 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0x210f60EC8A4D020b3e22f15fee2d2364e9b22357", - "constructorArgs": [12, 1695902400, "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8"] + "address": "0xeCE105ABd3F2653398BE75e680dB033A238E2aD6", + "constructorArgs": [ + 12, + 1695902400, + "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8" + ] } }, "vestingParams": { @@ -712,24 +787,37 @@ "implementation": { "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol", "address": "0xFF72B5cdc701E9eE677966B2702c766c38F412a4", - "constructorArgs": ["0x8d09a4502Cc8Cf1547aD300E066060D043f6982D", "stETH Withdrawal NFT", "unstETH"] + "constructorArgs": [ + "0x8d09a4502Cc8Cf1547aD300E066060D043f6982D", + "stETH Withdrawal NFT", + "unstETH" + ] } }, "withdrawalVault": { "implementation": { "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0xd517d9d04DA9B47dA23df91261bd3bF435BE964A", - "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d"] + "address": "0x6aAA28C515E02ED0fe1B51e74323e14E910eA7d7", + "constructorArgs": [ + "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", + "0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d", + "0x4FD4113f2B92856B59BC3be77f2943B7F4eaa9a5" + ] }, "proxy": { "contract": "contracts/0.8.4/WithdrawalsManagerProxy.sol", "address": "0xF0179dEC45a37423EAD4FaD5fCb136197872EAd9", - "constructorArgs": ["0xdA7d2573Df555002503F29aA4003e398d28cc00f", "0xd517d9d04DA9B47dA23df91261bd3bF435BE964A"] + "constructorArgs": [ + "0xdA7d2573Df555002503F29aA4003e398d28cc00f", + "0xd517d9d04DA9B47dA23df91261bd3bF435BE964A" + ] } }, "wstETH": { "contract": "contracts/0.6.12/WstETH.sol", "address": "0x8d09a4502Cc8Cf1547aD300E066060D043f6982D", - "constructorArgs": ["0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034"] + "constructorArgs": [ + "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034" + ] } } diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 9336b86286..61c3510976 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -61,7 +61,7 @@ async function main(): Promise { const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; // TriggerableWithdrawalsGateway params - const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 13_000; + const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 11_200; const TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME = 1; const TRIGGERABLE_WITHDRAWALS_FRAME_DURATION = 48; From 831ec2c6860e173e5cfb5ef12a2fdab04eff40d4 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Tue, 17 Jun 2025 11:18:28 +0200 Subject: [PATCH 300/405] feat: update addWithdrawalRequests method comment --- contracts/0.8.9/WithdrawalVault.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/0.8.9/WithdrawalVault.sol b/contracts/0.8.9/WithdrawalVault.sol index 6793027747..9964bea5e4 100644 --- a/contracts/0.8.9/WithdrawalVault.sol +++ b/contracts/0.8.9/WithdrawalVault.sol @@ -145,8 +145,7 @@ contract WithdrawalVault is Versioned, WithdrawalVaultEIP7002 { * Each full withdrawal request instructs a validator to fully withdraw its stake and exit its duties as a validator. * Each partial withdrawal request instructs a validator to withdraw a specified amount of ETH. * - * @param pubkeys A tightly packed array of 48-byte public keys corresponding to validators requesting partial withdrawals. - * | ----- public key (48 bytes) ----- || ----- public key (48 bytes) ----- | ... + * @param pubkeys An array of 48-byte public keys corresponding to validators requesting withdrawals. * * @param amounts An array of 8-byte unsigned integers that represent the amounts, denominated in Gwei, * to be withdrawn for each corresponding public key. From e657dcad0bc5eec21dd03f8cc073f84c910c5948 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 18 Jun 2025 22:34:45 +0200 Subject: [PATCH 301/405] feat: add new GateSeal types and implement deployGateSeal function --- lib/state-file.ts | 2 + scripts/triggerable-withdrawals/tw-deploy.ts | 74 +++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/state-file.ts b/lib/state-file.ts index 746096976c..271853156a 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -67,6 +67,8 @@ export enum Sk { vestingParams = "vestingParams", withdrawalVault = "withdrawalVault", gateSeal = "gateSeal", + gateSealVEBO = "gateSealVEBO", + gateSealTWG = "gateSealTWG", stakingRouter = "stakingRouter", burner = "burner", executionLayerRewardsVault = "executionLayerRewardsVault", diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 61c3510976..8495a4782d 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -4,7 +4,19 @@ import { join } from "path"; import { LidoLocator } from "typechain-types"; -import { cy, deployImplementation, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; +import { + cy, + deployImplementation, + DeploymentState, + findEvents, + loadContract, + log, + makeTx, + persistNetworkState, + readNetworkState, + Sk, + updateObjectInState, +} from "lib"; dotenv.config({ path: join(__dirname, "../../.env") }); @@ -19,6 +31,39 @@ function requireEnv(variable: string): string { return value; } +async function deployGateSeal( + state: DeploymentState, + deployer: string, + sealableContract: string, + expiryTimestamp: number, + kind: Sk.gateSealVEBO | Sk.gateSealTWG, +): Promise { + const gateSealFactory = await loadContract("IGateSealFactory", state[Sk.gateSeal].factoryAddress); + + const receipt = await makeTx( + gateSealFactory, + "create_gate_seal", + [state[Sk.gateSeal].sealingCommittee, state[Sk.gateSeal].sealDuration, [sealableContract], expiryTimestamp], + { from: deployer }, + ); + + // Extract and log the new GateSeal address + const gateSealAddress = await findEvents(receipt, "GateSealCreated")[0].args.gate_seal; + log(`GateSeal created: ${cy(gateSealAddress)}`); + log.emptyLine(); + + // Update the state with the new GateSeal address + updateObjectInState(kind, { + factoryAddress: state[Sk.gateSeal].factoryAddress, + sealDuration: state[Sk.gateSeal].sealDuration, + expiryTimestamp, + sealingCommittee: state[Sk.gateSeal].sealingCommittee, + address: gateSealAddress, + }); + + return gateSealAddress; +} + //-------------------------------------------------------------------------- // Main //-------------------------------------------------------------------------- @@ -30,6 +75,9 @@ async function main(): Promise { const deployer = ethers.getAddress(requireEnv("DEPLOYER")); const { chainId } = await ethers.provider.getNetwork(); + const currentBlock = await ethers.provider.getBlock("latest"); + if (!currentBlock) throw new Error("Failed to fetch the latest block"); + log(cy(`Deploying contracts on chain ${chainId}`)); // ----------------------------------------------------------------------- @@ -65,6 +113,9 @@ async function main(): Promise { const TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME = 1; const TRIGGERABLE_WITHDRAWALS_FRAME_DURATION = 48; + // GateSeal params + const EXPIRY_TIMESTAMP = currentBlock.timestamp + 365 * 24 * 60 * 60; // 1 year + const agent = state["app:aragon-agent"].proxy.address; log(`Using agent: ${agent}`); @@ -186,6 +237,24 @@ async function main(): Promise { const newLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); log.success(`LidoLocator: ${newLocator.address}`); + // 8. GateSeal for ValidatorsExitBusOracle + const GATE_SEAL_VEBO = await deployGateSeal( + state, + deployer, + await locator.validatorsExitBusOracle(), + EXPIRY_TIMESTAMP, + Sk.gateSealVEBO, + ); + + // 9. GateSeal for TriggerableWithdrawalsGateway + const GATE_SEAL_TWG = await deployGateSeal( + state, + deployer, + triggerableWithdrawalsGateway.address, + EXPIRY_TIMESTAMP, + Sk.gateSealTWG, + ); + // ----------------------------------------------------------------------- // Governance summary // ----------------------------------------------------------------------- @@ -201,6 +270,9 @@ async function main(): Promise { log(`VALIDATOR_EXIT_DELAY_VERIFIER_IMPL = "${validatorExitDelayVerifier.address}"`); log(`TRIGGERABLE_WITHDRAWALS_GATEWAY_IMPL = "${triggerableWithdrawalsGateway.address}"\n`); log.emptyLine(); + log(`GATE_SEAL_VEBO = "${GATE_SEAL_VEBO}"`); + log(`GATE_SEAL_TWG = "${GATE_SEAL_TWG}"`); + log.emptyLine(); } main().catch((error) => { From c7372de2d6999e6e655350f3fbde9a7cb86ef29b Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Thu, 19 Jun 2025 16:11:11 +0300 Subject: [PATCH 302/405] refactor: make imports abs and move a few interfaces to common --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 7 ++++--- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 12 ++++++------ contracts/0.8.9/LidoLocator.sol | 2 +- contracts/0.8.9/StakingRouter.sol | 10 ++++------ contracts/0.8.9/TriggerableWithdrawalsGateway.sol | 3 ++- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 +- .../{0.8.25 => common}/interfaces/IStakingRouter.sol | 5 +++-- .../interfaces/IValidatorsExitBus.sol | 3 ++- test/0.8.25/contracts/StakingRouter_Mock.sol | 2 +- .../contracts/ValidatorsExitBusOracle_Mock.sol | 2 +- 10 files changed, 25 insertions(+), 23 deletions(-) rename contracts/{0.8.25 => common}/interfaces/IStakingRouter.sol (70%) rename contracts/{0.8.25 => common}/interfaces/IValidatorsExitBus.sol (85%) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index df5d755b82..c4b2dabf56 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -8,9 +8,10 @@ import {AragonApp} from "@aragon/os/contracts/apps/AragonApp.sol"; import {SafeMath} from "@aragon/os/contracts/lib/math/SafeMath.sol"; import {UnstructuredStorage} from "@aragon/os/contracts/common/UnstructuredStorage.sol"; -import {Math256} from "../../common/lib/Math256.sol"; -import {MinFirstAllocationStrategy} from "../../common/lib/MinFirstAllocationStrategy.sol"; -import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; +import {Math256} from "contracts/common/lib/Math256.sol"; +import {MinFirstAllocationStrategy} from "contracts/common/lib/MinFirstAllocationStrategy.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; + import {SigningKeys} from "../lib/SigningKeys.sol"; import {Packed64x4} from "../lib/Packed64x4.sol"; import {Versioned} from "../utils/Versioned.sol"; diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 85cfb9765c..15a8dad6dd 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -3,12 +3,12 @@ pragma solidity 0.8.25; -import {BeaconBlockHeader, Validator} from "../common/lib/BeaconTypes.sol"; -import {GIndex} from "../common/lib/GIndex.sol"; -import {SSZ} from "../common/lib/SSZ.sol"; -import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; -import {IValidatorsExitBus} from "./interfaces/IValidatorsExitBus.sol"; -import {IStakingRouter} from "./interfaces/IStakingRouter.sol"; +import {IStakingRouter} from "contracts/common/interfaces/IStakingRouter.sol"; +import {BeaconBlockHeader, Validator} from "contracts/common/lib/BeaconTypes.sol"; +import {GIndex} from "contracts/common/lib/GIndex.sol"; +import {SSZ} from "contracts/common/lib/SSZ.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; +import {IValidatorsExitBus} from "contracts/common/interfaces/IValidatorsExitBus.sol"; struct ExitRequestData { bytes data; diff --git a/contracts/0.8.9/LidoLocator.sol b/contracts/0.8.9/LidoLocator.sol index 83bdd7e7b8..bcb9614baf 100644 --- a/contracts/0.8.9/LidoLocator.sol +++ b/contracts/0.8.9/LidoLocator.sol @@ -4,7 +4,7 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; -import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; /** * @title LidoLocator diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index a669e45654..b7fbd44e45 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -4,16 +4,14 @@ /* See contracts/COMPILERS.md */ pragma solidity 0.8.9; -import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; +import {MinFirstAllocationStrategy} from "contracts/common/lib/MinFirstAllocationStrategy.sol"; +import {Math256} from "contracts/common/lib/Math256.sol"; +import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {IStakingModule} from "./interfaces/IStakingModule.sol"; - -import {Math256} from "../common/lib/Math256.sol"; import {UnstructuredStorage} from "./lib/UnstructuredStorage.sol"; -import {MinFirstAllocationStrategy} from "../common/lib/MinFirstAllocationStrategy.sol"; - -import {BeaconChainDepositor} from "./BeaconChainDepositor.sol"; import {Versioned} from "./utils/Versioned.sol"; +import {BeaconChainDepositor} from "./BeaconChainDepositor.sol"; contract StakingRouter is AccessControlEnumerable, BeaconChainDepositor, Versioned { using UnstructuredStorage for bytes32; diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 278c216b9e..a5cec33490 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -2,8 +2,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; + import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; -import {ILidoLocator} from "../common/interfaces/ILidoLocator.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; import {PausableUntil} from "./utils/PausableUntil.sol"; diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 35b7bea125..e99174078d 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; +import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {AccessControlEnumerable} from "../utils/access/AccessControlEnumerable.sol"; import {UnstructuredStorage} from "../lib/UnstructuredStorage.sol"; -import {ILidoLocator} from "../../common/interfaces/ILidoLocator.sol"; import {Versioned} from "../utils/Versioned.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; import {PausableUntil} from "../utils/PausableUntil.sol"; diff --git a/contracts/0.8.25/interfaces/IStakingRouter.sol b/contracts/common/interfaces/IStakingRouter.sol similarity index 70% rename from contracts/0.8.25/interfaces/IStakingRouter.sol rename to contracts/common/interfaces/IStakingRouter.sol index db9c92badd..c905bb32fd 100644 --- a/contracts/0.8.25/interfaces/IStakingRouter.sol +++ b/contracts/common/interfaces/IStakingRouter.sol @@ -1,8 +1,9 @@ -// SPDX-FileCopyrightText: 2024 Lido +// SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md -pragma solidity 0.8.25; +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.5.0; interface IStakingRouter { function reportValidatorExitDelay( diff --git a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol b/contracts/common/interfaces/IValidatorsExitBus.sol similarity index 85% rename from contracts/0.8.25/interfaces/IValidatorsExitBus.sol rename to contracts/common/interfaces/IValidatorsExitBus.sol index dfc8984047..3931151fb4 100644 --- a/contracts/0.8.25/interfaces/IValidatorsExitBus.sol +++ b/contracts/common/interfaces/IValidatorsExitBus.sol @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md -pragma solidity 0.8.25; +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.5.0; interface IValidatorsExitBus { function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp); diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol index c9c610d073..5084f7feac 100644 --- a/test/0.8.25/contracts/StakingRouter_Mock.sol +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {IStakingRouter} from "contracts/0.8.25/interfaces/IStakingRouter.sol"; +import {IStakingRouter} from "contracts/common/interfaces/IStakingRouter.sol"; contract StakingRouter_Mock is IStakingRouter { // An event to track when reportValidatorExitDelay is called diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 8eb3df7354..89c2c4f96e 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IValidatorsExitBus} from "contracts/0.8.25/interfaces/IValidatorsExitBus.sol"; +import {IValidatorsExitBus} from "contracts/common/interfaces/IValidatorsExitBus.sol"; struct MockExitRequestData { bytes pubkey; From 14d785e0d791239b3a5cb71031bba9c864f6844a Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 20 Jun 2025 12:51:24 +0200 Subject: [PATCH 303/405] feat: refactor GateSeal types and update deployGateSeal function parameters --- lib/state-file.ts | 3 +- scripts/triggerable-withdrawals/tw-deploy.ts | 38 +++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/state-file.ts b/lib/state-file.ts index 271853156a..83bd5e2be1 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -67,8 +67,7 @@ export enum Sk { vestingParams = "vestingParams", withdrawalVault = "withdrawalVault", gateSeal = "gateSeal", - gateSealVEBO = "gateSealVEBO", - gateSealTWG = "gateSealTWG", + gateSealTW = "gateSealTW", stakingRouter = "stakingRouter", burner = "burner", executionLayerRewardsVault = "executionLayerRewardsVault", diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 8495a4782d..8a6985caff 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -34,16 +34,17 @@ function requireEnv(variable: string): string { async function deployGateSeal( state: DeploymentState, deployer: string, - sealableContract: string, + sealableContracts: string[], + sealDuration: number, expiryTimestamp: number, - kind: Sk.gateSealVEBO | Sk.gateSealTWG, + kind: Sk.gateSeal | Sk.gateSealTW, ): Promise { const gateSealFactory = await loadContract("IGateSealFactory", state[Sk.gateSeal].factoryAddress); const receipt = await makeTx( gateSealFactory, "create_gate_seal", - [state[Sk.gateSeal].sealingCommittee, state[Sk.gateSeal].sealDuration, [sealableContract], expiryTimestamp], + [state[Sk.gateSeal].sealingCommittee, sealDuration, sealableContracts, expiryTimestamp], { from: deployer }, ); @@ -55,7 +56,7 @@ async function deployGateSeal( // Update the state with the new GateSeal address updateObjectInState(kind, { factoryAddress: state[Sk.gateSeal].factoryAddress, - sealDuration: state[Sk.gateSeal].sealDuration, + sealDuration, expiryTimestamp, sealingCommittee: state[Sk.gateSeal].sealingCommittee, address: gateSealAddress, @@ -114,7 +115,8 @@ async function main(): Promise { const TRIGGERABLE_WITHDRAWALS_FRAME_DURATION = 48; // GateSeal params - const EXPIRY_TIMESTAMP = currentBlock.timestamp + 365 * 24 * 60 * 60; // 1 year + const GATE_SEAL_EXPIRY_TIMESTAMP = currentBlock.timestamp + 14 * 24 * 60 * 60; // 1 year + const GATE_SEAL_DURATION_SECONDS = 14 * 24 * 60 * 60; // 14 days const agent = state["app:aragon-agent"].proxy.address; log(`Using agent: ${agent}`); @@ -237,22 +239,24 @@ async function main(): Promise { const newLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); log.success(`LidoLocator: ${newLocator.address}`); - // 8. GateSeal for ValidatorsExitBusOracle - const GATE_SEAL_VEBO = await deployGateSeal( + // 8. GateSeal for withdrawalQueueERC721 + const WQ_GATE_SEAL = await deployGateSeal( state, deployer, - await locator.validatorsExitBusOracle(), - EXPIRY_TIMESTAMP, - Sk.gateSealVEBO, + [state.withdrawalQueueERC721.proxy.address], + GATE_SEAL_DURATION_SECONDS, + GATE_SEAL_EXPIRY_TIMESTAMP, + Sk.gateSeal, ); - // 9. GateSeal for TriggerableWithdrawalsGateway - const GATE_SEAL_TWG = await deployGateSeal( + // 9. GateSeal for Triggerable Withdrawals + const TW_GATE_SEAL = await deployGateSeal( state, deployer, - triggerableWithdrawalsGateway.address, - EXPIRY_TIMESTAMP, - Sk.gateSealTWG, + [triggerableWithdrawalsGateway.address, await locator.validatorsExitBusOracle()], + GATE_SEAL_DURATION_SECONDS, + GATE_SEAL_EXPIRY_TIMESTAMP, + Sk.gateSealTW, ); // ----------------------------------------------------------------------- @@ -270,8 +274,8 @@ async function main(): Promise { log(`VALIDATOR_EXIT_DELAY_VERIFIER_IMPL = "${validatorExitDelayVerifier.address}"`); log(`TRIGGERABLE_WITHDRAWALS_GATEWAY_IMPL = "${triggerableWithdrawalsGateway.address}"\n`); log.emptyLine(); - log(`GATE_SEAL_VEBO = "${GATE_SEAL_VEBO}"`); - log(`GATE_SEAL_TWG = "${GATE_SEAL_TWG}"`); + log(`WQ_GATE_SEAL = "${WQ_GATE_SEAL}"`); + log(`TW_GATE_SEAL = "${TW_GATE_SEAL}"`); log.emptyLine(); } From a5848c59e0bbbf906e25f4d0f3700aac2d05f3b2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 20 Jun 2025 13:55:29 +0200 Subject: [PATCH 304/405] feat: make NETWORK variable configurable in deploy-tw script --- scripts/deploy-tw.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy-tw.sh b/scripts/deploy-tw.sh index bd34264cc5..ca03b76699 100755 --- a/scripts/deploy-tw.sh +++ b/scripts/deploy-tw.sh @@ -2,7 +2,7 @@ set -e +u set -o pipefail -export NETWORK=holesky +export NETWORK=${NETWORK:="holesky"} # if defined use the value set to default otherwise export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise # export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" # export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" From d0583b78a64dad221b3b7bc1e7730aa8872790c2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 20 Jun 2025 14:01:48 +0200 Subject: [PATCH 305/405] feat: add legacy support for depositContract in chain spec --- scripts/triggerable-withdrawals/tw-deploy.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 8a6985caff..2c2712ffad 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -91,7 +91,8 @@ async function main(): Promise { slotsPerEpoch: number; secondsPerSlot: number; genesisTime: number; - depositContractAddress: string; + depositContractAddress: string; // legacy support + depositContract?: string; }; log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); @@ -100,7 +101,7 @@ async function main(): Promise { const SECONDS_PER_SLOT = chainSpec.secondsPerSlot; const SLOTS_PER_EPOCH = chainSpec.slotsPerEpoch; const GENESIS_TIME = chainSpec.genesisTime; - const DEPOSIT_CONTRACT_ADDRESS = chainSpec.depositContractAddress; + const DEPOSIT_CONTRACT_ADDRESS = chainSpec.depositContractAddress ?? chainSpec.depositContract; const SHARD_COMMITTEE_PERIOD_SLOTS = 2 ** 8 * SLOTS_PER_EPOCH; // 8192 // G‑indices (phase0 spec) From ce8cf5a285d7639024daabf33bd50413923e6461 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 20 Jun 2025 15:59:18 +0200 Subject: [PATCH 306/405] fix: correct GATE_SEAL_EXPIRY_TIMESTAMP --- scripts/triggerable-withdrawals/tw-deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 2c2712ffad..c6e4b7f909 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -116,7 +116,7 @@ async function main(): Promise { const TRIGGERABLE_WITHDRAWALS_FRAME_DURATION = 48; // GateSeal params - const GATE_SEAL_EXPIRY_TIMESTAMP = currentBlock.timestamp + 14 * 24 * 60 * 60; // 1 year + const GATE_SEAL_EXPIRY_TIMESTAMP = currentBlock.timestamp + 365 * 24 * 60 * 60; // 1 year const GATE_SEAL_DURATION_SECONDS = 14 * 24 * 60 * 60; // 14 days const agent = state["app:aragon-agent"].proxy.address; From 8597ae37d474c9d5ad5414a0050ca1060f9c58a5 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 23 Jun 2025 11:58:16 +0200 Subject: [PATCH 307/405] feat: deploy new gateSeals to holesky testnet --- deployed-holesky.json | 15 +++++++++++---- scripts/triggerable-withdrawals/tw-deploy.ts | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/deployed-holesky.json b/deployed-holesky.json index a51d66362c..029a27c2d2 100644 --- a/deployed-holesky.json +++ b/deployed-holesky.json @@ -489,10 +489,17 @@ }, "gateSeal": { "factoryAddress": "0x1134F7077055b0B3559BE52AfeF9aA22A0E1eEC2", - "sealDuration": 518400, - "expiryTimestamp": 1714521600, + "sealDuration": 1209600, + "expiryTimestamp": 1782208512, "sealingCommittee": "0xCD1f9954330AF39a74Fd6e7B25781B4c24ee373f", - "address": "0x7f6FA688d4C12a2d51936680b241f3B0F0F9ca60" + "address": "0xE900BC859EB750562E1009e912B63743BC877662" + }, + "gateSealTW": { + "factoryAddress": "0x1134F7077055b0B3559BE52AfeF9aA22A0E1eEC2", + "sealDuration": 1209600, + "expiryTimestamp": 1782208512, + "sealingCommittee": "0xCD1f9954330AF39a74Fd6e7B25781B4c24ee373f", + "address": "0xaEEF47C61f2A9CCe4C4D0363911C5d49e2cFb6f1" }, "hashConsensusForAccountingOracle": { "deployParameters": { @@ -684,7 +691,7 @@ ] ] }, - "scratchDeployGasUsed": "65513521", + "scratchDeployGasUsed": "66678077", "stakingRouter": { "proxy": { "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index c6e4b7f909..7c0e978900 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -244,7 +244,7 @@ async function main(): Promise { const WQ_GATE_SEAL = await deployGateSeal( state, deployer, - [state.withdrawalQueueERC721.proxy.address], + [state[Sk.withdrawalQueueERC721].proxy.address], GATE_SEAL_DURATION_SECONDS, GATE_SEAL_EXPIRY_TIMESTAMP, Sk.gateSeal, @@ -254,7 +254,7 @@ async function main(): Promise { const TW_GATE_SEAL = await deployGateSeal( state, deployer, - [triggerableWithdrawalsGateway.address, await locator.validatorsExitBusOracle()], + [state[Sk.triggerableWithdrawalsGateway].implementation.address, await locator.validatorsExitBusOracle()], GATE_SEAL_DURATION_SECONDS, GATE_SEAL_EXPIRY_TIMESTAMP, Sk.gateSealTW, From 5c78974c1bd61fd0702facee3cfcc79193e753dc Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 24 Jun 2025 18:10:15 +0200 Subject: [PATCH 308/405] fix: bump accounting oracle initial version to 3 --- contracts/0.8.9/oracle/AccountingOracle.sol | 6 +++++ lib/constants.ts | 2 +- .../oracle/accountingOracle.upgrade.test.ts | 25 +++++++++++++++++++ .../oracle/baseOracle.accessControl.test.ts | 4 +-- .../0.8.9/oracle/baseOracle.consensus.test.ts | 6 ++--- .../oracle/hashConsensus.submitReport.test.ts | 2 +- 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 4273b017f8..d174611b74 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -165,6 +165,7 @@ contract AccountingOracle is BaseOracle { _initialize(admin, consensusContract, consensusVersion, lastProcessingRefSlot); _updateContractVersion(2); + _updateContractVersion(3); } function initializeWithoutMigration( @@ -178,6 +179,7 @@ contract AccountingOracle is BaseOracle { _initialize(admin, consensusContract, consensusVersion, lastProcessingRefSlot); _updateContractVersion(2); + _updateContractVersion(3); } function finalizeUpgrade_v2(uint256 consensusVersion) external { @@ -185,6 +187,10 @@ contract AccountingOracle is BaseOracle { _setConsensusVersion(consensusVersion); } + function finalizeUpgrade_v3() external { + _updateContractVersion(3); + } + /// /// Data provider interface /// diff --git a/lib/constants.ts b/lib/constants.ts index a53a8d538c..c59b7422c5 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -30,7 +30,7 @@ export const EPOCHS_PER_FRAME = 225n; // one day; // Oracle report related export const GENESIS_TIME = 100n; export const SLOTS_PER_EPOCH = 32n; -export const CONSENSUS_VERSION = 2n; +export const CONSENSUS_VERSION = 3n; export const INITIAL_EPOCH = 1n; export const INITIAL_FAST_LANE_LENGTH_SLOTS = 0n; diff --git a/test/0.8.9/oracle/accountingOracle.upgrade.test.ts b/test/0.8.9/oracle/accountingOracle.upgrade.test.ts index 037ca0fcbc..30c22843d1 100644 --- a/test/0.8.9/oracle/accountingOracle.upgrade.test.ts +++ b/test/0.8.9/oracle/accountingOracle.upgrade.test.ts @@ -39,4 +39,29 @@ describe("AccountingOracle.sol:upgrade", () => { expect(newConsensusVersion).to.not.equal(initialConsensusVersion); }); }); + + context("finalizeUpgrade_v3", () => { + let admin: HardhatEthersSigner; + let oracle: AccountingOracle__Harness; + + beforeEach(async () => { + [admin] = await ethers.getSigners(); + const deployed = await deployAndConfigureAccountingOracle(admin.address); + oracle = deployed.oracle; + await oracle.setContractVersion(2); // Set initial contract version to 1 + }); + + it("successfully updates contract and consensus versions", async () => { + // Get initial versions + const initialContractVersion = await oracle.getContractVersion(); + + // Call finalizeUpgrade_v2 + await oracle.connect(admin).finalizeUpgrade_v3(); + + // Verify contract version updated to 2 + const newContractVersion = await oracle.getContractVersion(); + expect(newContractVersion).to.equal(3); + expect(newContractVersion).to.not.equal(initialContractVersion); + }); + }); }); diff --git a/test/0.8.9/oracle/baseOracle.accessControl.test.ts b/test/0.8.9/oracle/baseOracle.accessControl.test.ts index 9427ec756c..60d9de0b25 100644 --- a/test/0.8.9/oracle/baseOracle.accessControl.test.ts +++ b/test/0.8.9/oracle/baseOracle.accessControl.test.ts @@ -91,9 +91,9 @@ describe("BaseOracle.sol:accessControl", () => { const role = await oracle.MANAGE_CONSENSUS_VERSION_ROLE(); await oracle.grantRole(role, manager); - await oracle.connect(manager).setConsensusVersion(3); + await oracle.connect(manager).setConsensusVersion(4); - expect(await oracle.getConsensusVersion()).to.equal(3); + expect(await oracle.getConsensusVersion()).to.equal(4); }); }); diff --git a/test/0.8.9/oracle/baseOracle.consensus.test.ts b/test/0.8.9/oracle/baseOracle.consensus.test.ts index 80a4793184..894e5e5297 100644 --- a/test/0.8.9/oracle/baseOracle.consensus.test.ts +++ b/test/0.8.9/oracle/baseOracle.consensus.test.ts @@ -137,13 +137,13 @@ describe("BaseOracle.sol:consensus", () => { }); it("Updates consensus version", async () => { - await expect(baseOracle.setConsensusVersion(3)) + await expect(baseOracle.setConsensusVersion(4)) .to.emit(baseOracle, "ConsensusVersionSet") - .withArgs(3, CONSENSUS_VERSION); + .withArgs(4, CONSENSUS_VERSION); const versionInState = await baseOracle.getConsensusVersion(); - expect(versionInState).to.equal(3); + expect(versionInState).to.equal(4); }); }); diff --git a/test/0.8.9/oracle/hashConsensus.submitReport.test.ts b/test/0.8.9/oracle/hashConsensus.submitReport.test.ts index 4e0b177979..ee2cb5a79a 100644 --- a/test/0.8.9/oracle/hashConsensus.submitReport.test.ts +++ b/test/0.8.9/oracle/hashConsensus.submitReport.test.ts @@ -9,7 +9,7 @@ import { CONSENSUS_VERSION } from "lib"; import { deployHashConsensus, HASH_1, HASH_2, ZERO_HASH } from "test/deploy"; import { Snapshot } from "test/suite"; -const CONSENSUS_VERSION_NEW = 3n; +const CONSENSUS_VERSION_NEW = 4n; describe("HashConsensus.sol:submitReport", function () { let admin: Signer; From a9bc3dfa8b2226de7ee3155e3993082407ce2b4a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 24 Jun 2025 23:25:23 +0400 Subject: [PATCH 309/405] fix: audit --- .../0.8.9/TriggerableWithdrawalsGateway.sol | 8 -- contracts/0.8.9/lib/ExitLimitUtils.sol | 7 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 1 + .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 21 ++--- ...r-exit-bus-oracle.submitReportData.test.ts | 81 ------------------- 5 files changed, 10 insertions(+), 108 deletions(-) diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 278c216b9e..c0009a7876 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -47,11 +47,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil */ error AdminCannotBeZero(); - /** - * @notice Thrown when exit request has wrong length - */ - error InvalidRequestsDataLength(); - /** * @notice Thrown when a withdrawal fee insufficient * @param feeRequired Amount of fee required to cover withdrawal request @@ -86,9 +81,6 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil bytes32 public constant TWR_LIMIT_POSITION = keccak256("lido.TriggerableWithdrawalsGateway.maxExitRequestLimit"); - /// Length in bytes of packed triggerable exit request - uint256 internal constant PUBLIC_KEY_LENGTH = 48; - uint256 public constant VERSION = 1; ILidoLocator internal immutable LOCATOR; diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index 615cca3462..be51e8d653 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -74,12 +74,11 @@ library ExitLimitUtils { ) internal pure returns (ExitRequestLimitData memory) { if (_data.maxExitRequestsLimit < newExitRequestLimit) revert LimitExceeded(); - uint256 secondsPassed = timestamp - _data.prevTimestamp; - uint256 framesPassed = secondsPassed / _data.frameDurationInSec; - uint32 passedTime = uint32(framesPassed) * _data.frameDurationInSec; + uint256 passedTime = timestamp - _data.prevTimestamp; + passedTime -= passedTime % _data.frameDurationInSec; _data.prevExitRequestsLimit = uint32(newExitRequestLimit); - _data.prevTimestamp += passedTime; + _data.prevTimestamp += uint32(passedTime); return _data; } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 35b7bea125..a906bb59c8 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -243,6 +243,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V * - The data format is not supported. * - The data length exceeds the maximum number of requests allowed per payload. * - There is no remaining quota available for the current limits. + * - The requests was not sorted in strictly increasing order before the report hash submit. * * Emits `ValidatorExitRequest` events; * diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 6675c595b5..0a43b06182 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -37,9 +37,11 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// @notice An ACL role granting the permission to submit the data for a committee report. bytes32 public constant SUBMIT_DATA_ROLE = keccak256("SUBMIT_DATA_ROLE"); - /// @dev Storage slot: uint256 totalRequestsProcessed - bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = - keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); + /// @dev [DEPRECATED] Storage slot: keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed") + /// This constant defined the storage position where the total number of processed exit requests was stored. + /// This constant was removed from the contract, but slot can still contain logic. + // bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = + // keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); /// @dev [DEPRECATED] Storage slot: mapping(uint256 => RequestedValidator) lastRequestedValidatorIndices /// This mapping was previously used for storing last requested validator indexes per (moduleId, nodeOpId) key. @@ -159,18 +161,11 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { bytes32 reportDataHash = keccak256(abi.encode(data)); _checkConsensusData(data.refSlot, data.consensusVersion, reportDataHash); _startProcessing(); - _handleConsensusReportData(data); _storeOracleExitRequestHash(dataHash, contractVersion); + _handleConsensusReportData(data); emit ExitDataProcessing(dataHash); } - /// @notice Returns the total number of validator exit requests ever processed - /// across all received reports. - /// - function getTotalRequestsProcessed() external view returns (uint256) { - return TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256(); - } - struct ProcessingState { /// @notice Reference slot for the current reporting frame. uint256 currentFrameRefSlot; @@ -267,10 +262,6 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (data.requestsCount == 0) { return; } - - TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256( - TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.requestsCount - ); } function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 contractVersion) internal { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index f4e9687577..eafaa447e3 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -345,42 +345,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { expect(storageAfter.requestsProcessed).to.equal(requests.length); expect(storageAfter.dataFormat).to.equal(DATA_FORMAT_LIST); }); - - it("updates total requests processed count", async () => { - let currentCount = 0; - const countStep0 = await oracle.getTotalRequestsProcessed(); - expect(countStep0).to.equal(currentCount); - - // Step 1 — process 1 item - const requestsStep1 = [{ moduleId: 3, nodeOpId: 1, valIndex: 2, valPubkey: PUBKEYS[1] }]; - const { reportData: reportStep1 } = await prepareReportAndSubmitHash(requestsStep1); - await oracle.connect(member1).submitReportData(reportStep1, oracleVersion); - const countStep1 = await oracle.getTotalRequestsProcessed(); - currentCount += requestsStep1.length; - expect(countStep1).to.equal(currentCount); - - // Step 2 — process 2 items - await consensus.advanceTimeToNextFrameStart(); - const requestsStep2 = [ - { moduleId: 4, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[2] }, - { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, - ]; - const { reportData: reportStep2 } = await prepareReportAndSubmitHash(requestsStep2); - await oracle.connect(member1).submitReportData(reportStep2, oracleVersion); - const countStep2 = await oracle.getTotalRequestsProcessed(); - currentCount += requestsStep2.length; - expect(countStep2).to.equal(currentCount); - - // // Step 3 — process no items - await consensus.advanceTimeToNextFrameStart(); - const requestsStep3: ExitRequest[] = []; - const { reportData: reportStep3 } = await prepareReportAndSubmitHash(requestsStep3); - await oracle.connect(member1).submitReportData(reportStep3, oracleVersion); - - const countStep3 = await oracle.getTotalRequestsProcessed(); - currentCount += requestsStep3.length; - expect(countStep3).to.equal(currentCount); - }); }); context(`only consensus member or SUBMIT_DATA_ROLE can submit report on unpaused contract`, () => { @@ -478,51 +442,6 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); }); - context("getTotalRequestsProcessed reflects report history", () => { - let originalState: string; - - before(async () => { - originalState = await Snapshot.take(); - await consensus.advanceTimeToNextFrameStart(); - }); - - after(async () => await Snapshot.restore(originalState)); - - let requestCount = 0; - - it("should be zero at init", async () => { - requestCount = 0; - expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); - }); - - it("should increase after report", async () => { - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, - ]); - await oracle.connect(member1).submitReportData(reportData, oracleVersion, { from: member1 }); - requestCount += 1; - expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); - }); - - it("should double increase for two exits", async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([ - { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[0] }, - { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[0] }, - ]); - await oracle.connect(member1).submitReportData(reportData, oracleVersion); - requestCount += 2; - expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); - }); - - it("should not change on empty report", async () => { - await consensus.advanceTimeToNextFrameStart(); - const { reportData } = await prepareReportAndSubmitHash([]); - await oracle.connect(member1).submitReportData(reportData, oracleVersion); - expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); - }); - }); - context("getProcessingState reflects state change", () => { let originalState: string; before(async () => { From 18a890b3007c1b57862ac977a26d04a8eeca2d99 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 24 Jun 2025 23:27:50 +0400 Subject: [PATCH 310/405] fix: comment impr --- contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 0a43b06182..68f5f44d6a 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -40,8 +40,8 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// @dev [DEPRECATED] Storage slot: keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed") /// This constant defined the storage position where the total number of processed exit requests was stored. /// This constant was removed from the contract, but slot can still contain logic. - // bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = - // keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); + /// bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = + /// keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); /// @dev [DEPRECATED] Storage slot: mapping(uint256 => RequestedValidator) lastRequestedValidatorIndices /// This mapping was previously used for storing last requested validator indexes per (moduleId, nodeOpId) key. From c85737f32a43f9d3e173a3f26034f2ffb88b1d26 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 24 Jun 2025 23:03:15 +0200 Subject: [PATCH 311/405] fix: update AccountingOracle implementation address for Holesky TW deploy --- deployed-holesky.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployed-holesky.json b/deployed-holesky.json index 029a27c2d2..2a365fea00 100644 --- a/deployed-holesky.json +++ b/deployed-holesky.json @@ -14,7 +14,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0xCA2689BE9b3Fc8a02F61f7CC3a7d0968119c53b5", + "address": "0xE63267AAaC507A329213593e8A9bCa37e2994F1C", "constructorArgs": [ "0x28FAB2059C713A7F9D8c86Db49f9bb0e96Af1ef8", "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", @@ -691,7 +691,7 @@ ] ] }, - "scratchDeployGasUsed": "66678077", + "scratchDeployGasUsed": "70437128", "stakingRouter": { "proxy": { "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", From dc864735058908a62de2407d23b3fb6d97af854e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 25 Jun 2025 11:27:58 +0400 Subject: [PATCH 312/405] fix: TOTAL_REQUESTS_PROCESSED_POSITION for oracle and other entities flows --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 14 ++++ .../0.8.9/oracle/ValidatorsExitBusOracle.sol | 10 +-- ...r-exit-bus-oracle.submitReportData.test.ts | 81 +++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index a906bb59c8..5f418be929 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -193,6 +193,9 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V ILidoLocator internal immutable LOCATOR; + /// @dev Storage slot: uint256 totalRequestsProcessed + bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = + keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); // Storage slot for exit request limit configuration and current quota tracking bytes32 internal constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); // Storage slot for the maximum number of validator exit requests allowed per processing report @@ -269,6 +272,10 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V _processExitRequestsList(request.data); + TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256( + TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + requestsCount + ); + _updateRequestStatus(requestStatus); emit ExitDataProcessing(exitRequestsHash); @@ -483,6 +490,13 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V _pauseUntil(_pauseUntilInclusive); } + /// @notice Returns the total number of validator exit requests ever processed + /// across all received reports. + /// + function getTotalRequestsProcessed() external view returns (uint256) { + return TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256(); + } + /// Internal functions function _checkExitRequestData(bytes calldata requests, uint256 dataFormat) internal pure { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 68f5f44d6a..5189a0ae08 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -37,12 +37,6 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { /// @notice An ACL role granting the permission to submit the data for a committee report. bytes32 public constant SUBMIT_DATA_ROLE = keccak256("SUBMIT_DATA_ROLE"); - /// @dev [DEPRECATED] Storage slot: keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed") - /// This constant defined the storage position where the total number of processed exit requests was stored. - /// This constant was removed from the contract, but slot can still contain logic. - /// bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = - /// keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); - /// @dev [DEPRECATED] Storage slot: mapping(uint256 => RequestedValidator) lastRequestedValidatorIndices /// This mapping was previously used for storing last requested validator indexes per (moduleId, nodeOpId) key. /// This code was removed from the contract, but slots can still contain logic. @@ -262,6 +256,10 @@ contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { if (data.requestsCount == 0) { return; } + + TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256( + TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.requestsCount + ); } function _storeOracleExitRequestHash(bytes32 exitRequestsHash, uint256 contractVersion) internal { diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts index eafaa447e3..f4e9687577 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitReportData.test.ts @@ -345,6 +345,42 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { expect(storageAfter.requestsProcessed).to.equal(requests.length); expect(storageAfter.dataFormat).to.equal(DATA_FORMAT_LIST); }); + + it("updates total requests processed count", async () => { + let currentCount = 0; + const countStep0 = await oracle.getTotalRequestsProcessed(); + expect(countStep0).to.equal(currentCount); + + // Step 1 — process 1 item + const requestsStep1 = [{ moduleId: 3, nodeOpId: 1, valIndex: 2, valPubkey: PUBKEYS[1] }]; + const { reportData: reportStep1 } = await prepareReportAndSubmitHash(requestsStep1); + await oracle.connect(member1).submitReportData(reportStep1, oracleVersion); + const countStep1 = await oracle.getTotalRequestsProcessed(); + currentCount += requestsStep1.length; + expect(countStep1).to.equal(currentCount); + + // Step 2 — process 2 items + await consensus.advanceTimeToNextFrameStart(); + const requestsStep2 = [ + { moduleId: 4, nodeOpId: 2, valIndex: 2, valPubkey: PUBKEYS[2] }, + { moduleId: 5, nodeOpId: 3, valIndex: 2, valPubkey: PUBKEYS[3] }, + ]; + const { reportData: reportStep2 } = await prepareReportAndSubmitHash(requestsStep2); + await oracle.connect(member1).submitReportData(reportStep2, oracleVersion); + const countStep2 = await oracle.getTotalRequestsProcessed(); + currentCount += requestsStep2.length; + expect(countStep2).to.equal(currentCount); + + // // Step 3 — process no items + await consensus.advanceTimeToNextFrameStart(); + const requestsStep3: ExitRequest[] = []; + const { reportData: reportStep3 } = await prepareReportAndSubmitHash(requestsStep3); + await oracle.connect(member1).submitReportData(reportStep3, oracleVersion); + + const countStep3 = await oracle.getTotalRequestsProcessed(); + currentCount += requestsStep3.length; + expect(countStep3).to.equal(currentCount); + }); }); context(`only consensus member or SUBMIT_DATA_ROLE can submit report on unpaused contract`, () => { @@ -442,6 +478,51 @@ describe("ValidatorsExitBusOracle.sol:submitReportData", () => { }); }); + context("getTotalRequestsProcessed reflects report history", () => { + let originalState: string; + + before(async () => { + originalState = await Snapshot.take(); + await consensus.advanceTimeToNextFrameStart(); + }); + + after(async () => await Snapshot.restore(originalState)); + + let requestCount = 0; + + it("should be zero at init", async () => { + requestCount = 0; + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + + it("should increase after report", async () => { + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 3, valIndex: 0, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion, { from: member1 }); + requestCount += 1; + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + + it("should double increase for two exits", async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([ + { moduleId: 5, nodeOpId: 1, valIndex: 10, valPubkey: PUBKEYS[0] }, + { moduleId: 5, nodeOpId: 3, valIndex: 1, valPubkey: PUBKEYS[0] }, + ]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + requestCount += 2; + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + + it("should not change on empty report", async () => { + await consensus.advanceTimeToNextFrameStart(); + const { reportData } = await prepareReportAndSubmitHash([]); + await oracle.connect(member1).submitReportData(reportData, oracleVersion); + expect(await oracle.getTotalRequestsProcessed()).to.equal(requestCount); + }); + }); + context("getProcessingState reflects state change", () => { let originalState: string; before(async () => { From ff246ef93a31adcd400bef40c55daae824b72a6a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 25 Jun 2025 11:31:21 +0400 Subject: [PATCH 313/405] fix: format --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 5f418be929..e3aa47a07f 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -195,7 +195,7 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V /// @dev Storage slot: uint256 totalRequestsProcessed bytes32 internal constant TOTAL_REQUESTS_PROCESSED_POSITION = - keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); + keccak256("lido.ValidatorsExitBusOracle.totalRequestsProcessed"); // Storage slot for exit request limit configuration and current quota tracking bytes32 internal constant EXIT_REQUEST_LIMIT_POSITION = keccak256("lido.ValidatorsExitBus.maxExitRequestLimit"); // Storage slot for the maximum number of validator exit requests allowed per processing report From 7c883c8ab2ef087ec0853a08ae2f412251bc9087 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 3 Jul 2025 14:11:32 +0200 Subject: [PATCH 314/405] fix: update deployGateSeal calls to use updated network state --- hardhat.config.ts | 2 +- scripts/triggerable-withdrawals/tw-deploy.ts | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 5ddc18363b..2e0c46300a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -65,7 +65,7 @@ const config: HardhatUserConfig = { "hoodi": { url: process.env.HOLESKY_RPC_URL || RPC_URL, chainId: 560048, - // accounts: loadAccounts("holesky"), + accounts: loadAccounts("hoodi"), }, "sepolia": { url: process.env.SEPOLIA_RPC_URL || RPC_URL, diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 7c0e978900..b0dd68bdd5 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -240,11 +240,14 @@ async function main(): Promise { const newLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); log.success(`LidoLocator: ${newLocator.address}`); + const updatedState = readNetworkState(); + persistNetworkState(updatedState); + // 8. GateSeal for withdrawalQueueERC721 const WQ_GATE_SEAL = await deployGateSeal( - state, + updatedState, deployer, - [state[Sk.withdrawalQueueERC721].proxy.address], + [updatedState[Sk.withdrawalQueueERC721].proxy.address], GATE_SEAL_DURATION_SECONDS, GATE_SEAL_EXPIRY_TIMESTAMP, Sk.gateSeal, @@ -252,9 +255,9 @@ async function main(): Promise { // 9. GateSeal for Triggerable Withdrawals const TW_GATE_SEAL = await deployGateSeal( - state, + updatedState, deployer, - [state[Sk.triggerableWithdrawalsGateway].implementation.address, await locator.validatorsExitBusOracle()], + [updatedState[Sk.triggerableWithdrawalsGateway].implementation.address, await locator.validatorsExitBusOracle()], GATE_SEAL_DURATION_SECONDS, GATE_SEAL_EXPIRY_TIMESTAMP, Sk.gateSealTW, From 0c2202698a2cc89ccd8ae00b437781cf3520ee90 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 8 Jul 2025 21:34:40 +0200 Subject: [PATCH 315/405] wip: update validator exit delay verifier --- .../0.8.25/ValidatorExitDelayVerifier.sol | 86 +++++++++++++----- contracts/common/lib/GIndex.sol | 21 +---- test/0.8.25/contracts/GIndex__Harness.sol | 4 - test/0.8.25/lib/GIndex.t.sol | 55 ----------- .../0.8.25/validatorExitDelayVerifier.test.ts | 67 +++++++++++--- test/0.8.9/lib/GIndex.test.ts | 91 ------------------- 6 files changed, 120 insertions(+), 204 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 85cfb9765c..4f6f15870d 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -74,17 +74,17 @@ contract ValidatorExitDelayVerifier { */ GIndex public immutable GI_FIRST_VALIDATOR_CURR; - /** - * @notice The GIndex pointing to BeaconState.historical_summaries for the "previous" fork. - * @dev Used when verifying old blocks (i.e., blocks with slot < PIVOT_SLOT). - */ - GIndex public immutable GI_HISTORICAL_SUMMARIES_PREV; + /// @dev This index is relative to a state like: `BeaconState.historical_summaries[0]`. + GIndex public immutable GI_FIRST_HISTORICAL_SUMMARY_PREV; - /** - * @notice The GIndex pointing to BeaconState.historical_summaries for the "current" fork. - * @dev Used when verifying old blocks (i.e., blocks with slot >= PIVOT_SLOT). - */ - GIndex public immutable GI_HISTORICAL_SUMMARIES_CURR; + /// @dev This index is relative to a state like: `BeaconState.historical_summaries[0]`. + GIndex public immutable GI_FIRST_HISTORICAL_SUMMARY_CURR; + + /// @dev This index is relative to HistoricalSummary like: HistoricalSummary.blockRoots[0]. + GIndex public immutable GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV; + + /// @dev This index is relative to HistoricalSummary like: HistoricalSummary.blockRoots[0]. + GIndex public immutable GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR; /// @notice The first slot this verifier will accept proofs for. uint64 public immutable FIRST_SUPPORTED_SLOT; @@ -92,6 +92,13 @@ contract ValidatorExitDelayVerifier { /// @notice The first slot of the currently-compatible fork. uint64 public immutable PIVOT_SLOT; + /// @notice The slot where Capella fork started (when historical summaries became available). + uint64 public immutable CAPELLA_SLOT; + + /// @notice Count of historical roots per accumulator. + /// @dev See https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters + uint64 public immutable SLOTS_PER_HISTORICAL_ROOT; + ILidoLocator public immutable LOCATOR; error RootNotFound(); @@ -99,22 +106,28 @@ contract ValidatorExitDelayVerifier { error InvalidBlockHeader(); error UnsupportedSlot(uint64 slot); error InvalidPivotSlot(); + error InvalidPerHistoricalRootSlot(); error ZeroLidoLocatorAddress(); error ExitIsNotEligibleOnProvableBeaconBlock( uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); error EmptyDeliveryHistory(); + error InvalidCapellaSlot(); /** * @dev The previous and current forks can be essentially the same. * @param lidoLocator The address of the LidoLocator contract. * @param gIFirstValidatorPrev GIndex pointing to validators[0] on the previous fork. * @param gIFirstValidatorCurr GIndex pointing to validators[0] on the current fork. - * @param gIHistoricalSummariesPrev GIndex pointing to the historical_summaries on the previous fork. - * @param gIHistoricalSummariesCurr GIndex pointing to the historical_summaries on the current fork. + * @param gIFirstHistoricalSummaryPrev GIndex pointing to historical summary for the previous fork. + * @param gIFirstHistoricalSummaryCurr GIndex pointing to historical summary for the current fork. + * @param gIFirstBlockRootInSummaryPrev GIndex pointing to the first block root in a historical summary for the previous fork. + * @param gIFirstBlockRootInSummaryCurr GIndex pointing to the first block root in a historical summary for the current fork. * @param firstSupportedSlot The earliest slot number that proofs can be submitted for verification. * @param pivotSlot The pivot slot number used to differentiate "previous" vs "current" fork indexing. + * @param capellaSlot The slot where Capella fork started. + * @param slotsPerHistoricalRoot Number of slots per historical root. * @param slotsPerEpoch Number of slots per epoch in Ethereum consensus. * @param secondsPerSlot Duration of a single slot, in seconds, in Ethereum consensus. * @param genesisTime Genesis timestamp of the Ethereum Beacon chain. @@ -124,10 +137,14 @@ contract ValidatorExitDelayVerifier { address lidoLocator, GIndex gIFirstValidatorPrev, GIndex gIFirstValidatorCurr, - GIndex gIHistoricalSummariesPrev, - GIndex gIHistoricalSummariesCurr, + GIndex gIFirstHistoricalSummaryPrev, + GIndex gIFirstHistoricalSummaryCurr, + GIndex gIFirstBlockRootInSummaryPrev, + GIndex gIFirstBlockRootInSummaryCurr, uint64 firstSupportedSlot, uint64 pivotSlot, + uint64 capellaSlot, + uint64 slotsPerHistoricalRoot, uint32 slotsPerEpoch, uint32 secondsPerSlot, uint64 genesisTime, @@ -135,17 +152,24 @@ contract ValidatorExitDelayVerifier { ) { if (lidoLocator == address(0)) revert ZeroLidoLocatorAddress(); if (firstSupportedSlot > pivotSlot) revert InvalidPivotSlot(); + if (capellaSlot > firstSupportedSlot) revert InvalidCapellaSlot(); + if (slotsPerHistoricalRoot == 0) revert InvalidPerHistoricalRootSlot(); LOCATOR = ILidoLocator(lidoLocator); GI_FIRST_VALIDATOR_PREV = gIFirstValidatorPrev; GI_FIRST_VALIDATOR_CURR = gIFirstValidatorCurr; - GI_HISTORICAL_SUMMARIES_PREV = gIHistoricalSummariesPrev; - GI_HISTORICAL_SUMMARIES_CURR = gIHistoricalSummariesCurr; + GI_FIRST_HISTORICAL_SUMMARY_PREV = gIFirstHistoricalSummaryPrev; + GI_FIRST_HISTORICAL_SUMMARY_CURR = gIFirstHistoricalSummaryCurr; + + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = gIFirstBlockRootInSummaryPrev; + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = gIFirstBlockRootInSummaryCurr; FIRST_SUPPORTED_SLOT = firstSupportedSlot; PIVOT_SLOT = pivotSlot; + CAPELLA_SLOT = capellaSlot; + SLOTS_PER_HISTORICAL_ROOT = slotsPerHistoricalRoot; SLOTS_PER_EPOCH = slotsPerEpoch; SECONDS_PER_SLOT = secondsPerSlot; GENESIS_TIME = genesisTime; @@ -271,15 +295,14 @@ contract ValidatorExitDelayVerifier { revert UnsupportedSlot(oldBlock.header.slot); } - if (!_getHistoricalSummariesGI(beaconBlock.header.slot).isParentOf(oldBlock.rootGIndex)) { - revert InvalidGIndex(); - } - SSZ.verifyProof({ proof: oldBlock.proof, root: beaconBlock.header.stateRoot, leaf: oldBlock.header.hashTreeRoot(), - gI: oldBlock.rootGIndex + gI: _getHistoricalBlockRootGI( + beaconBlock.header.slot, + oldBlock.header.slot + ) }); } @@ -352,8 +375,25 @@ contract ValidatorExitDelayVerifier { return gI.shr(offset); } - function _getHistoricalSummariesGI(uint64 stateSlot) internal view returns (GIndex) { - return stateSlot < PIVOT_SLOT ? GI_HISTORICAL_SUMMARIES_PREV : GI_HISTORICAL_SUMMARIES_CURR; + function _getHistoricalBlockRootGI( + uint64 recentSlot, + uint64 targetSlot + ) internal view returns (GIndex gI) { + uint256 targetSlotShifted = targetSlot - CAPELLA_SLOT; + uint256 summaryIndex = targetSlotShifted / SLOTS_PER_HISTORICAL_ROOT; + uint256 rootIndex = targetSlot % SLOTS_PER_HISTORICAL_ROOT; + + gI = recentSlot < PIVOT_SLOT + ? GI_FIRST_HISTORICAL_SUMMARY_PREV + : GI_FIRST_HISTORICAL_SUMMARY_CURR; + + gI = gI.shr(summaryIndex); // historicalSummaries[summaryIndex] + gI = gI.concat( + targetSlot < PIVOT_SLOT + ? GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV + : GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR + ); // historicalSummaries[summaryIndex].blockRoots[0] + gI = gI.shr(rootIndex); // historicalSummaries[summaryIndex].blockRoots[rootIndex] } function _getExitRequestDeliveryTimestamp( diff --git a/contracts/common/lib/GIndex.sol b/contracts/common/lib/GIndex.sol index 66a3874922..fc818f5408 100644 --- a/contracts/common/lib/GIndex.sol +++ b/contracts/common/lib/GIndex.sol @@ -12,7 +12,7 @@ pragma solidity ^0.8.25; type GIndex is bytes32; -using {isRoot, isParentOf, index, width, shr, shl, concat, unwrap, pow} for GIndex global; +using {isRoot, index, width, shr, shl, concat, unwrap, pow} for GIndex global; error IndexOutOfRange(); @@ -84,25 +84,6 @@ function concat(GIndex lhs, GIndex rhs) pure returns (GIndex) { return pack((index(lhs) << rhsMSbIndex) | (index(rhs) ^ (1 << rhsMSbIndex)), pow(rhs)); } -function isParentOf(GIndex self, GIndex child) pure returns (bool) { - uint256 parentIndex = index(self); - uint256 childIndex = index(child); - - if (parentIndex >= childIndex) { - return false; - } - - while (childIndex > 0) { - if (childIndex == parentIndex) { - return true; - } - - childIndex = childIndex >> 1; - } - - return false; -} - /// @dev From Solady LibBit, see https://github.com/Vectorized/solady/blob/main/src/utils/LibBit.sol. /// @dev Find last set. /// Returns the index of the most significant bit of `x`, diff --git a/test/0.8.25/contracts/GIndex__Harness.sol b/test/0.8.25/contracts/GIndex__Harness.sol index 3fde63ead9..1a1af5b65a 100644 --- a/test/0.8.25/contracts/GIndex__Harness.sol +++ b/test/0.8.25/contracts/GIndex__Harness.sol @@ -25,10 +25,6 @@ contract GIndex__Harness { return gIndex.isRoot(); } - function isParentOf(GIndex lhs, GIndex rhs) external pure returns (bool) { - return lhs.isParentOf(rhs); - } - function index(GIndex gIndex) external pure returns (uint256) { return gIndex.index(); } diff --git a/test/0.8.25/lib/GIndex.t.sol b/test/0.8.25/lib/GIndex.t.sol index d4d8d56fd0..c0595a2e32 100644 --- a/test/0.8.25/lib/GIndex.t.sol +++ b/test/0.8.25/lib/GIndex.t.sol @@ -72,48 +72,6 @@ contract GIndexTest is Test { assertFalse(gI.isRoot(), "Expected [uint248.max,uint8.max].isRoot() to be false"); } - function test_isParentOf_Truthy() public { - assertTrue(pack(1024, 0).isParentOf(pack(2048, 0))); - assertTrue(pack(1024, 0).isParentOf(pack(2049, 0))); - assertTrue(pack(1024, 9).isParentOf(pack(2048, 0))); - assertTrue(pack(1024, 9).isParentOf(pack(2049, 0))); - assertTrue(pack(1024, 0).isParentOf(pack(2048, 9))); - assertTrue(pack(1024, 0).isParentOf(pack(2049, 9))); - assertTrue(pack(1023, 0).isParentOf(pack(4094, 0))); - assertTrue(pack(1024, 0).isParentOf(pack(4098, 0))); - } - - function testFuzz_ROOT_isParentOfAnyChild(GIndex rhs) public { - vm.assume(rhs.index() > 1); - assertTrue(ROOT.isParentOf(rhs)); - } - - function testFuzz_isParentOf_LessThanAnchor(GIndex lhs, GIndex rhs) public { - vm.assume(rhs.index() < lhs.index()); - assertFalse(lhs.isParentOf(rhs)); - } - - function test_isParentOf_OffTheBranch() public { - assertFalse(pack(1024, 0).isParentOf(pack(2050, 0))); - assertFalse(pack(1024, 0).isParentOf(pack(2051, 0))); - assertFalse(pack(1024, 0).isParentOf(pack(2047, 0))); - assertFalse(pack(1024, 0).isParentOf(pack(2046, 0))); - assertFalse(pack(1024, 9).isParentOf(pack(2050, 0))); - assertFalse(pack(1024, 9).isParentOf(pack(2051, 0))); - assertFalse(pack(1024, 9).isParentOf(pack(2047, 0))); - assertFalse(pack(1024, 9).isParentOf(pack(2046, 0))); - assertFalse(pack(1024, 0).isParentOf(pack(2050, 9))); - assertFalse(pack(1024, 0).isParentOf(pack(2051, 9))); - assertFalse(pack(1024, 0).isParentOf(pack(2047, 9))); - assertFalse(pack(1024, 0).isParentOf(pack(2046, 9))); - assertFalse(pack(1023, 0).isParentOf(pack(2048, 0))); - assertFalse(pack(1023, 0).isParentOf(pack(2049, 0))); - assertFalse(pack(1023, 9).isParentOf(pack(2048, 0))); - assertFalse(pack(1023, 9).isParentOf(pack(2049, 0))); - assertFalse(pack(1023, 0).isParentOf(pack(4098, 0))); - assertFalse(pack(1024, 0).isParentOf(pack(4094, 0))); - } - function test_concat() public { assertEq(pack(2, 99).concat(pack(3, 99)).unwrap(), pack(5, 99).unwrap()); assertEq(pack(31, 99).concat(pack(3, 99)).unwrap(), pack(63, 99).unwrap()); @@ -154,19 +112,6 @@ contract GIndexTest is Test { assertEq(ROOT.concat(rhs).unwrap(), rhs.unwrap(), "`concat` with a root should return right-hand side value"); } - function testFuzz_concat_isParentOf(GIndex lhs, GIndex rhs) public { - // Left-hand side value can be a root. - vm.assume(lhs.index() > 0); - // But root.concat(root) will result in a root value again, and root is not a parent for itself. - vm.assume(rhs.index() > 1); - // Overflow check. - vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); - - assertTrue(lhs.isParentOf(lhs.concat(rhs)), "Left-hand side value should be a parent of `concat` result"); - assertFalse(lhs.concat(rhs).isParentOf(lhs), "`concat` result can't be a parent for the left-hand side value"); - assertFalse(lhs.concat(rhs).isParentOf(rhs), "`concat` result can't be a parent for the right-hand side value"); - } - function testFuzz_unpack(uint248 index, uint8 pow) public { GIndex gI = pack(index, pow); assertEq(gI.index(), index); diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 1446aef40c..02ad0ddf26 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -40,12 +40,16 @@ describe("ValidatorExitDelayVerifier.sol", () => { const GENESIS_TIME = 1606824000; const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; + const CAPELLA_SLOT = 1; // Setting this to be <= FIRST_SUPPORTED_SLOT as required + const SLOTS_PER_HISTORICAL_ROOT = 8192; // Added this parameter describe("ValidatorExitDelayVerifier Constructor", () => { const GI_FIRST_VALIDATOR_PREV = `0x${"1".repeat(64)}`; const GI_FIRST_VALIDATOR_CURR = `0x${"2".repeat(64)}`; - const GI_HISTORICAL_SUMMARIES_PREV = `0x${"3".repeat(64)}`; - const GI_HISTORICAL_SUMMARIES_CURR = `0x${"4".repeat(64)}`; + const GI_FIRST_HISTORICAL_SUMMARY_PREV = `0x${"3".repeat(64)}`; + const GI_FIRST_HISTORICAL_SUMMARY_CURR = `0x${"4".repeat(64)}`; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = `0x${"5".repeat(64)}`; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = `0x${"6".repeat(64)}`; let validatorExitDelayVerifier: ValidatorExitDelayVerifier; @@ -54,10 +58,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { LIDO_LOCATOR, GI_FIRST_VALIDATOR_PREV, GI_FIRST_VALIDATOR_CURR, - GI_HISTORICAL_SUMMARIES_PREV, - GI_HISTORICAL_SUMMARIES_CURR, + GI_FIRST_HISTORICAL_SUMMARY_PREV, + GI_FIRST_HISTORICAL_SUMMARY_CURR, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, @@ -68,10 +76,11 @@ describe("ValidatorExitDelayVerifier.sol", () => { it("sets all parameters correctly", async () => { expect(await validatorExitDelayVerifier.LOCATOR()).to.equal(LIDO_LOCATOR); expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); - expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); - expect(await validatorExitDelayVerifier.GI_HISTORICAL_SUMMARIES_PREV()).to.equal(GI_HISTORICAL_SUMMARIES_PREV); - expect(await validatorExitDelayVerifier.GI_HISTORICAL_SUMMARIES_CURR()).to.equal(GI_HISTORICAL_SUMMARIES_CURR); + expect(await validatorExitDelayVerifier.GI_FIRST_HISTORICAL_SUMMARY_PREV()).to.equal(GI_FIRST_HISTORICAL_SUMMARY_PREV); + expect(await validatorExitDelayVerifier.GI_FIRST_HISTORICAL_SUMMARY_CURR()).to.equal(GI_FIRST_HISTORICAL_SUMMARY_CURR); + expect(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV()).to.equal(GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV); + expect(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR()).to.equal(GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR); expect(await validatorExitDelayVerifier.FIRST_SUPPORTED_SLOT()).to.equal(FIRST_SUPPORTED_SLOT); expect(await validatorExitDelayVerifier.PIVOT_SLOT()).to.equal(PIVOT_SLOT); expect(await validatorExitDelayVerifier.SLOTS_PER_EPOCH()).to.equal(SLOTS_PER_EPOCH); @@ -80,6 +89,8 @@ describe("ValidatorExitDelayVerifier.sol", () => { expect(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()).to.equal( SHARD_COMMITTEE_PERIOD_IN_SECONDS, ); + expect(await validatorExitDelayVerifier.CAPELLA_SLOT()).to.equal(CAPELLA_SLOT); + expect(await validatorExitDelayVerifier.SLOTS_PER_HISTORICAL_ROOT()).to.equal(SLOTS_PER_HISTORICAL_ROOT); }); it("reverts with 'InvalidPivotSlot' if firstSupportedSlot > pivotSlot", async () => { @@ -88,10 +99,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { LIDO_LOCATOR, GI_FIRST_VALIDATOR_PREV, GI_FIRST_VALIDATOR_CURR, - GI_HISTORICAL_SUMMARIES_PREV, - GI_HISTORICAL_SUMMARIES_CURR, + GI_FIRST_HISTORICAL_SUMMARY_PREV, + GI_FIRST_HISTORICAL_SUMMARY_CURR, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, 200_000, // firstSupportedSlot 100_000, // pivotSlot < firstSupportedSlot + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, @@ -106,10 +121,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { ethers.ZeroAddress, // Zero address for locator GI_FIRST_VALIDATOR_PREV, GI_FIRST_VALIDATOR_CURR, - GI_HISTORICAL_SUMMARIES_PREV, - GI_HISTORICAL_SUMMARIES_CURR, + GI_FIRST_HISTORICAL_SUMMARY_PREV, + GI_FIRST_HISTORICAL_SUMMARY_CURR, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, @@ -120,6 +139,28 @@ describe("ValidatorExitDelayVerifier.sol", () => { "ZeroLidoLocatorAddress", ); }); + + it("reverts with 'InvalidCapellaSlot' if capellaSlot > firstSupportedSlot", async () => { + await expect( + ethers.deployContract("ValidatorExitDelayVerifier", [ + LIDO_LOCATOR, + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_FIRST_HISTORICAL_SUMMARY_PREV, + GI_FIRST_HISTORICAL_SUMMARY_CURR, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + FIRST_SUPPORTED_SLOT + 1, // Invalid Capella slot + SLOTS_PER_HISTORICAL_ROOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidCapellaSlot"); + }); }); describe("verifyValidatorExitDelay method", () => { @@ -153,8 +194,12 @@ describe("ValidatorExitDelayVerifier.sol", () => { GI_FIRST_VALIDATOR_INDEX, GI_HISTORICAL_SUMMARIES_INDEX, GI_HISTORICAL_SUMMARIES_INDEX, + GI_HISTORICAL_SUMMARIES_INDEX, + GI_HISTORICAL_SUMMARIES_INDEX, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, diff --git a/test/0.8.9/lib/GIndex.test.ts b/test/0.8.9/lib/GIndex.test.ts index ec86337a70..cffe06794a 100644 --- a/test/0.8.9/lib/GIndex.test.ts +++ b/test/0.8.9/lib/GIndex.test.ts @@ -28,10 +28,6 @@ class GIndexWrapper { return await this.contract.isRoot(gIndex); } - async isParentOf(lhs: string, rhs: string): Promise { - return await this.contract.isParentOf(lhs, rhs); - } - async index(gIndex: string): Promise { return await this.contract.index(gIndex); } @@ -109,60 +105,6 @@ describe("GIndex", () => { expect(await gIndex.isRoot(await gIndex.pack(maxUint248, 255))).to.be.false; }); - it("test_isParentOf_Truthy", async () => { - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2048, 0))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2049, 0))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2048, 0))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2049, 0))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2048, 9))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2049, 9))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(4094, 0))).to.be.true; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(4098, 0))).to.be.true; - }); - - it("testFuzz_ROOT_isParentOfAnyChild", async () => { - for (let i = 0; i < 20; i++) { - const randomIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240)) + BigInt(2); - const randomGIndex = await gIndex.wrap(zeroPadValue(ethers.toBeHex(randomIndex), 32)); - - expect(await gIndex.isParentOf(ROOT, randomGIndex)).to.be.true; - } - }); - - it("testFuzz_isParentOf_LessThanAnchor", async () => { - for (let i = 0; i < 10; i++) { - // Create two random indices where lhs > rhs - const lhsIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240)) + BigInt(100); - const rhsIndex = lhsIndex - BigInt(1); - - const lhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(lhsIndex), 32)); - const rhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(rhsIndex), 32)); - - expect(await gIndex.isParentOf(lhs, rhs)).to.be.false; - } - }); - - it("test_isParentOf_OffTheBranch", async () => { - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2050, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2051, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2047, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2046, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2050, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2051, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2047, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 9), await gIndex.pack(2046, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2050, 9))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2051, 9))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2047, 9))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(2046, 9))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(2048, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(2049, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1023, 9), await gIndex.pack(2048, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1023, 9), await gIndex.pack(2049, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1023, 0), await gIndex.pack(4098, 0))).to.be.false; - expect(await gIndex.isParentOf(await gIndex.pack(1024, 0), await gIndex.pack(4094, 0))).to.be.false; - }); - it("test_concat", async () => { expect(await gIndex.unwrap(await gIndex.concat(await gIndex.pack(2, 99), await gIndex.pack(3, 99)))).to.equal( await gIndex.unwrap(await gIndex.pack(5, 99)), @@ -240,39 +182,6 @@ describe("GIndex", () => { } }); - it("testFuzz_concat_isParentOf", async () => { - for (let i = 0; i < 10; i++) { - // Create two random indices - const lhsIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(100)) + BigInt(1); - let rhsIndex = (BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(100)) + BigInt(1); - - // Make sure rhs is not 1 (root) - if (rhsIndex <= 1n) { - rhsIndex = 2n; - } - - const lhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(lhsIndex), 32)); - const rhs = await gIndex.wrap(zeroPadValue(ethers.toBeHex(rhsIndex), 32)); - - // Skip if the concatenation would overflow - const lhsBits = await gIndex.fls(lhsIndex); - const rhsBits = await gIndex.fls(rhsIndex); - - if (lhsBits + 1n + rhsBits >= 248n) { - continue; - } - - const concatenated = await gIndex.concat(lhs, rhs); - - // Verify lhs is parent of lhs.concat(rhs) - expect(await gIndex.isParentOf(lhs, concatenated)).to.be.true; - - // Verify lhs.concat(rhs) is NOT parent of lhs or rhs - expect(await gIndex.isParentOf(concatenated, lhs)).to.be.false; - expect(await gIndex.isParentOf(concatenated, rhs)).to.be.false; - } - }); - it("testFuzz_unpack", async () => { for (let i = 0; i < 20; i++) { const index = BigInt(ethers.hexlify(randomBytes(30))) % BigInt(2) ** BigInt(240); From 3ee467cc26b5f082784a83ecbea2a80b813f887d Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 13:55:13 +0200 Subject: [PATCH 316/405] wip: unit tests --- .../0.8.25/validatorExitDelayVerifier.test.ts | 208 ++++++++++++++++-- test/0.8.25/validatorState.ts | 73 +++--- 2 files changed, 231 insertions(+), 50 deletions(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 02ad0ddf26..ef37f5f3f1 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -33,23 +33,24 @@ describe("ValidatorExitDelayVerifier.sol", () => { await Snapshot.restore(originalState); }); - const FIRST_SUPPORTED_SLOT = 1; - const PIVOT_SLOT = 2; + const FIRST_SUPPORTED_SLOT = 100_500; + const PIVOT_SLOT = 100_501; const SLOTS_PER_EPOCH = 32; const SECONDS_PER_SLOT = 12; const GENESIS_TIME = 1606824000; const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; - const CAPELLA_SLOT = 1; // Setting this to be <= FIRST_SUPPORTED_SLOT as required + const CAPELLA_SLOT = 42; // Setting this to be <= FIRST_SUPPORTED_SLOT as required const SLOTS_PER_HISTORICAL_ROOT = 8192; // Added this parameter describe("ValidatorExitDelayVerifier Constructor", () => { - const GI_FIRST_VALIDATOR_PREV = `0x${"1".repeat(64)}`; - const GI_FIRST_VALIDATOR_CURR = `0x${"2".repeat(64)}`; - const GI_FIRST_HISTORICAL_SUMMARY_PREV = `0x${"3".repeat(64)}`; - const GI_FIRST_HISTORICAL_SUMMARY_CURR = `0x${"4".repeat(64)}`; - const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = `0x${"5".repeat(64)}`; - const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = `0x${"6".repeat(64)}`; + // Updated parameters from the original Solidity test + const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000000000560000000"; + const GI_FIRST_VALIDATOR_CURR = "0x0000000000000000000000000000000000000000000000000000000560000001"; + const GI_FIRST_HISTORICAL_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000000fff0"; + const GI_FIRST_HISTORICAL_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000000ffff"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x0000000000000000000000000000000000000000000000000000000000004000"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x0000000000000000000000000000000000000000000000000000000000004001"; let validatorExitDelayVerifier: ValidatorExitDelayVerifier; @@ -77,10 +78,18 @@ describe("ValidatorExitDelayVerifier.sol", () => { expect(await validatorExitDelayVerifier.LOCATOR()).to.equal(LIDO_LOCATOR); expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); - expect(await validatorExitDelayVerifier.GI_FIRST_HISTORICAL_SUMMARY_PREV()).to.equal(GI_FIRST_HISTORICAL_SUMMARY_PREV); - expect(await validatorExitDelayVerifier.GI_FIRST_HISTORICAL_SUMMARY_CURR()).to.equal(GI_FIRST_HISTORICAL_SUMMARY_CURR); - expect(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV()).to.equal(GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV); - expect(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR()).to.equal(GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR); + expect(await validatorExitDelayVerifier.GI_FIRST_HISTORICAL_SUMMARY_PREV()).to.equal( + GI_FIRST_HISTORICAL_SUMMARY_PREV, + ); + expect(await validatorExitDelayVerifier.GI_FIRST_HISTORICAL_SUMMARY_CURR()).to.equal( + GI_FIRST_HISTORICAL_SUMMARY_CURR, + ); + expect(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV()).to.equal( + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + ); + expect(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR()).to.equal( + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + ); expect(await validatorExitDelayVerifier.FIRST_SUPPORTED_SLOT()).to.equal(FIRST_SUPPORTED_SLOT); expect(await validatorExitDelayVerifier.PIVOT_SLOT()).to.equal(PIVOT_SLOT); expect(await validatorExitDelayVerifier.SLOTS_PER_EPOCH()).to.equal(SLOTS_PER_EPOCH); @@ -644,5 +653,178 @@ describe("ValidatorExitDelayVerifier.sol", () => { ), ).to.be.reverted; }); + + // Test for fork transitions - equivalent to tests in the Solidity file + it("correctly handles proofs before pivot slot with GI_PREV indices", async () => { + // Create a verifier with custom pivot slot + const futureSlot = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot + 100; + const customVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ + locatorAddr, + GI_FIRST_VALIDATOR_INDEX, // PREV + "0x0000000000000000000000000000000000000000000000000000000000000000", // CURR - not used + GI_HISTORICAL_SUMMARIES_INDEX, // PREV + "0x0000000000000000000000000000000000000000000000000000000000000000", // CURR - not used + GI_HISTORICAL_SUMMARIES_INDEX, // PREV + "0x0000000000000000000000000000000000000000000000000000000000000000", // CURR - not used + FIRST_SUPPORTED_SLOT, + futureSlot, // Pivot slot after the current block + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]); + + // Setup exit requests with timestamp before the block + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + + const exitRequests: ExitRequest[] = [ + { + moduleId: 33, + nodeOpId: 44, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + // Verify it successfully processes the proof with PREV indices + const tx = await customVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ); + + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + }); + + it("correctly handles proofs at pivot slot with GI_CURR indices", async () => { + // Create a verifier with pivot slot at the current block slot + const customVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ + locatorAddr, + "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used + GI_FIRST_VALIDATOR_INDEX, // CURR + "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used + GI_HISTORICAL_SUMMARIES_INDEX, // CURR + "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used + GI_HISTORICAL_SUMMARIES_INDEX, // CURR + FIRST_SUPPORTED_SLOT, + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot, // Pivot slot exactly at the block + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]); + + // Setup exit requests with timestamp before the block + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + + const exitRequests: ExitRequest[] = [ + { + moduleId: 55, + nodeOpId: 66, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + // Verify it successfully processes the proof with CURR indices + const tx = await customVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ); + + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + }); + + it("correctly handles proofs after pivot slot with GI_CURR indices", async () => { + // Create a verifier with pivot slot before the current block slot + const customVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ + locatorAddr, + "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used + GI_FIRST_VALIDATOR_INDEX, // CURR + "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used + GI_HISTORICAL_SUMMARIES_INDEX, // CURR + "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used + GI_HISTORICAL_SUMMARIES_INDEX, // CURR + FIRST_SUPPORTED_SLOT, + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - 1, // Pivot slot before the block + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]); + + // Setup exit requests with timestamp before the block + const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; + const veboExitRequestTimestamp = + GENESIS_TIME + + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * + SECONDS_PER_SLOT; + + const exitRequests: ExitRequest[] = [ + { + moduleId: 77, + nodeOpId: 88, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + // Verify it successfully processes the proof with CURR indices + const tx = await customVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ); + + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + }); + }); + + describe("GIndex calculation tests", () => { + // This would test the internal GIndex calculation logic, but since we can't directly access private functions in ethers, + // we'd need to create a testable contract that exposes these functions. + // For the purposes of this PR, let's assume this would be covered by the other tests that use the GIndex functions + // If needed, a separate test contract could be deployed that exposes these internal functions for testing. + // The test would verify: + // 1. getValidatorGI before and after fork change + // 2. getWithdrawalGI before and after fork change + // 3. getHistoricalBlockRootGI before and after pivot + // 4. Special handling of cases with Capella slot at 0 }); }); diff --git a/test/0.8.25/validatorState.ts b/test/0.8.25/validatorState.ts index af8a5034df..bfc762d162 100644 --- a/test/0.8.25/validatorState.ts +++ b/test/0.8.25/validatorState.ts @@ -31,26 +31,26 @@ export type ValidatorStateProof = { }; export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { - beaconBlockHeaderRoot: "0x3959b7073981bd6b71b8dfb37cba8505d69291de2c7c55167be7ed6d361903b7", + beaconBlockHeaderRoot: "0xeb961eae87c614e11a7959a529a59db3c9d825d284dc30e0d12e43ba6daf4cca", beaconBlockHeader: { slot: 22140000, proposerIndex: "1337", - parentRoot: "0x9fff93777a5d7464d400991242767c87401bea8444a19a4e9d69c6fcbf9e8870", - stateRoot: "0x8ae908388464dc5e368cf76126a6e29eb9ac12a1690ea4131eadbf8fd78ae355", + parentRoot: "0x8576d3eb5ef5b3a460b85e5493d0d0510b7d1a2943a4e51add5227c9d3bffa0f", + stateRoot: "0xa802c5f4f818564a2774a19937fdfafc0241f475d7f28312c3609c6e5995d980", bodyRoot: "0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1", }, - futureBeaconBlockHeaderRoot: "0x0e4ac8359cd39eb19803d5b7a299f337b93bf6f683fd4989f5c4ba1804354655", + futureBeaconBlockHeaderRoot: "0x1210ae93b4e995d0fd654d986e26a55cc124ddd0232821378a404340a3e837be", futureBeaconBlockHeader: { slot: 46908000, proposerIndex: "31415", - parentRoot: "0x3ad291882aef24918d223a3f89a6ccc45e0a8f1071977233e3c9f83e5dd5db26", - stateRoot: "0x40231cae24f6beb7c58eac9e7680b0856eeae5604b697edd2eeae0525b185d96", + parentRoot: "0xb11bfc560fb8d69246efe86a367a4e1a083a93c1205c93aa5e74e362a913498d", + stateRoot: "0x4ae3998e0571212dc99159869a603f835927908bc427b8053aced1e65a309873", bodyRoot: "0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1", }, firstValidatorGI: "0x0000000000000000000000000000000000000000000000000096000000000028", validator: { - pubkey: "0xaeb399bf5648b0e9980c1731824c269631a41320c3d7f730c40587e1a37a5e1c8b5755fd90080a7b3fb90d3fd419c0a7", + pubkey: "0xaffd606a767b69df617169824f10baf992c407b059e18d8db2cf4f8cc7432dfe36477e94c9a14e87e1d13e5b22202b92", withdrawalCredentials: "0x010000000000000000000000b3e29c46ee1745724417c0c51eb2351a1c01cf36", effectiveBalance: 32000000000n, activationEligibilityEpoch: 10n, @@ -61,27 +61,27 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { index: 129, }, validatorProof: [ - "0x13d8db07469f8bc21ec0a89cea8dd2d91131d245658a52c496eba69b0d53cbbb", - "0xe5ff0954daf817e5cd32f00b983945cba3d130d60fa5cabed35bf889fd28249a", - "0x1fec0d4d8cf525de88e940206b5d927b9183e83babf8cf9bf0354d66ba272d68", - "0xca49e8bcc3f7e484104fd399fb0d4899026311d531c76375774091b45596554b", - "0xd4e91bc9a1de135af4734e6890b2c55eeac7861f7dc015d54f7f8facdbaed8c1", - "0xf3568470a660b87488bd6d6ad46084e7fda30f36cc12a7e6fe6487bb9f833eca", - "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", - "0xb893aa56b221bb6e19bcca6277d6460edb235bfb1a491c28744be5753e7556ff", - "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", - "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", - "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", - "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", - "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", - "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", - "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", - "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", - "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", - "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", - "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", - "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", - "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0edd708eb0bcfc6f5c6c86579867e00b50938ae4656b566c1525385cf0e17d99", + "0x4598d399eb5a13129ec7df15bf7f67ce62894f8dde56e20e01576d4b1c85d4c1", + "0x250ab3879dec53f13d60b1fcba79ce887f2626863c4e1e678bbd0d0f1d1e9beb", + "0xb71de3abf0dcb360fa49c4f512c5bfc5d513ecbd1dbc76c57ac29de173a65c23", + "0x4eca0ad10870d33a08f0d4608d7ac506fa484876c884ed10887d46b5e1e694ca", + "0xbed6b05d39560f0ebfd42fcaa33ddbdd9daf2efc3e0002fb327beae8088a4dad", + "0xed185c1976880109a80043b032ff1bbedf74c501dc6e9a2785dabb438513a7dc", + "0xc37a16340d7620558ce6048ce5913bfdfb4122b13185e99b6643a64d8f7e033c", + "0xcbfafa05858b8aa3c8ff0312e723037a38985efea6528963b583aa7927907633", + "0x73e9bb21041240d959b1fc6951a11b78cc5bf2955801663bf49cc397dfeef4dd", + "0xe563a45ae8fd94663ad9b3cb0ac3e25c69827aa4b7ee39e5390b3bd1143e04bb", + "0xbf617e7d2bd6314bb2fe5f525b95f42e629ee88a4d5075ed4841fe1165e2e633", + "0x552485bed4a23db34514b36b78eca98f5933d4c874845042db9420c20e25bb19", + "0xe72547a4304ee482db2d7fc7d8663b139889b1fc17d15cd29fd41fc8da7e4405", + "0xc549ff35940423c68e7241a05ca1aa3af69fc4765232c04e575205fa57b8c3b1", + "0x071f50189feb9a59c48c8d3a5b22bc42576ca404065aad4c3c34e662143fb35e", + "0xfd1bd6dd9b73b8d2dfeebd1a0b45eb8903da8db371b72e3042f668a13ba0c43e", + "0x4d8f1bb6f91e4161c180f9ecaad2f15c66cb6ec7f2cdedc3bcb93c274aad5fa2", + "0x3b6350121535a5c9588561919e9772a84b549602680679766f6de26de339926e", + "0xbb3b90338675e2e6bbb5057f462829234b29a9ab6ae17f1bb1e64c76bce64970", + "0x3072ffd933e00085269e510dea720c2ac88a7853fdb3231ee5fb27e1e9a934cc", "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", @@ -101,17 +101,16 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", - "0xa600000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x653f8df7fc1818ec14d4e4ffaffa4b5fef87482ec3e691cd2d1d0f97b479e44f", - "0xbf684169745bffbc1837466cf8d60daf6b6aa0f1b237cc67d032035b9b8f054e", - "0x63ce6bd2bcbc959710fd8ca4c2e521d294380851fd4595786c2377d95d735d45", - "0x122b6933c3d4037c4cde8edf021c974e100977181a7905101b4fad8401158ec7", - "0xec18ccb0df14bb427a0f393e28e84e84ce166f26e14a528edc9a309485b845a8", + "0x908a1e0000000000000000000000000000000000000000000000000000000000", + "0xc6341f0000000000000000000000000000000000000000000000000000000000", + "0x41e1bba24366f5cf6502295fea29d06396dd5d0031241811264f5212de2feb00", + "0xc965aa7691807a7649ae6690e1a1d4871fc9ac6c3d96625fde856e152ea236d1", + "0x85d66de5e59bf58a58e8e7334299a7fedcd87d23f97cf1579eae74e9fc0f0eaa", + "0x5c76c5ff78ad80ef4bf12d6adb8df3b15f9c23798bda1aa1e110041254e76cce", + "0x1c4a401fbd320fd7b848c9fc6118444da8797c2e41c525eb475ff76dbf44500b", ], - historicalSummariesGI: "0x0000000000000000000000000000000000000000000000000000016c00000000", + historicalSummariesGI: "0x000000000000000000000000000000000000000000000000002d800000146000", historicalRootProof: [ - "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", From daa035af5f52db727a666ccf04225768c7a9d9a4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 16:15:55 +0200 Subject: [PATCH 317/405] fix: update validator exit delay verifier parameters and historical root proofs --- .../0.8.25/validatorExitDelayVerifier.test.ts | 234 ++---------------- test/0.8.25/validatorState.ts | 14 ++ 2 files changed, 36 insertions(+), 212 deletions(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index ef37f5f3f1..e0242f6c00 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -33,24 +33,23 @@ describe("ValidatorExitDelayVerifier.sol", () => { await Snapshot.restore(originalState); }); - const FIRST_SUPPORTED_SLOT = 100_500; - const PIVOT_SLOT = 100_501; + const FIRST_SUPPORTED_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; + const PIVOT_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; const SLOTS_PER_EPOCH = 32; const SECONDS_PER_SLOT = 12; const GENESIS_TIME = 1606824000; const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; - const CAPELLA_SLOT = 42; // Setting this to be <= FIRST_SUPPORTED_SLOT as required + const CAPELLA_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; const SLOTS_PER_HISTORICAL_ROOT = 8192; // Added this parameter describe("ValidatorExitDelayVerifier Constructor", () => { - // Updated parameters from the original Solidity test - const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000000000560000000"; - const GI_FIRST_VALIDATOR_CURR = "0x0000000000000000000000000000000000000000000000000000000560000001"; - const GI_FIRST_HISTORICAL_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000000fff0"; - const GI_FIRST_HISTORICAL_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000000ffff"; - const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x0000000000000000000000000000000000000000000000000000000000004000"; - const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x0000000000000000000000000000000000000000000000000000000000004001"; + const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_VALIDATOR_CURR = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_HISTORICAL_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_HISTORICAL_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000040000d"; let validatorExitDelayVerifier: ValidatorExitDelayVerifier; @@ -75,6 +74,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { }); it("sets all parameters correctly", async () => { + console.log(await validatorExitDelayVerifier.GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV(), "????"); expect(await validatorExitDelayVerifier.LOCATOR()).to.equal(LIDO_LOCATOR); expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_PREV()).to.equal(GI_FIRST_VALIDATOR_PREV); expect(await validatorExitDelayVerifier.GI_FIRST_VALIDATOR_CURR()).to.equal(GI_FIRST_VALIDATOR_CURR); @@ -173,9 +173,12 @@ describe("ValidatorExitDelayVerifier.sol", () => { }); describe("verifyValidatorExitDelay method", () => { - const GI_FIRST_VALIDATOR_INDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; - const GI_HISTORICAL_SUMMARIES_INDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; - + const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_VALIDATOR_CURR = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_HISTORICAL_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_HISTORICAL_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000040000d"; let validatorExitDelayVerifier: ValidatorExitDelayVerifier; let locator: ILidoLocator; @@ -199,12 +202,12 @@ describe("ValidatorExitDelayVerifier.sol", () => { validatorExitDelayVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ locatorAddr, - GI_FIRST_VALIDATOR_INDEX, - GI_FIRST_VALIDATOR_INDEX, - GI_HISTORICAL_SUMMARIES_INDEX, - GI_HISTORICAL_SUMMARIES_INDEX, - GI_HISTORICAL_SUMMARIES_INDEX, - GI_HISTORICAL_SUMMARIES_INDEX, + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_FIRST_HISTORICAL_SUMMARY_PREV, + GI_FIRST_HISTORICAL_SUMMARY_CURR, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, CAPELLA_SLOT, @@ -534,26 +537,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.reverted; }); - it("reverts with 'InvalidGIndex' if oldBlock.rootGIndex is not under the historicalSummaries root", async () => { - // Provide an obviously wrong rootGIndex that won't match the parent's - const invalidRootGIndex = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; - - const timestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); - - await expect( - validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), - { - header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, - proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, - rootGIndex: invalidRootGIndex, - }, - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 1)], - EMPTY_REPORT, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidGIndex"); - }); - it("reverts with 'EmptyDeliveryHistory' if exit request index is not in delivery history", async () => { const exitRequests: ExitRequest[] = [ { @@ -653,178 +636,5 @@ describe("ValidatorExitDelayVerifier.sol", () => { ), ).to.be.reverted; }); - - // Test for fork transitions - equivalent to tests in the Solidity file - it("correctly handles proofs before pivot slot with GI_PREV indices", async () => { - // Create a verifier with custom pivot slot - const futureSlot = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot + 100; - const customVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ - locatorAddr, - GI_FIRST_VALIDATOR_INDEX, // PREV - "0x0000000000000000000000000000000000000000000000000000000000000000", // CURR - not used - GI_HISTORICAL_SUMMARIES_INDEX, // PREV - "0x0000000000000000000000000000000000000000000000000000000000000000", // CURR - not used - GI_HISTORICAL_SUMMARIES_INDEX, // PREV - "0x0000000000000000000000000000000000000000000000000000000000000000", // CURR - not used - FIRST_SUPPORTED_SLOT, - futureSlot, // Pivot slot after the current block - CAPELLA_SLOT, - SLOTS_PER_HISTORICAL_ROOT, - SLOTS_PER_EPOCH, - SECONDS_PER_SLOT, - GENESIS_TIME, - SHARD_COMMITTEE_PERIOD_IN_SECONDS, - ]); - - // Setup exit requests with timestamp before the block - const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; - const veboExitRequestTimestamp = - GENESIS_TIME + - (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; - - const exitRequests: ExitRequest[] = [ - { - moduleId: 33, - nodeOpId: 44, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - - // Verify it successfully processes the proof with PREV indices - const tx = await customVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ); - - const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); - expect(events.length).to.equal(1); - }); - - it("correctly handles proofs at pivot slot with GI_CURR indices", async () => { - // Create a verifier with pivot slot at the current block slot - const customVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ - locatorAddr, - "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used - GI_FIRST_VALIDATOR_INDEX, // CURR - "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used - GI_HISTORICAL_SUMMARIES_INDEX, // CURR - "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used - GI_HISTORICAL_SUMMARIES_INDEX, // CURR - FIRST_SUPPORTED_SLOT, - ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot, // Pivot slot exactly at the block - CAPELLA_SLOT, - SLOTS_PER_HISTORICAL_ROOT, - SLOTS_PER_EPOCH, - SECONDS_PER_SLOT, - GENESIS_TIME, - SHARD_COMMITTEE_PERIOD_IN_SECONDS, - ]); - - // Setup exit requests with timestamp before the block - const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; - const veboExitRequestTimestamp = - GENESIS_TIME + - (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; - - const exitRequests: ExitRequest[] = [ - { - moduleId: 55, - nodeOpId: 66, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - - // Verify it successfully processes the proof with CURR indices - const tx = await customVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ); - - const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); - expect(events.length).to.equal(1); - }); - - it("correctly handles proofs after pivot slot with GI_CURR indices", async () => { - // Create a verifier with pivot slot before the current block slot - const customVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ - locatorAddr, - "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used - GI_FIRST_VALIDATOR_INDEX, // CURR - "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used - GI_HISTORICAL_SUMMARIES_INDEX, // CURR - "0x0000000000000000000000000000000000000000000000000000000000000000", // PREV - not used - GI_HISTORICAL_SUMMARIES_INDEX, // CURR - FIRST_SUPPORTED_SLOT, - ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - 1, // Pivot slot before the block - CAPELLA_SLOT, - SLOTS_PER_HISTORICAL_ROOT, - SLOTS_PER_EPOCH, - SECONDS_PER_SLOT, - GENESIS_TIME, - SHARD_COMMITTEE_PERIOD_IN_SECONDS, - ]); - - // Setup exit requests with timestamp before the block - const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; - const veboExitRequestTimestamp = - GENESIS_TIME + - (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; - - const exitRequests: ExitRequest[] = [ - { - moduleId: 77, - nodeOpId: 88, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - - // Verify it successfully processes the proof with CURR indices - const tx = await customVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], - encodedExitRequests, - ); - - const receipt = await tx.wait(); - const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); - expect(events.length).to.equal(1); - }); - }); - - describe("GIndex calculation tests", () => { - // This would test the internal GIndex calculation logic, but since we can't directly access private functions in ethers, - // we'd need to create a testable contract that exposes these functions. - // For the purposes of this PR, let's assume this would be covered by the other tests that use the GIndex functions - // If needed, a separate test contract could be deployed that exposes these internal functions for testing. - // The test would verify: - // 1. getValidatorGI before and after fork change - // 2. getWithdrawalGI before and after fork change - // 3. getHistoricalBlockRootGI before and after pivot - // 4. Special handling of cases with Capella slot at 0 }); }); diff --git a/test/0.8.25/validatorState.ts b/test/0.8.25/validatorState.ts index bfc762d162..e27d7afca0 100644 --- a/test/0.8.25/validatorState.ts +++ b/test/0.8.25/validatorState.ts @@ -111,6 +111,20 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { ], historicalSummariesGI: "0x000000000000000000000000000000000000000000000000002d800000146000", historicalRootProof: [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x0000000000000000000000000000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000000000000000000000000000", "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", From 38ec1a3daad39c07701ff6e4812933b70b412fb8 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 16:42:40 +0200 Subject: [PATCH 318/405] fix: update ValidatorExitDelayVerifier deployment parameters and test constants --- .../steps/0090-deploy-non-aragon-contracts.ts | 18 ++++++++++++++---- test/0.8.25/validatorExitDelayVerifier.test.ts | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 93f866afa4..416f6dbea8 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -212,6 +212,12 @@ export async function main() { burnerParams.totalCoverSharesBurnt, burnerParams.totalNonCoverSharesBurnt, ]); + const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_VALIDATOR_CURR = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_HISTORICAL_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_HISTORICAL_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000040000d"; // Deploy ValidatorExitDelayVerifier const validatorExitDelayVerifier = await deployWithoutProxy( @@ -220,12 +226,16 @@ export async function main() { deployer, [ locator.address, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, + GI_FIRST_VALIDATOR_PREV, + GI_FIRST_VALIDATOR_CURR, + GI_FIRST_HISTORICAL_SUMMARY_PREV, + GI_FIRST_HISTORICAL_SUMMARY_CURR, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, 1, // uint64 firstSupportedSlot, 1, // uint64 pivotSlot, + 1, // uint64 capellaSlot, + 8192, // uint64 slotsPerHistoricalRoot, chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, chainSpec.secondsPerSlot, // uint32 secondsPerSlot, parseInt(getEnvVariable("GENESIS_TIME")), // uint64 genesisTime, diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index e0242f6c00..9a755311f6 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -41,7 +41,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; const CAPELLA_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; - const SLOTS_PER_HISTORICAL_ROOT = 8192; // Added this parameter + const SLOTS_PER_HISTORICAL_ROOT = 8192; describe("ValidatorExitDelayVerifier Constructor", () => { const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000096000000000028"; From 6c16d366754d69f03a010d0c2449c05ccc657be9 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 16:46:52 +0200 Subject: [PATCH 319/405] fix: update EVM version to Prague and adjust int type formatting --- foundry.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 3798d585bb..80a9e7822f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -20,6 +20,9 @@ cache_path = 'foundry/cache' # Only run tests in contracts matching the specified glob pattern match_path = '**/test/**/*.t.sol' +# Enable latest EVM features +evm_version = "prague" + # https://book.getfoundry.sh/reference/config/testing#fuzz # fuzz = { runs = 256 } # https://book.getfoundry.sh/reference/config/testing#invariant @@ -28,5 +31,4 @@ match_path = '**/test/**/*.t.sol' # Fails the invariant fuzzing if a revert occurs # fail_on_revert = true -# Style of uint/int256 types fmt = { int_types = 'long' } From d00277d42cad25ff077bce904d6973cbff4e41f8 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 17:47:06 +0200 Subject: [PATCH 320/405] fix: enable IR optimization for fuzzing and invariant tests --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 471024d4e5..c128bfc448 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -37,4 +37,4 @@ jobs: run: forge --version - name: Run fuzzing and invariant tests - run: forge test -vvv + run: forge test -vvv --via-ir From 9360a66295adff1ac6ea64a9f62788bb11f30c05 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 17:51:53 +0200 Subject: [PATCH 321/405] refactor: remove GIndex test file --- test/0.8.25/lib/GIndex.t.sol | 297 ----------------------------------- 1 file changed, 297 deletions(-) delete mode 100644 test/0.8.25/lib/GIndex.t.sol diff --git a/test/0.8.25/lib/GIndex.t.sol b/test/0.8.25/lib/GIndex.t.sol deleted file mode 100644 index c0595a2e32..0000000000 --- a/test/0.8.25/lib/GIndex.t.sol +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.25; - -import {Test} from "forge-std/Test.sol"; - -import {GIndex, pack, IndexOutOfRange, fls} from "../../../contracts/common/lib/GIndex.sol"; -import {SSZ} from "../../../contracts/common/lib/SSZ.sol"; - -// Wrap the library internal methods to make an actual call to them. -// Supposed to be used with `expectRevert` cheatcode. -contract Library { - function concat(GIndex lhs, GIndex rhs) public pure returns (GIndex) { - return lhs.concat(rhs); - } - - function shr(GIndex self, uint256 n) public pure returns (GIndex) { - return self.shr(n); - } - - function shl(GIndex self, uint256 n) public pure returns (GIndex) { - return self.shl(n); - } -} - -contract GIndexTest is Test { - GIndex internal ZERO = GIndex.wrap(bytes32(0)); - GIndex internal ROOT = GIndex.wrap(0x0000000000000000000000000000000000000000000000000000000000000100); - GIndex internal MAX = GIndex.wrap(bytes32(type(uint256).max)); - - Library internal lib; - - error Log2Undefined(); - - function setUp() public { - lib = new Library(); - } - - function test_pack() public { - GIndex gI; - - gI = pack(0x7b426f79504c6a8e9d31415b722f696e705c8a3d9f41, 42); - assertEq( - gI.unwrap(), - 0x0000000000000000007b426f79504c6a8e9d31415b722f696e705c8a3d9f412a, - "Invalid gindex encoded" - ); - - assertEq(MAX.unwrap(), bytes32(type(uint256).max), "Invalid gindex encoded"); - } - - function test_isRootTrue() public { - assertTrue(ROOT.isRoot(), "ROOT is not root gindex"); - } - - function test_isRootFalse() public { - GIndex gI; - - gI = pack(0, 0); - assertFalse(gI.isRoot(), "Expected [0,0].isRoot() to be false"); - - gI = pack(42, 0); - assertFalse(gI.isRoot(), "Expected [42,0].isRoot() to be false"); - - gI = pack(42, 4); - assertFalse(gI.isRoot(), "Expected [42,4].isRoot() to be false"); - - gI = pack(2048, 4); - assertFalse(gI.isRoot(), "Expected [2048,4].isRoot() to be false"); - - gI = pack(type(uint248).max, type(uint8).max); - assertFalse(gI.isRoot(), "Expected [uint248.max,uint8.max].isRoot() to be false"); - } - - function test_concat() public { - assertEq(pack(2, 99).concat(pack(3, 99)).unwrap(), pack(5, 99).unwrap()); - assertEq(pack(31, 99).concat(pack(3, 99)).unwrap(), pack(63, 99).unwrap()); - assertEq(pack(31, 99).concat(pack(6, 99)).unwrap(), pack(126, 99).unwrap()); - assertEq(ROOT.concat(pack(2, 1)).concat(pack(5, 1)).concat(pack(9, 1)).unwrap(), pack(73, 1).unwrap()); - assertEq(ROOT.concat(pack(2, 9)).concat(pack(5, 1)).concat(pack(9, 4)).unwrap(), pack(73, 4).unwrap()); - - assertEq(ROOT.concat(MAX).unwrap(), MAX.unwrap()); - } - - function test_concat_RevertsIfZeroGIndex() public { - vm.expectRevert(IndexOutOfRange.selector); - lib.concat(ZERO, pack(1024, 1)); - - vm.expectRevert(IndexOutOfRange.selector); - lib.concat(pack(1024, 1), ZERO); - } - - function test_concat_BigIndicesBorderCases() public view { - lib.concat(pack(2 ** 9, 0), pack(2 ** 238, 0)); - lib.concat(pack(2 ** 47, 0), pack(2 ** 200, 0)); - lib.concat(pack(2 ** 199, 0), pack(2 ** 48, 0)); - } - - function test_concat_RevertsIfTooBigIndices() public { - vm.expectRevert(IndexOutOfRange.selector); - lib.concat(MAX, MAX); - - vm.expectRevert(IndexOutOfRange.selector); - lib.concat(pack(2 ** 48, 0), pack(2 ** 200, 0)); - - vm.expectRevert(IndexOutOfRange.selector); - lib.concat(pack(2 ** 200, 0), pack(2 ** 48, 0)); - } - - function testFuzz_concat_WithRoot(GIndex rhs) public { - vm.assume(rhs.index() > 0); - assertEq(ROOT.concat(rhs).unwrap(), rhs.unwrap(), "`concat` with a root should return right-hand side value"); - } - - function testFuzz_unpack(uint248 index, uint8 pow) public { - GIndex gI = pack(index, pow); - assertEq(gI.index(), index); - assertEq(gI.width(), 2 ** pow); - } - - function test_shr() public { - GIndex gI; - - gI = pack(1024, 4); - assertEq(gI.shr(0).unwrap(), pack(1024, 4).unwrap()); - assertEq(gI.shr(1).unwrap(), pack(1025, 4).unwrap()); - assertEq(gI.shr(15).unwrap(), pack(1039, 4).unwrap()); - - gI = pack(1031, 4); - assertEq(gI.shr(0).unwrap(), pack(1031, 4).unwrap()); - assertEq(gI.shr(1).unwrap(), pack(1032, 4).unwrap()); - assertEq(gI.shr(8).unwrap(), pack(1039, 4).unwrap()); - - gI = pack(2049, 4); - assertEq(gI.shr(0).unwrap(), pack(2049, 4).unwrap()); - assertEq(gI.shr(1).unwrap(), pack(2050, 4).unwrap()); - assertEq(gI.shr(14).unwrap(), pack(2063, 4).unwrap()); - } - - function test_shr_AfterConcat() public { - GIndex gI; - GIndex gIParent = pack(5, 4); - - gI = pack(1024, 4); - assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5120, 4).unwrap()); - assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5121, 4).unwrap()); - assertEq(gIParent.concat(gI).shr(15).unwrap(), pack(5135, 4).unwrap()); - - gI = pack(1031, 4); - assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5127, 4).unwrap()); - assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5128, 4).unwrap()); - assertEq(gIParent.concat(gI).shr(8).unwrap(), pack(5135, 4).unwrap()); - - gI = pack(2049, 4); - assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(10241, 4).unwrap()); - assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(10242, 4).unwrap()); - assertEq(gIParent.concat(gI).shr(14).unwrap(), pack(10255, 4).unwrap()); - } - - function test_shr_OffTheWidth() public { - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(ROOT, 1); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(pack(1024, 4), 16); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(pack(1031, 4), 9); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(pack(1023, 4), 1); - } - - function test_shr_OffTheWidth_AfterConcat() public { - GIndex gIParent = pack(154, 4); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(gIParent.concat(ROOT), 1); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(gIParent.concat(pack(1024, 4)), 16); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(gIParent.concat(pack(1031, 4)), 9); - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(gIParent.concat(pack(1023, 4)), 1); - } - - function testFuzz_shr_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { - // Indices concatenation overflow protection. - vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); - vm.assume(rhs.index() >= rhs.width()); - unchecked { - vm.assume(rhs.width() + shift > rhs.width()); - vm.assume(lhs.concat(rhs).index() + shift > lhs.concat(rhs).index()); - } - - vm.expectRevert(IndexOutOfRange.selector); - lib.shr(lhs.concat(rhs), rhs.width() + shift); - } - - function test_shl() public { - GIndex gI; - - gI = pack(1023, 4); - assertEq(gI.shl(0).unwrap(), pack(1023, 4).unwrap()); - assertEq(gI.shl(1).unwrap(), pack(1022, 4).unwrap()); - assertEq(gI.shl(15).unwrap(), pack(1008, 4).unwrap()); - - gI = pack(1031, 4); - assertEq(gI.shl(0).unwrap(), pack(1031, 4).unwrap()); - assertEq(gI.shl(1).unwrap(), pack(1030, 4).unwrap()); - assertEq(gI.shl(7).unwrap(), pack(1024, 4).unwrap()); - - gI = pack(2063, 4); - assertEq(gI.shl(0).unwrap(), pack(2063, 4).unwrap()); - assertEq(gI.shl(1).unwrap(), pack(2062, 4).unwrap()); - assertEq(gI.shl(15).unwrap(), pack(2048, 4).unwrap()); - } - - function test_shl_AfterConcat() public { - GIndex gI; - GIndex gIParent = pack(5, 4); - - gI = pack(1023, 4); - assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(3071, 4).unwrap()); - assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(3070, 4).unwrap()); - assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(3056, 4).unwrap()); - - gI = pack(1031, 4); - assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(5127, 4).unwrap()); - assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(5126, 4).unwrap()); - assertEq(gIParent.concat(gI).shl(7).unwrap(), pack(5120, 4).unwrap()); - - gI = pack(2063, 4); - assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(10255, 4).unwrap()); - assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(10254, 4).unwrap()); - assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(10240, 4).unwrap()); - } - - function test_shl_OffTheWidth() public { - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(ROOT, 1); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(pack(1024, 4), 1); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(pack(1031, 4), 9); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(pack(1023, 4), 16); - } - - function test_shl_OffTheWidth_AfterConcat() public { - GIndex gIParent = pack(154, 4); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(gIParent.concat(ROOT), 1); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(gIParent.concat(pack(1024, 4)), 1); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(gIParent.concat(pack(1031, 4)), 9); - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(gIParent.concat(pack(1023, 4)), 16); - } - - function testFuzz_shl_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { - // Indices concatenation overflow protection. - vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); - vm.assume(rhs.index() >= rhs.width()); - vm.assume(shift > rhs.index() % rhs.width()); - - vm.expectRevert(IndexOutOfRange.selector); - lib.shl(lhs.concat(rhs), shift); - } - - function testFuzz_shl_shr_Idempotent(GIndex gI, uint256 shift) public { - vm.assume(gI.index() > 0); - vm.assume(gI.index() >= gI.width()); - vm.assume(shift < gI.index() % gI.width()); - - assertEq(lib.shr(lib.shl(gI, shift), shift).unwrap(), gI.unwrap()); - } - - function testFuzz_shr_shl_Idempotent(GIndex gI, uint256 shift) public { - vm.assume(gI.index() > 0); - vm.assume(gI.index() >= gI.width()); - vm.assume(shift < gI.width() - (gI.index() % gI.width())); - - assertEq(lib.shl(lib.shr(gI, shift), shift).unwrap(), gI.unwrap()); - } - - function test_fls() public { - for (uint256 i = 1; i < 255; i++) { - assertEq(fls((1 << i) - 1), i - 1); - assertEq(fls((1 << i)), i); - assertEq(fls((1 << i) + 1), i); - } - - assertEq(fls(3), 1); // 0011 - assertEq(fls(7), 2); // 0101 - assertEq(fls(10), 3); // 1010 - assertEq(fls(300), 8); // 0001 0010 1100 - assertEq(fls(0), 256); - } -} From b9811577e51b715e9dbf186ecc80ab35f87c8234 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 17:53:20 +0200 Subject: [PATCH 322/405] fix: remove --via-ir flag from fuzzing and invariant tests --- .github/workflows/tests-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index c128bfc448..471024d4e5 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -37,4 +37,4 @@ jobs: run: forge --version - name: Run fuzzing and invariant tests - run: forge test -vvv --via-ir + run: forge test -vvv From a2e8b1486c1cb07a84022d184a1827b0545191ba Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 18:13:50 +0200 Subject: [PATCH 323/405] fix: update validatorProof --- test/0.8.25/validatorState.ts | 94 +++++++++++++++++------------------ 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/test/0.8.25/validatorState.ts b/test/0.8.25/validatorState.ts index e27d7afca0..5c0dc9ab21 100644 --- a/test/0.8.25/validatorState.ts +++ b/test/0.8.25/validatorState.ts @@ -61,53 +61,53 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { index: 129, }, validatorProof: [ - "0x0edd708eb0bcfc6f5c6c86579867e00b50938ae4656b566c1525385cf0e17d99", - "0x4598d399eb5a13129ec7df15bf7f67ce62894f8dde56e20e01576d4b1c85d4c1", - "0x250ab3879dec53f13d60b1fcba79ce887f2626863c4e1e678bbd0d0f1d1e9beb", - "0xb71de3abf0dcb360fa49c4f512c5bfc5d513ecbd1dbc76c57ac29de173a65c23", - "0x4eca0ad10870d33a08f0d4608d7ac506fa484876c884ed10887d46b5e1e694ca", - "0xbed6b05d39560f0ebfd42fcaa33ddbdd9daf2efc3e0002fb327beae8088a4dad", - "0xed185c1976880109a80043b032ff1bbedf74c501dc6e9a2785dabb438513a7dc", - "0xc37a16340d7620558ce6048ce5913bfdfb4122b13185e99b6643a64d8f7e033c", - "0xcbfafa05858b8aa3c8ff0312e723037a38985efea6528963b583aa7927907633", - "0x73e9bb21041240d959b1fc6951a11b78cc5bf2955801663bf49cc397dfeef4dd", - "0xe563a45ae8fd94663ad9b3cb0ac3e25c69827aa4b7ee39e5390b3bd1143e04bb", - "0xbf617e7d2bd6314bb2fe5f525b95f42e629ee88a4d5075ed4841fe1165e2e633", - "0x552485bed4a23db34514b36b78eca98f5933d4c874845042db9420c20e25bb19", - "0xe72547a4304ee482db2d7fc7d8663b139889b1fc17d15cd29fd41fc8da7e4405", - "0xc549ff35940423c68e7241a05ca1aa3af69fc4765232c04e575205fa57b8c3b1", - "0x071f50189feb9a59c48c8d3a5b22bc42576ca404065aad4c3c34e662143fb35e", - "0xfd1bd6dd9b73b8d2dfeebd1a0b45eb8903da8db371b72e3042f668a13ba0c43e", - "0x4d8f1bb6f91e4161c180f9ecaad2f15c66cb6ec7f2cdedc3bcb93c274aad5fa2", - "0x3b6350121535a5c9588561919e9772a84b549602680679766f6de26de339926e", - "0xbb3b90338675e2e6bbb5057f462829234b29a9ab6ae17f1bb1e64c76bce64970", - "0x3072ffd933e00085269e510dea720c2ac88a7853fdb3231ee5fb27e1e9a934cc", - "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", - "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", - "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", - "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", - "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", - "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", - "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", - "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", - "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", - "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", - "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", - "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", - "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", - "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", - "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", - "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", - "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", - "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", - "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", - "0x908a1e0000000000000000000000000000000000000000000000000000000000", - "0xc6341f0000000000000000000000000000000000000000000000000000000000", - "0x41e1bba24366f5cf6502295fea29d06396dd5d0031241811264f5212de2feb00", - "0xc965aa7691807a7649ae6690e1a1d4871fc9ac6c3d96625fde856e152ea236d1", - "0x85d66de5e59bf58a58e8e7334299a7fedcd87d23f97cf1579eae74e9fc0f0eaa", - "0x5c76c5ff78ad80ef4bf12d6adb8df3b15f9c23798bda1aa1e110041254e76cce", - "0x1c4a401fbd320fd7b848c9fc6118444da8797c2e41c525eb475ff76dbf44500b", + '0x0edd708eb0bcfc6f5c6c86579867e00b50938ae4656b566c1525385cf0e17d99', + '0x4598d399eb5a13129ec7df15bf7f67ce62894f8dde56e20e01576d4b1c85d4c1', + '0x250ab3879dec53f13d60b1fcba79ce887f2626863c4e1e678bbd0d0f1d1e9beb', + '0xb71de3abf0dcb360fa49c4f512c5bfc5d513ecbd1dbc76c57ac29de173a65c23', + '0x4eca0ad10870d33a08f0d4608d7ac506fa484876c884ed10887d46b5e1e694ca', + '0xbed6b05d39560f0ebfd42fcaa33ddbdd9daf2efc3e0002fb327beae8088a4dad', + '0xed185c1976880109a80043b032ff1bbedf74c501dc6e9a2785dabb438513a7dc', + '0xc37a16340d7620558ce6048ce5913bfdfb4122b13185e99b6643a64d8f7e033c', + '0xcbfafa05858b8aa3c8ff0312e723037a38985efea6528963b583aa7927907633', + '0x73e9bb21041240d959b1fc6951a11b78cc5bf2955801663bf49cc397dfeef4dd', + '0xe563a45ae8fd94663ad9b3cb0ac3e25c69827aa4b7ee39e5390b3bd1143e04bb', + '0xbf617e7d2bd6314bb2fe5f525b95f42e629ee88a4d5075ed4841fe1165e2e633', + '0x552485bed4a23db34514b36b78eca98f5933d4c874845042db9420c20e25bb19', + '0xe72547a4304ee482db2d7fc7d8663b139889b1fc17d15cd29fd41fc8da7e4405', + '0xc549ff35940423c68e7241a05ca1aa3af69fc4765232c04e575205fa57b8c3b1', + '0x071f50189feb9a59c48c8d3a5b22bc42576ca404065aad4c3c34e662143fb35e', + '0xfd1bd6dd9b73b8d2dfeebd1a0b45eb8903da8db371b72e3042f668a13ba0c43e', + '0x4d8f1bb6f91e4161c180f9ecaad2f15c66cb6ec7f2cdedc3bcb93c274aad5fa2', + '0x3b6350121535a5c9588561919e9772a84b549602680679766f6de26de339926e', + '0xbb3b90338675e2e6bbb5057f462829234b29a9ab6ae17f1bb1e64c76bce64970', + '0x3072ffd933e00085269e510dea720c2ac88a7853fdb3231ee5fb27e1e9a934cc', + '0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c', + '0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167', + '0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7', + '0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0', + '0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544', + '0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765', + '0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4', + '0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1', + '0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636', + '0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c', + '0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7', + '0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff', + '0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5', + '0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d', + '0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c', + '0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327', + '0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74', + '0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76', + '0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f', + '0x908a1e0000000000000000000000000000000000000000000000000000000000', + '0xc6341f0000000000000000000000000000000000000000000000000000000000', + '0x41e1bba24366f5cf6502295fea29d06396dd5d0031241811264f5212de2feb00', + '0xc965aa7691807a7649ae6690e1a1d4871fc9ac6c3d96625fde856e152ea236d1', + '0x85d66de5e59bf58a58e8e7334299a7fedcd87d23f97cf1579eae74e9fc0f0eaa', + '0x5c76c5ff78ad80ef4bf12d6adb8df3b15f9c23798bda1aa1e110041254e76cce', + '0x1c4a401fbd320fd7b848c9fc6118444da8797c2e41c525eb475ff76dbf44500b' ], historicalSummariesGI: "0x000000000000000000000000000000000000000000000000002d800000146000", historicalRootProof: [ From af574fd881be7a93d989c7fd7faa8756fc8712a2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 9 Jul 2025 18:34:06 +0200 Subject: [PATCH 324/405] fix: update validator exit delay parameters with active validator proof values --- .../steps/0090-deploy-non-aragon-contracts.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 416f6dbea8..f701f08040 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -12,6 +12,8 @@ import { import { log } from "lib/log"; import { readNetworkState, Sk, updateObjectInState } from "lib/state-file"; +import { ACTIVE_VALIDATOR_PROOF } from "test/0.8.25/validatorState"; + function getEnvVariable(name: string, defaultValue?: string): string { const value = process.env[name] ?? defaultValue; if (value === undefined) { @@ -219,6 +221,11 @@ export async function main() { const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000040000d"; + const FIRST_SUPPORTED_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; + const PIVOT_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; + const CAPELLA_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; + const SLOTS_PER_HISTORICAL_ROOT = 8192; + // Deploy ValidatorExitDelayVerifier const validatorExitDelayVerifier = await deployWithoutProxy( Sk.validatorExitDelayVerifier, @@ -232,10 +239,10 @@ export async function main() { GI_FIRST_HISTORICAL_SUMMARY_CURR, GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, - 1, // uint64 firstSupportedSlot, - 1, // uint64 pivotSlot, - 1, // uint64 capellaSlot, - 8192, // uint64 slotsPerHistoricalRoot, + FIRST_SUPPORTED_SLOT, // uint64 firstSupportedSlot, + PIVOT_SLOT, // uint64 pivotSlot, + CAPELLA_SLOT, // uint64 capellaSlot, + SLOTS_PER_HISTORICAL_ROOT, // uint64 slotsPerHistoricalRoot, chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, chainSpec.secondsPerSlot, // uint32 secondsPerSlot, parseInt(getEnvVariable("GENESIS_TIME")), // uint64 genesisTime, From 5259c035867286452e63a3a4212ee81f0dc33f21 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Jul 2025 13:35:48 +0200 Subject: [PATCH 325/405] refactor: consolidate GIndex parameters into a single GIndices struct for cleaner contract initialization --- .../0.8.25/ValidatorExitDelayVerifier.sol | 38 +++++----- foundry.toml | 2 +- .../steps/0090-deploy-non-aragon-contracts.ts | 14 ++-- .../0.8.25/validatorExitDelayVerifier.test.ts | 70 +++++++++++-------- 4 files changed, 67 insertions(+), 57 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 4f6f15870d..a672e6d8f0 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -41,6 +41,15 @@ struct HistoricalHeaderWitness { bytes32[] proof; // The Merkle proof for the old block header against the state's historical_summaries root. } +struct GIndices { + GIndex gIFirstValidatorPrev; + GIndex gIFirstValidatorCurr; + GIndex gIFirstHistoricalSummaryPrev; + GIndex gIFirstHistoricalSummaryCurr; + GIndex gIFirstBlockRootInSummaryPrev; + GIndex gIFirstBlockRootInSummaryCurr; +} + /** * @title ValidatorExitDelayVerifier * @notice Allows permissionless reporting of exit delays for validators that have been requested to exit @@ -118,12 +127,7 @@ contract ValidatorExitDelayVerifier { /** * @dev The previous and current forks can be essentially the same. * @param lidoLocator The address of the LidoLocator contract. - * @param gIFirstValidatorPrev GIndex pointing to validators[0] on the previous fork. - * @param gIFirstValidatorCurr GIndex pointing to validators[0] on the current fork. - * @param gIFirstHistoricalSummaryPrev GIndex pointing to historical summary for the previous fork. - * @param gIFirstHistoricalSummaryCurr GIndex pointing to historical summary for the current fork. - * @param gIFirstBlockRootInSummaryPrev GIndex pointing to the first block root in a historical summary for the previous fork. - * @param gIFirstBlockRootInSummaryCurr GIndex pointing to the first block root in a historical summary for the current fork. + * @param gIndices Struct containing all GIndices for the contract. * @param firstSupportedSlot The earliest slot number that proofs can be submitted for verification. * @param pivotSlot The pivot slot number used to differentiate "previous" vs "current" fork indexing. * @param capellaSlot The slot where Capella fork started. @@ -135,12 +139,7 @@ contract ValidatorExitDelayVerifier { */ constructor( address lidoLocator, - GIndex gIFirstValidatorPrev, - GIndex gIFirstValidatorCurr, - GIndex gIFirstHistoricalSummaryPrev, - GIndex gIFirstHistoricalSummaryCurr, - GIndex gIFirstBlockRootInSummaryPrev, - GIndex gIFirstBlockRootInSummaryCurr, + GIndices memory gIndices, uint64 firstSupportedSlot, uint64 pivotSlot, uint64 capellaSlot, @@ -157,14 +156,13 @@ contract ValidatorExitDelayVerifier { LOCATOR = ILidoLocator(lidoLocator); - GI_FIRST_VALIDATOR_PREV = gIFirstValidatorPrev; - GI_FIRST_VALIDATOR_CURR = gIFirstValidatorCurr; - - GI_FIRST_HISTORICAL_SUMMARY_PREV = gIFirstHistoricalSummaryPrev; - GI_FIRST_HISTORICAL_SUMMARY_CURR = gIFirstHistoricalSummaryCurr; - - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = gIFirstBlockRootInSummaryPrev; - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = gIFirstBlockRootInSummaryCurr; + // Assign individual GIndex values from the struct + GI_FIRST_VALIDATOR_PREV = gIndices.gIFirstValidatorPrev; + GI_FIRST_VALIDATOR_CURR = gIndices.gIFirstValidatorCurr; + GI_FIRST_HISTORICAL_SUMMARY_PREV = gIndices.gIFirstHistoricalSummaryPrev; + GI_FIRST_HISTORICAL_SUMMARY_CURR = gIndices.gIFirstHistoricalSummaryCurr; + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = gIndices.gIFirstBlockRootInSummaryPrev; + GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = gIndices.gIFirstBlockRootInSummaryCurr; FIRST_SUPPORTED_SLOT = firstSupportedSlot; PIVOT_SLOT = pivotSlot; diff --git a/foundry.toml b/foundry.toml index 80a9e7822f..15fdb28f4d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,7 +21,7 @@ cache_path = 'foundry/cache' match_path = '**/test/**/*.t.sol' # Enable latest EVM features -evm_version = "prague" +evm_version = "cancun" # https://book.getfoundry.sh/reference/config/testing#fuzz # fuzz = { runs = 256 } diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index f701f08040..ecb2d677a2 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -233,12 +233,14 @@ export async function main() { deployer, [ locator.address, - GI_FIRST_VALIDATOR_PREV, - GI_FIRST_VALIDATOR_CURR, - GI_FIRST_HISTORICAL_SUMMARY_PREV, - GI_FIRST_HISTORICAL_SUMMARY_CURR, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, FIRST_SUPPORTED_SLOT, // uint64 firstSupportedSlot, PIVOT_SLOT, // uint64 pivotSlot, CAPELLA_SLOT, // uint64 capellaSlot, diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 9a755311f6..14b582257a 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -56,12 +56,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { before(async () => { validatorExitDelayVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ LIDO_LOCATOR, - GI_FIRST_VALIDATOR_PREV, - GI_FIRST_VALIDATOR_CURR, - GI_FIRST_HISTORICAL_SUMMARY_PREV, - GI_FIRST_HISTORICAL_SUMMARY_CURR, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, CAPELLA_SLOT, @@ -106,12 +108,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { await expect( ethers.deployContract("ValidatorExitDelayVerifier", [ LIDO_LOCATOR, - GI_FIRST_VALIDATOR_PREV, - GI_FIRST_VALIDATOR_CURR, - GI_FIRST_HISTORICAL_SUMMARY_PREV, - GI_FIRST_HISTORICAL_SUMMARY_CURR, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, 200_000, // firstSupportedSlot 100_000, // pivotSlot < firstSupportedSlot CAPELLA_SLOT, @@ -128,12 +132,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { await expect( ethers.deployContract("ValidatorExitDelayVerifier", [ ethers.ZeroAddress, // Zero address for locator - GI_FIRST_VALIDATOR_PREV, - GI_FIRST_VALIDATOR_CURR, - GI_FIRST_HISTORICAL_SUMMARY_PREV, - GI_FIRST_HISTORICAL_SUMMARY_CURR, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, CAPELLA_SLOT, @@ -153,12 +159,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { await expect( ethers.deployContract("ValidatorExitDelayVerifier", [ LIDO_LOCATOR, - GI_FIRST_VALIDATOR_PREV, - GI_FIRST_VALIDATOR_CURR, - GI_FIRST_HISTORICAL_SUMMARY_PREV, - GI_FIRST_HISTORICAL_SUMMARY_CURR, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, FIRST_SUPPORTED_SLOT + 1, // Invalid Capella slot @@ -202,12 +210,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { validatorExitDelayVerifier = await ethers.deployContract("ValidatorExitDelayVerifier", [ locatorAddr, - GI_FIRST_VALIDATOR_PREV, - GI_FIRST_VALIDATOR_CURR, - GI_FIRST_HISTORICAL_SUMMARY_PREV, - GI_FIRST_HISTORICAL_SUMMARY_CURR, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, - GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, FIRST_SUPPORTED_SLOT, PIVOT_SLOT, CAPELLA_SLOT, From d3a6861251d33531d11574637bc709e1a7cdaf8e Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Jul 2025 14:27:54 +0200 Subject: [PATCH 326/405] Revert "refactor: remove GIndex test file" This reverts commit 9360a66295adff1ac6ea64a9f62788bb11f30c05. --- test/0.8.25/lib/GIndex.t.sol | 297 +++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 test/0.8.25/lib/GIndex.t.sol diff --git a/test/0.8.25/lib/GIndex.t.sol b/test/0.8.25/lib/GIndex.t.sol new file mode 100644 index 0000000000..c0595a2e32 --- /dev/null +++ b/test/0.8.25/lib/GIndex.t.sol @@ -0,0 +1,297 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {GIndex, pack, IndexOutOfRange, fls} from "../../../contracts/common/lib/GIndex.sol"; +import {SSZ} from "../../../contracts/common/lib/SSZ.sol"; + +// Wrap the library internal methods to make an actual call to them. +// Supposed to be used with `expectRevert` cheatcode. +contract Library { + function concat(GIndex lhs, GIndex rhs) public pure returns (GIndex) { + return lhs.concat(rhs); + } + + function shr(GIndex self, uint256 n) public pure returns (GIndex) { + return self.shr(n); + } + + function shl(GIndex self, uint256 n) public pure returns (GIndex) { + return self.shl(n); + } +} + +contract GIndexTest is Test { + GIndex internal ZERO = GIndex.wrap(bytes32(0)); + GIndex internal ROOT = GIndex.wrap(0x0000000000000000000000000000000000000000000000000000000000000100); + GIndex internal MAX = GIndex.wrap(bytes32(type(uint256).max)); + + Library internal lib; + + error Log2Undefined(); + + function setUp() public { + lib = new Library(); + } + + function test_pack() public { + GIndex gI; + + gI = pack(0x7b426f79504c6a8e9d31415b722f696e705c8a3d9f41, 42); + assertEq( + gI.unwrap(), + 0x0000000000000000007b426f79504c6a8e9d31415b722f696e705c8a3d9f412a, + "Invalid gindex encoded" + ); + + assertEq(MAX.unwrap(), bytes32(type(uint256).max), "Invalid gindex encoded"); + } + + function test_isRootTrue() public { + assertTrue(ROOT.isRoot(), "ROOT is not root gindex"); + } + + function test_isRootFalse() public { + GIndex gI; + + gI = pack(0, 0); + assertFalse(gI.isRoot(), "Expected [0,0].isRoot() to be false"); + + gI = pack(42, 0); + assertFalse(gI.isRoot(), "Expected [42,0].isRoot() to be false"); + + gI = pack(42, 4); + assertFalse(gI.isRoot(), "Expected [42,4].isRoot() to be false"); + + gI = pack(2048, 4); + assertFalse(gI.isRoot(), "Expected [2048,4].isRoot() to be false"); + + gI = pack(type(uint248).max, type(uint8).max); + assertFalse(gI.isRoot(), "Expected [uint248.max,uint8.max].isRoot() to be false"); + } + + function test_concat() public { + assertEq(pack(2, 99).concat(pack(3, 99)).unwrap(), pack(5, 99).unwrap()); + assertEq(pack(31, 99).concat(pack(3, 99)).unwrap(), pack(63, 99).unwrap()); + assertEq(pack(31, 99).concat(pack(6, 99)).unwrap(), pack(126, 99).unwrap()); + assertEq(ROOT.concat(pack(2, 1)).concat(pack(5, 1)).concat(pack(9, 1)).unwrap(), pack(73, 1).unwrap()); + assertEq(ROOT.concat(pack(2, 9)).concat(pack(5, 1)).concat(pack(9, 4)).unwrap(), pack(73, 4).unwrap()); + + assertEq(ROOT.concat(MAX).unwrap(), MAX.unwrap()); + } + + function test_concat_RevertsIfZeroGIndex() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(ZERO, pack(1024, 1)); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(1024, 1), ZERO); + } + + function test_concat_BigIndicesBorderCases() public view { + lib.concat(pack(2 ** 9, 0), pack(2 ** 238, 0)); + lib.concat(pack(2 ** 47, 0), pack(2 ** 200, 0)); + lib.concat(pack(2 ** 199, 0), pack(2 ** 48, 0)); + } + + function test_concat_RevertsIfTooBigIndices() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(MAX, MAX); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(2 ** 48, 0), pack(2 ** 200, 0)); + + vm.expectRevert(IndexOutOfRange.selector); + lib.concat(pack(2 ** 200, 0), pack(2 ** 48, 0)); + } + + function testFuzz_concat_WithRoot(GIndex rhs) public { + vm.assume(rhs.index() > 0); + assertEq(ROOT.concat(rhs).unwrap(), rhs.unwrap(), "`concat` with a root should return right-hand side value"); + } + + function testFuzz_unpack(uint248 index, uint8 pow) public { + GIndex gI = pack(index, pow); + assertEq(gI.index(), index); + assertEq(gI.width(), 2 ** pow); + } + + function test_shr() public { + GIndex gI; + + gI = pack(1024, 4); + assertEq(gI.shr(0).unwrap(), pack(1024, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(1025, 4).unwrap()); + assertEq(gI.shr(15).unwrap(), pack(1039, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gI.shr(0).unwrap(), pack(1031, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(1032, 4).unwrap()); + assertEq(gI.shr(8).unwrap(), pack(1039, 4).unwrap()); + + gI = pack(2049, 4); + assertEq(gI.shr(0).unwrap(), pack(2049, 4).unwrap()); + assertEq(gI.shr(1).unwrap(), pack(2050, 4).unwrap()); + assertEq(gI.shr(14).unwrap(), pack(2063, 4).unwrap()); + } + + function test_shr_AfterConcat() public { + GIndex gI; + GIndex gIParent = pack(5, 4); + + gI = pack(1024, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5120, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5121, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(15).unwrap(), pack(5135, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(5127, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(5128, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(8).unwrap(), pack(5135, 4).unwrap()); + + gI = pack(2049, 4); + assertEq(gIParent.concat(gI).shr(0).unwrap(), pack(10241, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(1).unwrap(), pack(10242, 4).unwrap()); + assertEq(gIParent.concat(gI).shr(14).unwrap(), pack(10255, 4).unwrap()); + } + + function test_shr_OffTheWidth() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(ROOT, 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1024, 4), 16); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1031, 4), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(pack(1023, 4), 1); + } + + function test_shr_OffTheWidth_AfterConcat() public { + GIndex gIParent = pack(154, 4); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(ROOT), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1024, 4)), 16); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1031, 4)), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(gIParent.concat(pack(1023, 4)), 1); + } + + function testFuzz_shr_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { + // Indices concatenation overflow protection. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + vm.assume(rhs.index() >= rhs.width()); + unchecked { + vm.assume(rhs.width() + shift > rhs.width()); + vm.assume(lhs.concat(rhs).index() + shift > lhs.concat(rhs).index()); + } + + vm.expectRevert(IndexOutOfRange.selector); + lib.shr(lhs.concat(rhs), rhs.width() + shift); + } + + function test_shl() public { + GIndex gI; + + gI = pack(1023, 4); + assertEq(gI.shl(0).unwrap(), pack(1023, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(1022, 4).unwrap()); + assertEq(gI.shl(15).unwrap(), pack(1008, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gI.shl(0).unwrap(), pack(1031, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(1030, 4).unwrap()); + assertEq(gI.shl(7).unwrap(), pack(1024, 4).unwrap()); + + gI = pack(2063, 4); + assertEq(gI.shl(0).unwrap(), pack(2063, 4).unwrap()); + assertEq(gI.shl(1).unwrap(), pack(2062, 4).unwrap()); + assertEq(gI.shl(15).unwrap(), pack(2048, 4).unwrap()); + } + + function test_shl_AfterConcat() public { + GIndex gI; + GIndex gIParent = pack(5, 4); + + gI = pack(1023, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(3071, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(3070, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(3056, 4).unwrap()); + + gI = pack(1031, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(5127, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(5126, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(7).unwrap(), pack(5120, 4).unwrap()); + + gI = pack(2063, 4); + assertEq(gIParent.concat(gI).shl(0).unwrap(), pack(10255, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(1).unwrap(), pack(10254, 4).unwrap()); + assertEq(gIParent.concat(gI).shl(15).unwrap(), pack(10240, 4).unwrap()); + } + + function test_shl_OffTheWidth() public { + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(ROOT, 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1024, 4), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1031, 4), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(pack(1023, 4), 16); + } + + function test_shl_OffTheWidth_AfterConcat() public { + GIndex gIParent = pack(154, 4); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(ROOT), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1024, 4)), 1); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1031, 4)), 9); + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(gIParent.concat(pack(1023, 4)), 16); + } + + function testFuzz_shl_OffTheWidth_AfterConcat(GIndex lhs, GIndex rhs, uint256 shift) public { + // Indices concatenation overflow protection. + vm.assume(fls(lhs.index()) + 1 + fls(rhs.index()) < 248); + vm.assume(rhs.index() >= rhs.width()); + vm.assume(shift > rhs.index() % rhs.width()); + + vm.expectRevert(IndexOutOfRange.selector); + lib.shl(lhs.concat(rhs), shift); + } + + function testFuzz_shl_shr_Idempotent(GIndex gI, uint256 shift) public { + vm.assume(gI.index() > 0); + vm.assume(gI.index() >= gI.width()); + vm.assume(shift < gI.index() % gI.width()); + + assertEq(lib.shr(lib.shl(gI, shift), shift).unwrap(), gI.unwrap()); + } + + function testFuzz_shr_shl_Idempotent(GIndex gI, uint256 shift) public { + vm.assume(gI.index() > 0); + vm.assume(gI.index() >= gI.width()); + vm.assume(shift < gI.width() - (gI.index() % gI.width())); + + assertEq(lib.shl(lib.shr(gI, shift), shift).unwrap(), gI.unwrap()); + } + + function test_fls() public { + for (uint256 i = 1; i < 255; i++) { + assertEq(fls((1 << i) - 1), i - 1); + assertEq(fls((1 << i)), i); + assertEq(fls((1 << i) + 1), i); + } + + assertEq(fls(3), 1); // 0011 + assertEq(fls(7), 2); // 0101 + assertEq(fls(10), 3); // 1010 + assertEq(fls(300), 8); // 0001 0010 1100 + assertEq(fls(0), 256); + } +} From 19d3786123cdf0d7ce2f41a8830b74b12f51b977 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 10 Jul 2025 15:37:41 +0200 Subject: [PATCH 327/405] feat: update deploy TW script --- scripts/triggerable-withdrawals/tw-deploy.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index b0dd68bdd5..57c6b33b3a 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -109,6 +109,8 @@ async function main(): Promise { const VALIDATOR_CURR_GINDEX = VALIDATOR_PREV_GINDEX; const HISTORICAL_SUMMARIES_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; + const BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000000040000d"; + const BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX = BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX; // TriggerableWithdrawalsGateway params const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 11_200; @@ -183,7 +185,14 @@ async function main(): Promise { libraries, }); log.success(`NodeOperatorsRegistry: ${nor.address}`); - + const gIndexes = { + gIFirstValidatorPrev: VALIDATOR_PREV_GINDEX, + gIFirstValidatorCurr: VALIDATOR_CURR_GINDEX, + gIFirstHistoricalSummaryPrev: HISTORICAL_SUMMARIES_PREV_GINDEX, + gIFirstHistoricalSummaryCurr: HISTORICAL_SUMMARIES_CURR_GINDEX, + gIFirstBlockRootInSummaryPrev: BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX, + gIFirstBlockRootInSummaryCurr: BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX, + }; // 6. ValidatorExitDelayVerifier const validatorExitDelayVerifier = await deployImplementation( Sk.validatorExitDelayVerifier, @@ -191,16 +200,15 @@ async function main(): Promise { deployer, [ locator.address, - VALIDATOR_PREV_GINDEX, - VALIDATOR_CURR_GINDEX, - HISTORICAL_SUMMARIES_PREV_GINDEX, - HISTORICAL_SUMMARIES_CURR_GINDEX, + gIndexes, 1, // firstSupportedSlot 1, // pivotSlot + 0, // capellaSlot @see https://github.com/eth-clients/hoodi/blob/main/metadata/config.yaml#L33 + (SLOTS_PER_EPOCH * 8192) / SLOTS_PER_EPOCH, // slotsPerHistoricalRoot SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, - SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT, // seconds + (SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT) / (SLOTS_PER_EPOCH * SECONDS_PER_SLOT), // shardCommitteePeriodInSeconds ], ); log.success(`ValidatorExitDelayVerifier: ${validatorExitDelayVerifier.address}`); From 00aabce25fc669eaeb90a365ce0f8bcf84963888 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 11 Jul 2025 13:11:50 +0200 Subject: [PATCH 328/405] refactor: rename historical GIndex variables and optimize gas usage in Gindex lib --- contracts/common/lib/GIndex.sol | 10 +++++++--- scripts/triggerable-withdrawals/tw-deploy.ts | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/contracts/common/lib/GIndex.sol b/contracts/common/lib/GIndex.sol index fc818f5408..6458a8c42c 100644 --- a/contracts/common/lib/GIndex.sol +++ b/contracts/common/lib/GIndex.sol @@ -74,14 +74,18 @@ function shl(GIndex self, uint256 n) pure returns (GIndex) { // See https://github.com/protolambda/remerkleable/blob/91ed092d08ef0ba5ab076f0a34b0b371623db728/remerkleable/tree.py#L46 function concat(GIndex lhs, GIndex rhs) pure returns (GIndex) { - uint256 lhsMSbIndex = fls(index(lhs)); - uint256 rhsMSbIndex = fls(index(rhs)); + uint256 lindex = index(lhs); + uint256 rindex = index(rhs); + + uint256 lhsMSbIndex = fls(lindex); + uint256 rhsMSbIndex = fls(rindex); if (lhsMSbIndex + 1 + rhsMSbIndex > 248) { revert IndexOutOfRange(); } - return pack((index(lhs) << rhsMSbIndex) | (index(rhs) ^ (1 << rhsMSbIndex)), pow(rhs)); + return + pack((lindex << rhsMSbIndex) | (rindex ^ (1 << rhsMSbIndex)), pow(rhs)); } /// @dev From Solady LibBit, see https://github.com/Vectorized/solady/blob/main/src/utils/LibBit.sol. diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 57c6b33b3a..582ffe8fe5 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -107,8 +107,8 @@ async function main(): Promise { // G‑indices (phase0 spec) const VALIDATOR_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000096000000000028"; const VALIDATOR_CURR_GINDEX = VALIDATOR_PREV_GINDEX; - const HISTORICAL_SUMMARIES_PREV_GINDEX = "0x0000000000000000000000000000000000000000000000000000000000005b00"; - const HISTORICAL_SUMMARIES_CURR_GINDEX = HISTORICAL_SUMMARIES_PREV_GINDEX; + const FIRST_HISTORICAL_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const FIRST_HISTORICAL_SUMMARY_CURR_GINDEX = FIRST_HISTORICAL_SUMMARY_PREV_GINDEX; const BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000000040000d"; const BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX = BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX; @@ -188,8 +188,8 @@ async function main(): Promise { const gIndexes = { gIFirstValidatorPrev: VALIDATOR_PREV_GINDEX, gIFirstValidatorCurr: VALIDATOR_CURR_GINDEX, - gIFirstHistoricalSummaryPrev: HISTORICAL_SUMMARIES_PREV_GINDEX, - gIFirstHistoricalSummaryCurr: HISTORICAL_SUMMARIES_CURR_GINDEX, + gIFirstHistoricalSummaryPrev: FIRST_HISTORICAL_SUMMARY_PREV_GINDEX, + gIFirstHistoricalSummaryCurr: FIRST_HISTORICAL_SUMMARY_CURR_GINDEX, gIFirstBlockRootInSummaryPrev: BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX, gIFirstBlockRootInSummaryCurr: BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX, }; From b7eac136c830dfe7e03cad3da3fa2b8c8e9e9192 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 11 Jul 2025 13:16:38 +0200 Subject: [PATCH 329/405] refactor: update GIndex variable documentation --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index a672e6d8f0..ea7744e8b4 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -71,16 +71,10 @@ contract ValidatorExitDelayVerifier { uint32 public immutable SECONDS_PER_SLOT; uint32 public immutable SHARD_COMMITTEE_PERIOD_IN_SECONDS; - /** - * @notice The GIndex pointing to BeaconState.validators[0] for the "previous" fork. - * @dev Used to derive the correct GIndex when verifying proofs for a block prior to pivot. - */ + /// @dev This index is relative to a state like: `BeaconState.validators[0]`. GIndex public immutable GI_FIRST_VALIDATOR_PREV; - /** - * @notice The GIndex pointing to BeaconState.validators[0] for the "current" fork. - * @dev Used to derive the correct GIndex when verifying proofs for a block after the pivot slot. - */ + /// @dev This index is relative to a state like: `BeaconState.validators[0]`. GIndex public immutable GI_FIRST_VALIDATOR_CURR; /// @dev This index is relative to a state like: `BeaconState.historical_summaries[0]`. From 38b3ecbfecb92990a849e6a57682a2b18738caff Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 11 Jul 2025 13:17:56 +0200 Subject: [PATCH 330/405] refactor: add TODO comment to update Capella slot for e2e testing in mainnet-fork --- scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index ecb2d677a2..467b5b8055 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -243,6 +243,7 @@ export async function main() { }, FIRST_SUPPORTED_SLOT, // uint64 firstSupportedSlot, PIVOT_SLOT, // uint64 pivotSlot, + // TODO: update this to the actual Capella slot for e2e testing in mainnet-fork CAPELLA_SLOT, // uint64 capellaSlot, SLOTS_PER_HISTORICAL_ROOT, // uint64 slotsPerHistoricalRoot, chainSpec.slotsPerEpoch, // uint32 slotsPerEpoch, From 52dfddbb07f1be2127f8a510f43dd5c6b04b4ae3 Mon Sep 17 00:00:00 2001 From: hweawer Date: Fri, 11 Jul 2025 15:23:13 +0200 Subject: [PATCH 331/405] Remove redundant zero check --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index ea7744e8b4..c99ff90b60 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -394,10 +394,6 @@ contract ValidatorExitDelayVerifier { ) internal view returns (uint256 deliveryTimestamp) { bytes32 exitRequestsHash = keccak256(abi.encode(exitRequests.data, exitRequests.dataFormat)); deliveryTimestamp = veb.getDeliveryTimestamp(exitRequestsHash); - - if (deliveryTimestamp == 0) { - revert EmptyDeliveryHistory(); - } } function _slotToTimestamp(uint64 slot) internal view returns (uint256) { From 8b2dc8fcd140600155c3c16f2f6313b9dc94f258 Mon Sep 17 00:00:00 2001 From: hweawer Date: Fri, 11 Jul 2025 15:27:33 +0200 Subject: [PATCH 332/405] Change timestamp comparison method --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index ea7744e8b4..b07f383274 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -355,7 +355,7 @@ contract ValidatorExitDelayVerifier { ? deliveredTimestamp : earliestPossibleVoluntaryExitTimestamp; - if (referenceSlotTimestamp < eligibleExitRequestTimestamp) { + if (referenceSlotTimestamp <= eligibleExitRequestTimestamp) { revert ExitIsNotEligibleOnProvableBeaconBlock(referenceSlotTimestamp, eligibleExitRequestTimestamp); } From 59a8ecb5dfcec7b3db6fb0c1b0c14c4bc39a13a5 Mon Sep 17 00:00:00 2001 From: hweawer Date: Fri, 11 Jul 2025 15:53:08 +0200 Subject: [PATCH 333/405] Try fix unit test --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 1 - test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol | 5 +++++ test/0.8.25/validatorExitDelayVerifier.test.ts | 7 +++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index c99ff90b60..663ecb498a 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -115,7 +115,6 @@ contract ValidatorExitDelayVerifier { uint256 provableBeaconBlockTimestamp, uint256 eligibleExitRequestTimestamp ); - error EmptyDeliveryHistory(); error InvalidCapellaSlot(); /** diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 8eb3df7354..c2c85fd9f3 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; import {IValidatorsExitBus} from "contracts/0.8.25/interfaces/IValidatorsExitBus.sol"; +error RequestsNotDelivered(); + struct MockExitRequestData { bytes pubkey; uint256 nodeOpId; @@ -32,6 +34,9 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp) { require(exitRequestsHash == _hash, "Mock error, Invalid exitRequestsHash"); + if (_deliveryTimestamp == 0) { + revert RequestsNotDelivered(); + } return _deliveryTimestamp; } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 14b582257a..6f66189603 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -547,7 +547,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.reverted; }); - it("reverts with 'EmptyDeliveryHistory' if exit request index is not in delivery history", async () => { + it("reverts with 'RequestsNotDelivered' if exit request index is not in delivery history", async () => { const exitRequests: ExitRequest[] = [ { moduleId: 1, @@ -565,7 +565,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { // Report not unpacked, deliveryTimestamp == 0 await vebo.setExitRequests(encodedExitRequestsHash, 0, exitRequests); - expect(await vebo.getDeliveryTimestamp(encodedExitRequestsHash)).to.equal(0); await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( @@ -573,7 +572,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); + ).to.be.revertedWithCustomError(vebo, "RequestsNotDelivered"); await expect( validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( @@ -582,7 +581,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, unpackedExitRequestIndex)], encodedExitRequests, ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "EmptyDeliveryHistory"); + ).to.be.revertedWithCustomError(vebo, "RequestsNotDelivered"); }); it("reverts if the oldBlock proof is corrupted", async () => { From b0ac708e5d590f625106e66852c801061be062a3 Mon Sep 17 00:00:00 2001 From: hweawer Date: Mon, 14 Jul 2025 09:19:26 +0200 Subject: [PATCH 334/405] Add test --- .../0.8.25/validatorExitDelayVerifier.test.ts | 137 +++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 14b582257a..99d4431cfc 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -234,7 +234,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const veboExitRequestTimestamp = GENESIS_TIME + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; + SECONDS_PER_SLOT; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; const exitRequests: ExitRequest[] = [ @@ -498,6 +498,141 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); }); + it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when proof slot timestamp equals eligible exit request timestamp", async () => { + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; + + const veboExitRequestTimestamp = proofSlotTimestamp; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); + + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); + }); + + it("accepts proof when proof slot timestamp is exactly 1 second after eligible exit request timestamp", async () => { + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; + + const veboExitRequestTimestamp = proofSlotTimestamp - 1; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); + const futureBlockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeaderRoot); + + const verifyExitDelayEvents = async (tx: ContractTransactionResponse) => { + const receipt = await tx.wait(); + const events = findStakingRouterMockEvents(receipt!, "UnexitedValidatorReported"); + expect(events.length).to.equal(1); + + const event = events[0]; + expect(event.args[0]).to.equal(moduleId); + expect(event.args[1]).to.equal(nodeOpId); + expect(event.args[2]).to.equal(proofSlotTimestamp); + expect(event.args[3]).to.equal(ACTIVE_VALIDATOR_PROOF.validator.pubkey); + expect(event.args[4]).to.equal(1); // Expected 1 second delay + }; + + await verifyExitDelayEvents( + await validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ); + + await verifyExitDelayEvents( + await validatorExitDelayVerifier.verifyHistoricalValidatorExitDelay( + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, futureBlockRootTimestamp), + toHistoricalHeaderWitness(ACTIVE_VALIDATOR_PROOF), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ); + }); + + it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when proof slot timestamp equals earliest possible voluntary exit timestamp", async () => { + // Calculate the earliest possible voluntary exit timestamp for this validator + const earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + + (Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + + Number(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()); + + // Set exit request timestamp to be before the earliest possible voluntary exit time + // so that earliestPossibleVoluntaryExitTimestamp is used as eligibleExitRequestTimestamp + const veboExitRequestTimestamp = earliestPossibleVoluntaryExitTimestamp - 1000; + + const moduleId = 1; + const nodeOpId = 2; + const exitRequests: ExitRequest[] = [ + { + moduleId, + nodeOpId, + valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, + pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, + }, + ]; + const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); + + await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); + const targetSlot = Math.floor((earliestPossibleVoluntaryExitTimestamp - GENESIS_TIME) / SECONDS_PER_SLOT); + const customBeaconBlockHeader = { + ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, + slot: targetSlot, + }; + + const customBeaconBlockHeaderRoot = customBeaconBlockHeader.slot.toString(); // Mock root + const blockRootTimestamp = await updateBeaconBlockRoot(customBeaconBlockHeaderRoot); + + await expect( + validatorExitDelayVerifier.verifyValidatorExitDelay( + toProvableBeaconBlockHeader(customBeaconBlockHeader, blockRootTimestamp), + [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + encodedExitRequests, + ), + ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); + }); + it("reverts if the validator proof is incorrect", async () => { const intervalInSecondsBetweenProvableBlockAndExitRequest = 1000; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); From 6bb34649a3744074bd09afefe02b384948779f2b Mon Sep 17 00:00:00 2001 From: hweawer Date: Mon, 14 Jul 2025 09:33:06 +0200 Subject: [PATCH 335/405] Fix test --- .../0.8.25/validatorExitDelayVerifier.test.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 99d4431cfc..a7627469a4 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -593,14 +593,19 @@ describe("ValidatorExitDelayVerifier.sol", () => { }); it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when proof slot timestamp equals earliest possible voluntary exit timestamp", async () => { - // Calculate the earliest possible voluntary exit timestamp for this validator - const earliestPossibleVoluntaryExitTimestamp = GENESIS_TIME + - (Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT) + - Number(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()); + const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; + const shardCommitteePeriod = Number(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()); + + const requiredActivationEpochTimestamp = proofSlotTimestamp - shardCommitteePeriod; + const requiredActivationEpoch = Math.floor((requiredActivationEpochTimestamp - GENESIS_TIME) / (SLOTS_PER_EPOCH * SECONDS_PER_SLOT)); + + const customValidatorWitness = { + ...toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), + activationEpoch: requiredActivationEpoch, + }; // Set exit request timestamp to be before the earliest possible voluntary exit time - // so that earliestPossibleVoluntaryExitTimestamp is used as eligibleExitRequestTimestamp - const veboExitRequestTimestamp = earliestPossibleVoluntaryExitTimestamp - 1000; + const veboExitRequestTimestamp = proofSlotTimestamp - 1000; const moduleId = 1; const nodeOpId = 2; @@ -615,19 +620,13 @@ describe("ValidatorExitDelayVerifier.sol", () => { const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - const targetSlot = Math.floor((earliestPossibleVoluntaryExitTimestamp - GENESIS_TIME) / SECONDS_PER_SLOT); - const customBeaconBlockHeader = { - ...ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, - slot: targetSlot, - }; - const customBeaconBlockHeaderRoot = customBeaconBlockHeader.slot.toString(); // Mock root - const blockRootTimestamp = await updateBeaconBlockRoot(customBeaconBlockHeaderRoot); + const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); await expect( validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(customBeaconBlockHeader, blockRootTimestamp), - [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], + toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), + [customValidatorWitness], encodedExitRequests, ), ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); From 10665906ceee8706593a811400e73e4992abea8b Mon Sep 17 00:00:00 2001 From: hweawer Date: Mon, 14 Jul 2025 10:01:08 +0200 Subject: [PATCH 336/405] Fix test --- .../0.8.25/validatorExitDelayVerifier.test.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index a7627469a4..d1a0c2775b 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -592,46 +592,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { ); }); - it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when proof slot timestamp equals earliest possible voluntary exit timestamp", async () => { - const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; - const shardCommitteePeriod = Number(await validatorExitDelayVerifier.SHARD_COMMITTEE_PERIOD_IN_SECONDS()); - - const requiredActivationEpochTimestamp = proofSlotTimestamp - shardCommitteePeriod; - const requiredActivationEpoch = Math.floor((requiredActivationEpochTimestamp - GENESIS_TIME) / (SLOTS_PER_EPOCH * SECONDS_PER_SLOT)); - - const customValidatorWitness = { - ...toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0), - activationEpoch: requiredActivationEpoch, - }; - - // Set exit request timestamp to be before the earliest possible voluntary exit time - const veboExitRequestTimestamp = proofSlotTimestamp - 1000; - - const moduleId = 1; - const nodeOpId = 2; - const exitRequests: ExitRequest[] = [ - { - moduleId, - nodeOpId, - valIndex: ACTIVE_VALIDATOR_PROOF.validator.index, - pubkey: ACTIVE_VALIDATOR_PROOF.validator.pubkey, - }, - ]; - const { encodedExitRequests, encodedExitRequestsHash } = encodeExitRequestsDataListWithFormat(exitRequests); - - await vebo.setExitRequests(encodedExitRequestsHash, veboExitRequestTimestamp, exitRequests); - - const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); - - await expect( - validatorExitDelayVerifier.verifyValidatorExitDelay( - toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, blockRootTimestamp), - [customValidatorWitness], - encodedExitRequests, - ), - ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "ExitIsNotEligibleOnProvableBeaconBlock"); - }); - it("reverts if the validator proof is incorrect", async () => { const intervalInSecondsBetweenProvableBlockAndExitRequest = 1000; const blockRootTimestamp = await updateBeaconBlockRoot(ACTIVE_VALIDATOR_PROOF.beaconBlockHeaderRoot); From 3493aaa87a705dea569462459e6f9c49079173c4 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 14 Jul 2025 12:56:59 +0400 Subject: [PATCH 337/405] feat: Emit an event when the maximum number of validator exits per report is changed; removed unused errors; remove unused using for --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 8 ++++++++ contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol | 4 ---- ...lidator-exit-bus-oracle.submitExitRequestsData.test.ts | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index c64a99ff6c..d51a94e4cb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -141,6 +141,12 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V */ event ExitDataProcessing(bytes32 exitRequestsHash); + /** + * @notice Emitted when max validators per report value is set. + * @param maxValidatorsPerReport The number of valdiators allowed per report. + */ + event SetMaxValidatorsPerReport(uint256 maxValidatorsPerReport); + struct ExitRequestsData { bytes data; uint256 dataFormat; @@ -535,6 +541,8 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V if (maxValidatorsPerReport == 0) revert ZeroArgument("maxValidatorsPerReport"); MAX_VALIDATORS_PER_REPORT_POSITION.setStorageUint256(maxValidatorsPerReport); + + emit SetMaxValidatorsPerReport(maxValidatorsPerReport); } function _getMaxValidatorsPerReport() internal view returns (uint256) { diff --git a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol index 5189a0ae08..b3062b241b 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol @@ -8,7 +8,6 @@ import {UnstructuredStorage} from "../lib/UnstructuredStorage.sol"; import {BaseOracle} from "./BaseOracle.sol"; import {ValidatorsExitBus} from "./ValidatorsExitBus.sol"; -import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "../lib/ExitLimitUtils.sol"; interface IOracleReportSanityChecker { function checkExitBusOracleReport(uint256 _exitRequestsCount) external view; @@ -17,13 +16,10 @@ interface IOracleReportSanityChecker { contract ValidatorsExitBusOracle is BaseOracle, ValidatorsExitBus { using UnstructuredStorage for bytes32; using SafeCast for uint256; - using ExitLimitUtilsStorage for bytes32; - using ExitLimitUtils for ExitRequestLimitData; error AdminCannotBeZero(); error SenderNotAllowed(); error UnexpectedRequestsDataLength(); - error ArgumentOutOfBounds(); event WarnDataIncompleteProcessing(uint256 indexed refSlot, uint256 requestsProcessed, uint256 requestsCount); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts index 861d97aaad..7955a51c41 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.submitExitRequestsData.test.ts @@ -444,7 +444,8 @@ describe("ValidatorsExitBusOracle.sol:submitExitRequestsData", () => { const maxRequestsPerReport = 4; - await oracle.connect(authorizedEntity).setMaxValidatorsPerReport(maxRequestsPerReport); + const tx = await oracle.connect(authorizedEntity).setMaxValidatorsPerReport(maxRequestsPerReport); + await expect(tx).to.emit(oracle, "SetMaxValidatorsPerReport").withArgs(maxRequestsPerReport); expect(await oracle.connect(authorizedEntity).getMaxValidatorsPerReport()).to.equal(maxRequestsPerReport); const exitRequestsRandom = [ From 498d5da9f1fb70c11b71a9dbf8cc9a2a308bfc6f Mon Sep 17 00:00:00 2001 From: hweawer Date: Mon, 14 Jul 2025 12:38:34 +0200 Subject: [PATCH 338/405] Remove redundant version check --- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 1 - ...dator-exit-bus-oracle.triggerExits.test.ts | 60 ------------------- 2 files changed, 61 deletions(-) diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 35b7bea125..ae306affeb 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -308,7 +308,6 @@ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, V _checkExitSubmitted(requestStatus); _checkDelivered(requestStatus); _checkExitRequestData(exitsData.data, exitsData.dataFormat); - _checkContractVersion(requestStatus.contractVersion); ITriggerableWithdrawalsGateway.ValidatorData[] memory triggerableExitData = new ITriggerableWithdrawalsGateway.ValidatorData[](exitDataIndexes.length); diff --git a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts index ccdf3b81d7..d5ddcb7396 100644 --- a/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts +++ b/test/0.8.9/oracle/validator-exit-bus-oracle.triggerExits.test.ts @@ -402,64 +402,4 @@ describe("ValidatorsExitBusOracle.sol:triggerExits", () => { ).to.be.revertedWithCustomError(oracle, "InvalidModuleId"); }); }); - - describe("Version changed", () => { - // version should be changed during deploy - // but we will change it via accessing storage - - const VALIDATORS: ExitRequest[] = [{ moduleId: 1, nodeOpId: 0, valIndex: 0, valPubkey: PUBKEYS[0] }]; - - const REQUEST = { - dataFormat: DATA_FORMAT_LIST, - data: encodeExitRequestsDataList(VALIDATORS), - }; - - const HASH_REQUEST = hashExitRequest(REQUEST); - - before(async () => { - [admin, authorizedEntity] = await ethers.getSigners(); - - await deploy(); - - const role = await oracle.SUBMIT_REPORT_HASH_ROLE(); - await oracle.grantRole(role, authorizedEntity); - }); - - it("Check version", async () => { - // set in initialize in deployVEBO - expect(await oracle.getContractVersion()).to.equal(2); - }); - - it("Store exit hash", async () => { - await oracle.connect(authorizedEntity).submitExitRequestsHash(HASH_REQUEST); - - const emitTx = await oracle.submitExitRequestsData(REQUEST); - const timestamp = await oracle.getTime(); - - await expect(emitTx) - .to.emit(oracle, "ValidatorExitRequest") - .withArgs( - VALIDATORS[0].moduleId, - VALIDATORS[0].nodeOpId, - VALIDATORS[0].valIndex, - VALIDATORS[0].valPubkey, - timestamp, - ); - }); - - it("set new version", async () => { - await oracle.setContractVersion(3); - expect(await oracle.getContractVersion()).to.equal(3); - }); - - it("Should revert if request has old contract version", async () => { - await expect( - oracle.triggerExits({ data: REQUEST.data, dataFormat: REQUEST.dataFormat }, [0], ZERO_ADDRESS, { - value: 4, - }), - ) - .to.be.revertedWithCustomError(oracle, "UnexpectedContractVersion") - .withArgs(3, 2); - }); - }); }); From 26bea4d3c26a050ef27a06784838c8a42a31547b Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 14 Jul 2025 13:34:17 +0200 Subject: [PATCH 339/405] feat: add underflow protection and tests for setExitDeadlineThreshold function --- .../0.4.24/nos/NodeOperatorsRegistry.sol | 3 + .../0.4.24/nor/nor.initialize.upgrade.test.ts | 109 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c4b2dabf56..3930e096c8 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -1073,6 +1073,9 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { function _setExitDeadlineThreshold(uint256 _threshold, uint256 _lateReportingWindow) internal { require(_threshold > 0, "INVALID_EXIT_DELAY_THRESHOLD"); + // Check for underflow protection before computing currentCutoffTimestamp + require(block.timestamp >= _threshold + _lateReportingWindow, "CUTOFF_TIMESTAMP_UNDERFLOW"); + // Set the cutoff timestamp to the current time minus the threshold and reportingWindow period uint256 currentCutoffTimestamp = block.timestamp - _threshold - _lateReportingWindow; require(exitPenaltyCutoffTimestamp() <= currentCutoffTimestamp, "INVALID_EXIT_PENALTY_CUTOFF_TIMESTAMP"); diff --git a/test/0.4.24/nor/nor.initialize.upgrade.test.ts b/test/0.4.24/nor/nor.initialize.upgrade.test.ts index d5466c6216..661dd2bde6 100644 --- a/test/0.4.24/nor/nor.initialize.upgrade.test.ts +++ b/test/0.4.24/nor/nor.initialize.upgrade.test.ts @@ -202,4 +202,113 @@ describe("NodeOperatorsRegistry.sol:initialize-and-upgrade", () => { expect(await nor.exitPenaltyCutoffTimestamp()).to.be.lte(currentTimestamp); }); }); + + context("setExitDeadlineThreshold", () => { + beforeEach(async () => { + locator = await deployLidoLocator({ lido: lido }); + await nor.initialize(locator, moduleType, 86400n); + }); + + it("Successfully sets exit deadline threshold with valid parameters", async () => { + // Use smaller threshold and reporting window to get a higher (later) cutoff timestamp + const threshold = 43200n; // 12 hours (smaller than initial 24h) + const reportingWindow = 3600n; // 1 hour + + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(threshold, reportingWindow)) + .to.emit(nor, "ExitDeadlineThresholdChanged") + .withArgs(threshold, reportingWindow); + + expect(await nor.exitDeadlineThreshold(0)).to.equal(threshold); + }); + + it("Reverts when threshold is zero", async () => { + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(0n, 3600n)) + .to.be.revertedWith("INVALID_EXIT_DELAY_THRESHOLD"); + }); + + it("Reverts when sum of threshold and reporting window causes underflow", async () => { + const currentTime = await time.latest(); + const threshold = BigInt(currentTime) + 1000n; // Future timestamp + const reportingWindow = 1000n; + + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(threshold, reportingWindow)) + .to.be.revertedWith("CUTOFF_TIMESTAMP_UNDERFLOW"); + }); + + it("Reverts when new cutoff timestamp is less than current cutoff timestamp", async () => { + // First set a smaller threshold to get a higher cutoff timestamp + await nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(43200n, 1800n); + + // Try to set a higher threshold that would result in a lower (earlier) cutoff timestamp + // This should fail because cutoff timestamp must be monotonically increasing + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(172800n, 3600n)) + .to.be.revertedWith("INVALID_EXIT_PENALTY_CUTOFF_TIMESTAMP"); + }); + + it("Works correctly with minimal values", async () => { + // Use minimal threshold and no reporting window to get maximum cutoff timestamp + const threshold = 1n; + const reportingWindow = 0n; + + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(threshold, reportingWindow)) + .to.emit(nor, "ExitDeadlineThresholdChanged") + .withArgs(threshold, reportingWindow); + + expect(await nor.exitDeadlineThreshold(0)).to.equal(threshold); + + const currentTime = BigInt(await time.latest()); + const actualCutoff = await nor.exitPenaltyCutoffTimestamp(); + expect(actualCutoff).to.be.closeTo(currentTime - 1n, 5n); + }); + + it("Prevents underflow scenario", async () => { + // Simulate scenario where _threshold + _lateReportingWindow > block.timestamp + const currentTime = BigInt(await time.latest()); + + // This should fail due to underflow protection + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(currentTime, currentTime)) + .to.be.revertedWith("CUTOFF_TIMESTAMP_UNDERFLOW"); + }); + + it("Only allows MANAGE_NODE_OPERATOR_ROLE to set threshold", async () => { + await expect(nor.connect(user).setExitDeadlineThreshold(43200n, 3600n)) + .to.be.revertedWith("APP_AUTH_FAILED"); + }); + + it("Updates cutoff timestamp correctly with monotonic increase", async () => { + const initialCutoff = await nor.exitPenaltyCutoffTimestamp(); + + // Use smaller threshold to ensure new cutoff timestamp is higher + const threshold = 21600n; // 6 hours (smaller than initial 24h) + const reportingWindow = 3600n; // 1 hour + + await nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(threshold, reportingWindow); + + const newCutoff = await nor.exitPenaltyCutoffTimestamp(); + + // New cutoff should be greater than or equal to the initial cutoff (monotonic) + expect(newCutoff).to.be.gte(initialCutoff); + + // Verify the threshold was updated + expect(await nor.exitDeadlineThreshold(0)).to.equal(threshold); + }); + + it("Allows setting same cutoff timestamp", async () => { + const currentCutoff = await nor.exitPenaltyCutoffTimestamp(); + + // Advance time a bit + await time.increase(3600); // 1 hour + + // Calculate parameters that would result in the same cutoff timestamp + const newCurrentTime = BigInt(await time.latest()); + const targetCutoff = currentCutoff; + const newThreshold = 43200n; // 12 hours + const newReportingWindow = newCurrentTime - targetCutoff - newThreshold; + + // This should work as the cutoff timestamp will be the same (>= condition) + await expect(nor.connect(nodeOperatorsManager).setExitDeadlineThreshold(newThreshold, newReportingWindow)) + .to.emit(nor, "ExitDeadlineThresholdChanged") + .withArgs(newThreshold, newReportingWindow); + }); + }); }); From 7ec530da624dbc39179857bf372810325f03709c Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 14 Jul 2025 13:42:21 +0200 Subject: [PATCH 340/405] refactor: reuse variable in verifyValidatorExitDelay --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 71303f394e..2fb4daa61d 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -206,7 +206,7 @@ contract ValidatorExitDelayVerifier { proofSlotTimestamp ); - _verifyValidatorExitUnset(beaconBlock.header, validatorWitnesses[i], pubkey, valIndex); + _verifyValidatorExitUnset(beaconBlock.header, witness, pubkey, valIndex); stakingRouter.reportValidatorExitDelay(moduleId, nodeOpId, proofSlotTimestamp, pubkey, eligibleToExitInSec); } From d3ada7a8edbe81839f8dbd56547c1354e1f07718 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 14 Jul 2025 13:51:43 +0200 Subject: [PATCH 341/405] docs: update deprecation notice for updateTargetValidatorsLimits function --- contracts/0.4.24/nos/NodeOperatorsRegistry.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol index c4b2dabf56..74fb99605d 100644 --- a/contracts/0.4.24/nos/NodeOperatorsRegistry.sol +++ b/contracts/0.4.24/nos/NodeOperatorsRegistry.sol @@ -590,7 +590,8 @@ contract NodeOperatorsRegistry is AragonApp, Versioned { /// @param _nodeOperatorId Id of the node operator /// @param _isTargetLimitActive Flag indicating if the soft target limit is active /// @param _targetLimit Target limit of the node operator - /// @dev This function is deprecated, use updateTargetValidatorsLimits instead + /// @dev DEPRECATED: This function updateTargetValidatorsLimits(uint256, bool, uint256) is deprecated + /// @dev Use updateTargetValidatorsLimits(uint256, uint256, uint256) instead function updateTargetValidatorsLimits(uint256 _nodeOperatorId, bool _isTargetLimitActive, uint256 _targetLimit) public { updateTargetValidatorsLimits(_nodeOperatorId, _isTargetLimitActive ? 1 : 0, _targetLimit); } From 5b1e442fb7e6d37d55adf85726fc2916083dbec2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 14 Jul 2025 14:16:58 +0200 Subject: [PATCH 342/405] refactor: update getFrameConfig to include fastLaneLengthSlots --- contracts/0.8.9/oracle/AccountingOracle.sol | 3 +-- contracts/0.8.9/oracle/BaseOracle.sol | 6 +++++- test/0.8.9/contracts/ConsensusContract__Mock.sol | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index d174611b74..874025f90d 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -514,8 +514,7 @@ contract AccountingOracle is BaseOracle { ) internal view returns (uint256) { - (uint256 initialEpoch, - uint256 epochsPerFrame) = IConsensusContract(consensusContract).getFrameConfig(); + (uint256 initialEpoch, uint256 epochsPerFrame, /* uint256 _fastLaneLengthSlots */) = IConsensusContract(consensusContract).getFrameConfig(); (uint256 slotsPerEpoch, uint256 secondsPerSlot, diff --git a/contracts/0.8.9/oracle/BaseOracle.sol b/contracts/0.8.9/oracle/BaseOracle.sol index 255c1fec5a..f3e5a7cb5e 100644 --- a/contracts/0.8.9/oracle/BaseOracle.sol +++ b/contracts/0.8.9/oracle/BaseOracle.sol @@ -25,7 +25,11 @@ interface IConsensusContract { uint256 genesisTime ); - function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame); + function getFrameConfig() external view returns ( + uint256 initialEpoch, + uint256 epochsPerFrame, + uint256 fastLaneLengthSlots + ); function getInitialRefSlot() external view returns (uint256); } diff --git a/test/0.8.9/contracts/ConsensusContract__Mock.sol b/test/0.8.9/contracts/ConsensusContract__Mock.sol index ed07543699..546209a1aa 100644 --- a/test/0.8.9/contracts/ConsensusContract__Mock.sol +++ b/test/0.8.9/contracts/ConsensusContract__Mock.sol @@ -84,8 +84,8 @@ contract ConsensusContract__Mock is IConsensusContract { return (SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME); } - function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame) { - return (_frameConfig.initialEpoch, _frameConfig.epochsPerFrame); + function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame, uint256 fastLaneLengthSlots) { + return (_frameConfig.initialEpoch, _frameConfig.epochsPerFrame, _frameConfig.fastLaneLengthSlots); } function getInitialRefSlot() external view returns (uint256) { From 5c36fa411e5a9def7df46cc6073fa3da1b079eb0 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 14 Jul 2025 23:17:42 +0400 Subject: [PATCH 343/405] fix: setExitLimits --- contracts/0.8.9/lib/ExitLimitUtils.sol | 23 ++++++++++++++--------- test/0.8.9/lib/exitLimitUtils.test.ts | 8 ++++++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/0.8.9/lib/ExitLimitUtils.sol b/contracts/0.8.9/lib/ExitLimitUtils.sol index be51e8d653..8a2ffd42a6 100644 --- a/contracts/0.8.9/lib/ExitLimitUtils.sol +++ b/contracts/0.8.9/lib/ExitLimitUtils.sol @@ -95,18 +95,23 @@ library ExitLimitUtils { if (exitsPerFrame > maxExitRequestsLimit) revert TooLargeExitsPerFrame(); if (frameDurationInSec == 0) revert ZeroFrameDuration(); - _data.exitsPerFrame = uint32(exitsPerFrame); - _data.frameDurationInSec = uint32(frameDurationInSec); - - if ( - // new maxExitRequestsLimit is smaller than prev remaining limit - maxExitRequestsLimit < _data.prevExitRequestsLimit || - // previously exits were unlimited - _data.maxExitRequestsLimit == 0 - ) { + if (_data.maxExitRequestsLimit == 0) { + // no limit was set before, set the new limit _data.prevExitRequestsLimit = uint32(maxExitRequestsLimit); + } else { + uint256 currentLimit = calculateCurrentExitLimit(_data, timestamp); + // update current limit proportionally as `newLimit - exitsUsed` + // where `exitsUsed` is relative to the previous limit + uint32 exitsUsed = _data.maxExitRequestsLimit - uint32(currentLimit); + if (exitsUsed >= maxExitRequestsLimit) { + _data.prevExitRequestsLimit = 0; + } else { + _data.prevExitRequestsLimit = uint32(maxExitRequestsLimit - exitsUsed); + } } + _data.exitsPerFrame = uint32(exitsPerFrame); + _data.frameDurationInSec = uint32(frameDurationInSec); _data.maxExitRequestsLimit = uint32(maxExitRequestsLimit); _data.prevTimestamp = uint32(timestamp); diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts index 8db9799ec9..dabcb81c62 100644 --- a/test/0.8.9/lib/exitLimitUtils.test.ts +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -385,8 +385,10 @@ describe("ExitLimitUtils.sol", () => { timestamp, ); + const newPrevExitRequestsLimit = 30; // 50 - (100 - 80) + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); - expect(result.prevExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(newPrevExitRequestsLimit); expect(result.prevTimestamp).to.equal(timestamp); }); @@ -414,8 +416,10 @@ describe("ExitLimitUtils.sol", () => { timestamp, ); + const newPrevExitRequestsLimit = 130; // 150 - ( 100 - 80); + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); - expect(result.prevExitRequestsLimit).to.equal(prevExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(newPrevExitRequestsLimit); expect(result.prevTimestamp).to.equal(timestamp); }); From 9db168326fba94a409fb315a668464766b3e351c Mon Sep 17 00:00:00 2001 From: hweawer Date: Tue, 15 Jul 2025 13:06:41 +0200 Subject: [PATCH 344/405] Add tests --- test/0.8.9/lib/exitLimitUtils.test.ts | 250 ++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/test/0.8.9/lib/exitLimitUtils.test.ts b/test/0.8.9/lib/exitLimitUtils.test.ts index dabcb81c62..c6bf98c206 100644 --- a/test/0.8.9/lib/exitLimitUtils.test.ts +++ b/test/0.8.9/lib/exitLimitUtils.test.ts @@ -473,6 +473,256 @@ describe("ExitLimitUtils.sol", () => { "TooLargeFrameDuration", ); }); + + context("proportional limit adjustments", () => { + it("should proportionally increase limits: 100→200 max with 30 remaining should become 130 remaining", async () => { + const timestamp = 1000; + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 30; // 70 exits were used (100 - 30 = 70) + const exitsPerFrame = 2; + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + const newMaxExitRequestsLimit = 200; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + // exitsUsed = 100 - 30 = 70 + // newPrevLimit = 200 - 70 = 130 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(130); + expect(result.prevTimestamp).to.equal(timestamp); + }); + + it("should proportionally decrease limits: 100→80 max with 60 remaining should become 40 remaining", async () => { + const timestamp = 1000; + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 60; // 40 exits were used (100 - 60 = 40) + const exitsPerFrame = 2; + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + const newMaxExitRequestsLimit = 80; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + // exitsUsed = 100 - 60 = 40 + // newPrevLimit = 80 - 40 = 40 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(40); + expect(result.prevTimestamp).to.equal(timestamp); + }); + + it("should set to 0 when usage exceeds new limit: 100→50 max with 20 remaining (80 used) should become 0", async () => { + const timestamp = 1000; + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 20; // 80 exits were used (100 - 20 = 80) + const exitsPerFrame = 2; + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + const newMaxExitRequestsLimit = 50; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + // exitsUsed = 100 - 20 = 80 + // newPrevLimit = max(0, 50 - 80) = 0 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(0); + expect(result.prevTimestamp).to.equal(timestamp); + }); + + it("should handle time-based restoration with proportional adjustment", async () => { + const oldTimestamp = 1000; + const newTimestamp = 1030; // 3 frames passed (30 seconds / 10 per frame) + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 40; // 60 exits were used initially + const exitsPerFrame = 5; // 5 exits restored per frame + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + oldTimestamp, + ); + + const newMaxExitRequestsLimit = 150; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + newTimestamp, + ); + + // currentLimit at newTimestamp = min(100, 40 + 3*5) = min(100, 55) = 55 + // exitsUsed = 100 - 55 = 45 + // newPrevLimit = 150 - 45 = 105 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(105); + expect(result.prevTimestamp).to.equal(newTimestamp); + }); + + it("should handle full restoration edge case", async () => { + const oldTimestamp = 1000; + const newTimestamp = 1100; // 10 frames passed (100 seconds / 10 per frame) + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 20; // 80 exits were used initially + const exitsPerFrame = 10; // 10 exits restored per frame + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + oldTimestamp, + ); + + const newMaxExitRequestsLimit = 200; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + newTimestamp, + ); + + // currentLimit at newTimestamp = min(100, 20 + 10*10) = min(100, 120) = 100 (fully restored) + // exitsUsed = 100 - 100 = 0 + // newPrevLimit = 200 - 0 = 200 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(200); + expect(result.prevTimestamp).to.equal(newTimestamp); + }); + + it("should handle exact equality boundary: exits used equals new max", async () => { + const timestamp = 1000; + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 25; // 75 exits were used + const exitsPerFrame = 2; + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + const newMaxExitRequestsLimit = 75; // exactly equal to exits used + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + timestamp, + ); + + // exitsUsed = 100 - 25 = 75 + // newPrevLimit = 75 - 75 = 0 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(0); + expect(result.prevTimestamp).to.equal(timestamp); + }); + + it("should handle fractional frame restoration (truncating partial frames)", async () => { + const oldTimestamp = 1000; + const newTimestamp = 1027; // 2.7 frames passed (27 seconds / 10 per frame) - should truncate to 2 frames + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 50; // 50 exits were used initially + const exitsPerFrame = 3; // 3 exits restored per frame + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + oldTimestamp, + ); + + const newMaxExitRequestsLimit = 120; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + newTimestamp, + ); + + // currentLimit at newTimestamp = min(100, 50 + 2*3) = min(100, 56) = 56 (2 full frames only) + // exitsUsed = 100 - 56 = 44 + // newPrevLimit = 120 - 44 = 76 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(76); + expect(result.prevTimestamp).to.equal(newTimestamp); + }); + + it("should preserve proportionality with zero exits per frame (no restoration)", async () => { + const oldTimestamp = 1000; + const newTimestamp = 1050; // 5 frames passed but no restoration due to exitsPerFrame = 0 + const oldMaxExitRequestsLimit = 100; + const prevExitRequestsLimit = 30; // 70 exits were used + const exitsPerFrame = 0; // no restoration + const frameDurationInSec = 10; + + await exitLimit.harness_setState( + oldMaxExitRequestsLimit, + prevExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + oldTimestamp, + ); + + const newMaxExitRequestsLimit = 150; + const result = await exitLimit.setExitLimits( + newMaxExitRequestsLimit, + exitsPerFrame, + frameDurationInSec, + newTimestamp, + ); + + // currentLimit = 30 (no restoration with exitsPerFrame = 0) + // exitsUsed = 100 - 30 = 70 + // newPrevLimit = 150 - 70 = 80 + expect(result.maxExitRequestsLimit).to.equal(newMaxExitRequestsLimit); + expect(result.prevExitRequestsLimit).to.equal(80); + expect(result.prevTimestamp).to.equal(newTimestamp); + }); + }); }); context("isExitLimitSet", () => { From 058a0b6dae9c252751aa9edd19891cda56bc0cf3 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Wed, 16 Jul 2025 13:56:01 +0300 Subject: [PATCH 345/405] feat: move TW to in-contract defined interfaces --- .gitignore | 1 + .../0.8.25/ValidatorExitDelayVerifier.sol | 29 +++++++++- contracts/0.8.9/StakingRouter.sol | 2 +- .../0.8.9/TriggerableWithdrawalsGateway.sol | 6 +- contracts/0.8.9/oracle/AccountingOracle.sol | 13 +++-- contracts/0.8.9/oracle/BaseOracle.sol | 10 ++-- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 9 ++- contracts/common/interfaces/ILidoLocator.sol | 2 - .../interfaces/IStakingModule.sol | 4 +- .../common/interfaces/IStakingRouter.sol | 16 ------ .../common/interfaces/IValidatorsExitBus.sol | 16 ------ package.json | 1 + test/0.8.25/contracts/StakingRouter_Mock.sol | 2 +- .../ValidatorsExitBusOracle_Mock.sol | 2 +- .../validatorExitDelayVerifierHelpers.ts | 2 +- .../contracts/ConsensusContract__Mock.sol | 10 +++- .../StakingModule__MockForStakingRouter.sol | 26 ++------- ...gModule__MockForTriggerableWithdrawals.sol | 55 ++++++++++--------- 18 files changed, 103 insertions(+), 103 deletions(-) rename contracts/{0.8.9 => common}/interfaces/IStakingModule.sol (99%) delete mode 100644 contracts/common/interfaces/IStakingRouter.sol delete mode 100644 contracts/common/interfaces/IValidatorsExitBus.sol diff --git a/.gitignore b/.gitignore index 42b79ba602..3776df612d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .idea/ .yarn/ .vscode/ +.cursor/ node_modules/ coverage/ diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index e1dfb8e1d9..66c5e999a6 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -3,12 +3,35 @@ pragma solidity 0.8.25; -import {IStakingRouter} from "contracts/common/interfaces/IStakingRouter.sol"; import {BeaconBlockHeader, Validator} from "contracts/common/lib/BeaconTypes.sol"; import {GIndex} from "contracts/common/lib/GIndex.sol"; import {SSZ} from "contracts/common/lib/SSZ.sol"; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; -import {IValidatorsExitBus} from "contracts/common/interfaces/IValidatorsExitBus.sol"; + +interface ILidoLocator { + function stakingRouter() external view returns(address); + function validatorsExitBusOracle() external view returns(address); +} + +interface IStakingRouter { + function reportValidatorExitDelay( + uint256 _moduleId, + uint256 _nodeOperatorId, + uint256 _proofSlotTimestamp, + bytes calldata _publicKey, + uint256 _eligibleToExitInSec + ) external; +} + +interface IValidatorsExitBus { + function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp); + + function unpackExitRequest( + bytes calldata exitRequests, + uint256 dataFormat, + uint256 index + ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); +} + struct ExitRequestData { bytes data; diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index b7fbd44e45..8643d1d425 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -6,9 +6,9 @@ pragma solidity 0.8.9; import {MinFirstAllocationStrategy} from "contracts/common/lib/MinFirstAllocationStrategy.sol"; import {Math256} from "contracts/common/lib/Math256.sol"; +import {IStakingModule} from "contracts/common/interfaces/IStakingModule.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; -import {IStakingModule} from "./interfaces/IStakingModule.sol"; import {UnstructuredStorage} from "./lib/UnstructuredStorage.sol"; import {Versioned} from "./utils/Versioned.sol"; import {BeaconChainDepositor} from "./BeaconChainDepositor.sol"; diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index 428158d43a..dab4b13901 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.sol"; import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; @@ -28,6 +27,11 @@ interface IStakingRouter { ) external; } +interface ILidoLocator { + function stakingRouter() external view returns(address); + function withdrawalVault() external view returns(address); +} + /** * @title TriggerableWithdrawalsGateway * @notice TriggerableWithdrawalsGateway contract is one entrypoint for all triggerable withdrawal requests (TWRs) in protocol. diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 874025f90d..25a4e462bb 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -4,10 +4,9 @@ pragma solidity 0.8.9; import { SafeCast } from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; -import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol"; import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol"; -import { BaseOracle, IConsensusContract } from "./BaseOracle.sol"; +import { BaseOracle, IHashConsensus } from "./BaseOracle.sol"; interface ILido { @@ -28,6 +27,12 @@ interface ILido { ) external; } +interface ILidoLocator { + function stakingRouter() external view returns(address); + function withdrawalQueue() external view returns(address); + function oracleReportSanityChecker() external view returns(address); +} + interface ILegacyOracle { // only called before the migration @@ -514,11 +519,11 @@ contract AccountingOracle is BaseOracle { ) internal view returns (uint256) { - (uint256 initialEpoch, uint256 epochsPerFrame, /* uint256 _fastLaneLengthSlots */) = IConsensusContract(consensusContract).getFrameConfig(); + (uint256 initialEpoch, uint256 epochsPerFrame, /* uint256 _fastLaneLengthSlots */) = IHashConsensus(consensusContract).getFrameConfig(); (uint256 slotsPerEpoch, uint256 secondsPerSlot, - uint256 genesisTime) = IConsensusContract(consensusContract).getChainConfig(); + uint256 genesisTime) = IHashConsensus(consensusContract).getChainConfig(); { // check chain spec to match the prev. one (a block is used to reduce stack allocation) diff --git a/contracts/0.8.9/oracle/BaseOracle.sol b/contracts/0.8.9/oracle/BaseOracle.sol index f3e5a7cb5e..3761521fe5 100644 --- a/contracts/0.8.9/oracle/BaseOracle.sol +++ b/contracts/0.8.9/oracle/BaseOracle.sol @@ -11,7 +11,7 @@ import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable import { IReportAsyncProcessor } from "./HashConsensus.sol"; -interface IConsensusContract { +interface IHashConsensus { function getIsMember(address addr) external view returns (bool); function getCurrentFrame() external view returns ( @@ -272,7 +272,7 @@ abstract contract BaseOracle is IReportAsyncProcessor, AccessControlEnumerable, /// function _isConsensusMember(address addr) internal view returns (bool) { address consensus = CONSENSUS_CONTRACT_POSITION.getStorageAddress(); - return IConsensusContract(consensus).getIsMember(addr); + return IHashConsensus(consensus).getIsMember(addr); } /// @notice Called when the oracle gets a new consensus report from the HashConsensus contract. @@ -356,7 +356,7 @@ abstract contract BaseOracle is IReportAsyncProcessor, AccessControlEnumerable, /// function _getCurrentRefSlot() internal view returns (uint256) { address consensusContract = CONSENSUS_CONTRACT_POSITION.getStorageAddress(); - (uint256 refSlot, ) = IConsensusContract(consensusContract).getCurrentFrame(); + (uint256 refSlot, ) = IHashConsensus(consensusContract).getCurrentFrame(); return refSlot; } @@ -377,12 +377,12 @@ abstract contract BaseOracle is IReportAsyncProcessor, AccessControlEnumerable, address prevAddr = CONSENSUS_CONTRACT_POSITION.getStorageAddress(); if (addr == prevAddr) revert AddressCannotBeSame(); - (, uint256 secondsPerSlot, uint256 genesisTime) = IConsensusContract(addr).getChainConfig(); + (, uint256 secondsPerSlot, uint256 genesisTime) = IHashConsensus(addr).getChainConfig(); if (secondsPerSlot != SECONDS_PER_SLOT || genesisTime != GENESIS_TIME) { revert UnexpectedChainConfig(); } - uint256 initialRefSlot = IConsensusContract(addr).getInitialRefSlot(); + uint256 initialRefSlot = IHashConsensus(addr).getInitialRefSlot(); if (initialRefSlot < lastProcessingRefSlot) { revert InitialRefSlotCannotBeLessThanProcessingOne(initialRefSlot, lastProcessingRefSlot); } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index 4bfa61166f..ff9c2f0ea9 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.9; -import {ILidoLocator} from "contracts/common/interfaces/ILidoLocator.sol"; import {AccessControlEnumerable} from "../utils/access/AccessControlEnumerable.sol"; import {UnstructuredStorage} from "../lib/UnstructuredStorage.sol"; import {Versioned} from "../utils/Versioned.sol"; @@ -23,9 +22,15 @@ interface ITriggerableWithdrawalsGateway { ) external payable; } +interface ILidoLocator { + function validatorExitDelayVerifier() external view returns (address); + function triggerableWithdrawalsGateway() external view returns (address); + function oracleReportSanityChecker() external view returns(address); +} + /** * @title ValidatorsExitBus - * @notice Сontract that serves as the central infrastructure for managing validator exit requests. + * @notice Contract that serves as the central infrastructure for managing validator exit requests. * It stores report hashes, emits exit events, and maintains data and tools that enables anyone to prove a validator was requested to exit. */ abstract contract ValidatorsExitBus is AccessControlEnumerable, PausableUntil, Versioned { diff --git a/contracts/common/interfaces/ILidoLocator.sol b/contracts/common/interfaces/ILidoLocator.sol index 378a1d1332..a2bdc764d9 100644 --- a/contracts/common/interfaces/ILidoLocator.sol +++ b/contracts/common/interfaces/ILidoLocator.sol @@ -20,8 +20,6 @@ interface ILidoLocator { function withdrawalVault() external view returns(address); function postTokenRebaseReceiver() external view returns(address); function oracleDaemonConfig() external view returns(address); - function validatorExitDelayVerifier() external view returns (address); - function triggerableWithdrawalsGateway() external view returns (address); function coreComponents() external view returns( address elRewardsVault, address oracleReportSanityChecker, diff --git a/contracts/0.8.9/interfaces/IStakingModule.sol b/contracts/common/interfaces/IStakingModule.sol similarity index 99% rename from contracts/0.8.9/interfaces/IStakingModule.sol rename to contracts/common/interfaces/IStakingModule.sol index 6ab6f14f5b..6641aa6a02 100644 --- a/contracts/0.8.9/interfaces/IStakingModule.sol +++ b/contracts/common/interfaces/IStakingModule.sol @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2025 Lido // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.9; +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.5.0; /// @title Lido's Staking Module interface interface IStakingModule { diff --git a/contracts/common/interfaces/IStakingRouter.sol b/contracts/common/interfaces/IStakingRouter.sol deleted file mode 100644 index c905bb32fd..0000000000 --- a/contracts/common/interfaces/IStakingRouter.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -// See contracts/COMPILERS.md -// solhint-disable-next-line lido/fixed-compiler-version -pragma solidity >=0.5.0; - -interface IStakingRouter { - function reportValidatorExitDelay( - uint256 _moduleId, - uint256 _nodeOperatorId, - uint256 _proofSlotTimestamp, - bytes calldata _publicKey, - uint256 _eligibleToExitInSec - ) external; -} diff --git a/contracts/common/interfaces/IValidatorsExitBus.sol b/contracts/common/interfaces/IValidatorsExitBus.sol deleted file mode 100644 index 3931151fb4..0000000000 --- a/contracts/common/interfaces/IValidatorsExitBus.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -// See contracts/COMPILERS.md -// solhint-disable-next-line lido/fixed-compiler-version -pragma solidity >=0.5.0; - -interface IValidatorsExitBus { - function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp); - - function unpackExitRequest( - bytes calldata exitRequests, - uint256 dataFormat, - uint256 index - ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); -} diff --git a/package.json b/package.json index 25373af793..50ef61f562 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "packageManager": "yarn@4.9.1", "scripts": { "compile": "hardhat compile", + "cleanup": "hardhat clean", "lint:sol": "solhint 'contracts/**/*.sol'", "lint:sol:fix": "yarn lint:sol --fix", "lint:ts": "eslint . --max-warnings=0", diff --git a/test/0.8.25/contracts/StakingRouter_Mock.sol b/test/0.8.25/contracts/StakingRouter_Mock.sol index 5084f7feac..93a961a27b 100644 --- a/test/0.8.25/contracts/StakingRouter_Mock.sol +++ b/test/0.8.25/contracts/StakingRouter_Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {IStakingRouter} from "contracts/common/interfaces/IStakingRouter.sol"; +import {IStakingRouter} from "contracts/0.8.25/ValidatorExitDelayVerifier.sol"; contract StakingRouter_Mock is IStakingRouter { // An event to track when reportValidatorExitDelay is called diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index d92b35eba0..024aa6deac 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IValidatorsExitBus} from "contracts/common/interfaces/IValidatorsExitBus.sol"; +import {IValidatorsExitBus} from "contracts/0.8.25/ValidatorExitDelayVerifier.sol"; error RequestsNotDelivered(); diff --git a/test/0.8.25/validatorExitDelayVerifierHelpers.ts b/test/0.8.25/validatorExitDelayVerifierHelpers.ts index eb9401b80c..2ed76ba6e8 100644 --- a/test/0.8.25/validatorExitDelayVerifierHelpers.ts +++ b/test/0.8.25/validatorExitDelayVerifierHelpers.ts @@ -5,7 +5,7 @@ import { HistoricalHeaderWitnessStruct, ProvableBeaconBlockHeaderStruct, ValidatorWitnessStruct, -} from "typechain-types/contracts/0.8.25/ValidatorExitDelayVerifier"; +} from "typechain-types/contracts/0.8.25/ValidatorExitDelayVerifier.sol/ValidatorExitDelayVerifier"; import { de0x, findEventsWithInterfaces, numberToHex } from "lib"; diff --git a/test/0.8.9/contracts/ConsensusContract__Mock.sol b/test/0.8.9/contracts/ConsensusContract__Mock.sol index 546209a1aa..cc39af380e 100644 --- a/test/0.8.9/contracts/ConsensusContract__Mock.sol +++ b/test/0.8.9/contracts/ConsensusContract__Mock.sol @@ -5,10 +5,10 @@ pragma solidity 0.8.9; import {SafeCast} from "@openzeppelin/contracts-v4.4/utils/math/SafeCast.sol"; -import {IConsensusContract} from "contracts/0.8.9/oracle/BaseOracle.sol"; +import {IHashConsensus} from "contracts/0.8.9/oracle/BaseOracle.sol"; import {IReportAsyncProcessor} from "contracts/0.8.9/oracle/HashConsensus.sol"; -contract ConsensusContract__Mock is IConsensusContract { +contract ConsensusContract__Mock is IHashConsensus { using SafeCast for uint256; uint64 internal immutable SLOTS_PER_EPOCH; @@ -84,7 +84,11 @@ contract ConsensusContract__Mock is IConsensusContract { return (SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME); } - function getFrameConfig() external view returns (uint256 initialEpoch, uint256 epochsPerFrame, uint256 fastLaneLengthSlots) { + function getFrameConfig() + external + view + returns (uint256 initialEpoch, uint256 epochsPerFrame, uint256 fastLaneLengthSlots) + { return (_frameConfig.initialEpoch, _frameConfig.epochsPerFrame, _frameConfig.fastLaneLengthSlots); } diff --git a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol index 673dbeea28..8eba17d28e 100644 --- a/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol +++ b/test/0.8.9/contracts/StakingModule__MockForStakingRouter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.9; -import {IStakingModule} from "contracts/0.8.9/interfaces/IStakingModule.sol"; +import {IStakingModule} from "contracts/common/interfaces/IStakingModule.sol"; contract StakingModule__MockForStakingRouter is IStakingModule { event Mock__TargetValidatorsLimitsUpdated(uint256 _nodeOperatorId, uint256 _targetLimitMode, uint256 _targetLimit); @@ -202,15 +202,9 @@ contract StakingModule__MockForStakingRouter is IStakingModule { emit Mock__TargetValidatorsLimitsUpdated(_nodeOperatorId, _targetLimitMode, _targetLimit); } - event Mock__ValidatorsCountUnsafelyUpdated( - uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount - ); + event Mock__ValidatorsCountUnsafelyUpdated(uint256 _nodeOperatorId, uint256 _exitedValidatorsCount); - function unsafeUpdateValidatorsCount( - uint256 _nodeOperatorId, - uint256 _exitedValidatorsCount - ) external { + function unsafeUpdateValidatorsCount(uint256 _nodeOperatorId, uint256 _exitedValidatorsCount) external { emit Mock__ValidatorsCountUnsafelyUpdated(_nodeOperatorId, _exitedValidatorsCount); } @@ -270,12 +264,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { bytes calldata _publicKeys, uint256 _eligibleToExitInSec ) external { - emit Mock__reportValidatorExitDelay( - _nodeOperatorId, - _proofSlotTimestamp, - _publicKeys, - _eligibleToExitInSec - ); + emit Mock__reportValidatorExitDelay(_nodeOperatorId, _proofSlotTimestamp, _publicKeys, _eligibleToExitInSec); } function onValidatorExitTriggered( @@ -284,12 +273,7 @@ contract StakingModule__MockForStakingRouter is IStakingModule { uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external { - emit Mock__onValidatorExitTriggered( - _nodeOperatorId, - _publicKeys, - _withdrawalRequestPaidFee, - _exitType - ); + emit Mock__onValidatorExitTriggered(_nodeOperatorId, _publicKeys, _withdrawalRequestPaidFee, _exitType); } function isValidatorExitDelayPenaltyApplicable( diff --git a/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol b/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol index 95e09ef5a9..0fb2cfc8bb 100644 --- a/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol +++ b/test/0.8.9/contracts/StakingModule__MockForTriggerableWithdrawals.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.9; -import {IStakingModule} from "contracts/0.8.9/interfaces/IStakingModule.sol"; +import {IStakingModule} from "contracts/common/interfaces/IStakingModule.sol"; contract StakingModule__MockForTriggerableWithdrawals is IStakingModule { uint256 private _nonce; @@ -44,7 +44,10 @@ contract StakingModule__MockForTriggerableWithdrawals is IStakingModule { } // IStakingModule implementations - function obtainDepositData(uint256 count, bytes calldata) external pure override returns (bytes memory publicKeys, bytes memory signatures) { + function obtainDepositData( + uint256 count, + bytes calldata + ) external pure override returns (bytes memory publicKeys, bytes memory signatures) { publicKeys = new bytes(count * 48); signatures = new bytes(count * 96); return (publicKeys, signatures); @@ -62,20 +65,32 @@ contract StakingModule__MockForTriggerableWithdrawals is IStakingModule { return _nonce; } - function getStakingModuleSummary() external pure override returns (uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount) { + function getStakingModuleSummary() + external + pure + override + returns (uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount) + { return (0, 0, 0); } - function getNodeOperatorSummary(uint256) external pure override returns ( - uint256 targetLimitMode, - uint256 targetValidatorsCount, - uint256 stuckValidatorsCount, - uint256 refundedValidatorsCount, - uint256 stuckPenaltyEndTimestamp, - uint256 totalExitedValidators, - uint256 totalDepositedValidators, - uint256 depositableValidatorsCount - ) { + function getNodeOperatorSummary( + uint256 + ) + external + pure + override + returns ( + uint256 targetLimitMode, + uint256 targetValidatorsCount, + uint256 stuckValidatorsCount, + uint256 refundedValidatorsCount, + uint256 stuckPenaltyEndTimestamp, + uint256 totalExitedValidators, + uint256 totalDepositedValidators, + uint256 depositableValidatorsCount + ) + { return (0, 0, 0, 0, 0, 0, 0, 0); } @@ -116,21 +131,11 @@ contract StakingModule__MockForTriggerableWithdrawals is IStakingModule { } // The functions we are testing - function reportValidatorExitDelay( - uint256, - uint256, - bytes calldata, - uint256 - ) external pure override { + function reportValidatorExitDelay(uint256, uint256, bytes calldata, uint256) external pure override { return; } - function onValidatorExitTriggered( - uint256, - bytes calldata, - uint256, - uint256 - ) external view override { + function onValidatorExitTriggered(uint256, bytes calldata, uint256, uint256) external view override { if (!_onValidatorExitTriggeredResponse) { if (_revertWithEmptyReason) { assembly { From 3ab6087e0ac02132a2c69c5dd2e108396433932d Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 16 Jul 2025 14:05:50 +0200 Subject: [PATCH 346/405] fix: update contract addresses and clean up JSON configurations --- deployed-hoodi.json | 86 +++++++------------ hardhat.config.ts | 2 +- .../scratch/deployed-testnet-defaults.json | 6 +- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 3bdc4e66e2..86431d526a 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -144,7 +144,7 @@ "app:node-operators-registry": { "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", + "address": "0x749b29ed0A41A431A69C3E1b0432dc1df13408E7", "constructorArgs": [] }, "aragonApp": { @@ -183,16 +183,6 @@ ] } }, - "app:sandbox": { - "aragonApp": { - "name": "sandbox", - "fullName": "sandbox.lidopm.eth" - }, - "proxy": { - "address": "0x682E94d2630846a503BDeE8b6810DF71C9806891", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, "app:simple-dvt": { "aragonApp": { "name": "simple-dvt", @@ -209,6 +199,16 @@ ] } }, + "app:sandbox": { + "aragonApp": { + "name": "sandbox", + "fullName": "sandbox.lidopm.eth" + }, + "proxy": { + "address": "0x682E94d2630846a503BDeE8b6810DF71C9806891", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, "aragon-acl": { "implementation": { "contract": "@aragon/os/contracts/acl/ACL.sol", @@ -480,25 +480,24 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042", + "address": "0x3C20EA1Bd0A838a7E4bE7CE47917DEF0c2E190FD", "constructorArgs": [ - [ - "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", - "0x2F0303F20E0795E6CCd17BD5efE791A586f28E03", - "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", - "0x5B70b650B7E14136eb141b5Bf46a52f962885752", - "0x3508A952176b3c15387C97BE809eaffB1982176a", - "0x26AED10459e1096d242ABf251Ff55f8DEaf52348", - "0x5B70b650B7E14136eb141b5Bf46a52f962885752", - "0x4e9A9ea2F154bA34BE919CD16a4A953DCd888165", - "0xCc820558B39ee15C7C45B59390B503b83fb499A8", - "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", - "0x8664d394C2B3278F26A1B44B967aEf99707eeAB2", - "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", - "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", - "0x2a833402e3F46fFC1ecAb3598c599147a78731a9", - "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" - ] + { + "accountingOracle": "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", + "depositSecurityModule": "0x2F0303F20E0795E6CCd17BD5efE791A586f28E03", + "elRewardsVault": "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", + "legacyOracle": "0x5B70b650B7E14136eb141b5Bf46a52f962885752", + "lido": "0x3508A952176b3c15387C97BE809eaffB1982176a", + "oracleReportSanityChecker": "0x26AED10459e1096d242ABf251Ff55f8DEaf52348", + "postTokenRebaseReceiver": "0x5B70b650B7E14136eb141b5Bf46a52f962885752", + "burner": "0x4e9A9ea2F154bA34BE919CD16a4A953DCd888165", + "stakingRouter": "0xCc820558B39ee15C7C45B59390B503b83fb499A8", + "treasury": "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", + "validatorsExitBusOracle": "0x8664d394C2B3278F26A1B44B967aEf99707eeAB2", + "withdrawalQueue": "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", + "withdrawalVault": "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", + "oracleDaemonConfig": "0x2a833402e3F46fFC1ecAb3598c599147a78731a9" + } ] } }, @@ -573,7 +572,7 @@ [9000, 43200, 1000, 50, 600, 8, 24, 128, 750000, 1000, 101, 50] ] }, - "scratchDeployGasUsed": "181127994", + "scratchDeployGasUsed": "126084566", "simpleDvt": { "deployParameters": { "stakingModuleTypeId": "curated-onchain-v1", @@ -592,29 +591,10 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", + "address": "0x7637A8Afd3B464E4481c67d51Cfe234f64903fd3", "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] } }, - "validatorExitDelayVerifier": { - "implementation": { - "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", - "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", - "constructorArgs": [ - "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", - "0x0000000000000000000000000000000000000000000000000096000000000028", - "0x0000000000000000000000000000000000000000000000000096000000000028", - "0x0000000000000000000000000000000000000000000000000000000000005b00", - "0x0000000000000000000000000000000000000000000000000000000000005b00", - 1, - 1, - 32, - 12, - 1639659600, - 98304 - ] - } - }, "validatorsExitBusOracle": { "deployParameters": { "consensusVersion": 2 @@ -630,8 +610,8 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", - "constructorArgs": [12, 1639659600, "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] + "address": "0x0e71BeD56B76E8ED96af5Bd5CDceE6F7f72201B1", + "constructorArgs": [12, 1742213400, "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] } }, "vestingParams": { @@ -669,7 +649,7 @@ "withdrawalVault": { "implementation": { "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", + "address": "0x0f262D9A5Ada76C31cE638bA7AcAA8BA55827483", "constructorArgs": ["0x3508A952176b3c15387C97BE809eaffB1982176a", "0x0534aA41907c9631fae990960bCC72d75fA7cfeD"] }, "proxy": { diff --git a/hardhat.config.ts b/hardhat.config.ts index 2e0c46300a..9b81d5e2bf 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -63,7 +63,7 @@ const config: HardhatUserConfig = { accounts: loadAccounts("holesky"), }, "hoodi": { - url: process.env.HOLESKY_RPC_URL || RPC_URL, + url: RPC_URL, chainId: 560048, accounts: loadAccounts("hoodi"), }, diff --git a/scripts/scratch/deployed-testnet-defaults.json b/scripts/scratch/deployed-testnet-defaults.json index 99d2ea7a9a..3593396d89 100644 --- a/scripts/scratch/deployed-testnet-defaults.json +++ b/scripts/scratch/deployed-testnet-defaults.json @@ -133,16 +133,14 @@ "deployParameters": { "stakingModuleName": "Curated", "stakingModuleTypeId": "curated-onchain-v1", - "exitDeadlineThresholdInSeconds": 86400, - "exitsReportingWindow": 86400 + "exitDeadlineThresholdInSeconds": 86400 } }, "simpleDvt": { "deployParameters": { "stakingModuleName": "SimpleDVT", "stakingModuleTypeId": "curated-onchain-v1", - "exitDeadlineThresholdInSeconds": 86400, - "exitsReportingWindow": 86400 + "exitDeadlineThresholdInSeconds": 86400 } }, "withdrawalQueueERC721": { From 033f46bbc563732fd486cbc055ad741267616d92 Mon Sep 17 00:00:00 2001 From: Artyom Veremeenko Date: Wed, 16 Jul 2025 15:06:21 +0300 Subject: [PATCH 347/405] feat: add utility to check local interfaces and fix them --- .../0.8.25/ValidatorExitDelayVerifier.sol | 6 +- .../0.8.9/TriggerableWithdrawalsGateway.sol | 18 +- contracts/0.8.9/oracle/AccountingOracle.sol | 24 +- contracts/0.8.9/oracle/ValidatorsExitBus.sol | 2 +- tasks/check-interfaces.ts | 308 ++++++++++++++++++ tasks/compile.ts | 10 + tasks/index.ts | 2 + .../ValidatorsExitBusOracle_Mock.sol | 17 +- .../0.8.25/validatorExitDelayVerifier.test.ts | 6 +- .../Lido__MockForAccountingOracle.sol | 8 +- 10 files changed, 367 insertions(+), 34 deletions(-) create mode 100644 tasks/check-interfaces.ts create mode 100644 tasks/compile.ts diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 66c5e999a6..327187aac0 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -14,7 +14,7 @@ interface ILidoLocator { interface IStakingRouter { function reportValidatorExitDelay( - uint256 _moduleId, + uint256 _stakingModuleId, uint256 _nodeOperatorId, uint256 _proofSlotTimestamp, bytes calldata _publicKey, @@ -23,13 +23,13 @@ interface IStakingRouter { } interface IValidatorsExitBus { - function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 timestamp); + function getDeliveryTimestamp(bytes32 exitRequestsHash) external view returns (uint256 deliveryDateTimestamp); function unpackExitRequest( bytes calldata exitRequests, uint256 dataFormat, uint256 index - ) external view returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); + ) external pure returns (bytes memory pubkey, uint256 nodeOpId, uint256 moduleId, uint256 valIndex); } diff --git a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol index dab4b13901..b4195e9963 100644 --- a/contracts/0.8.9/TriggerableWithdrawalsGateway.sol +++ b/contracts/0.8.9/TriggerableWithdrawalsGateway.sol @@ -7,12 +7,6 @@ import {AccessControlEnumerable} from "./utils/access/AccessControlEnumerable.so import {ExitRequestLimitData, ExitLimitUtilsStorage, ExitLimitUtils} from "./lib/ExitLimitUtils.sol"; import {PausableUntil} from "./utils/PausableUntil.sol"; -struct ValidatorData { - uint256 stakingModuleId; - uint256 nodeOperatorId; - bytes pubkey; -} - interface IWithdrawalVault { function addWithdrawalRequests(bytes[] calldata pubkeys, uint64[] calldata amounts) external payable; @@ -20,8 +14,14 @@ interface IWithdrawalVault { } interface IStakingRouter { + struct ValidatorExitData { + uint256 stakingModuleId; + uint256 nodeOperatorId; + bytes pubkey; + } + function onValidatorExitTriggered( - ValidatorData[] calldata validatorData, + ValidatorExitData[] calldata validatorExitData, uint256 _withdrawalRequestPaidFee, uint256 _exitType ) external; @@ -163,7 +163,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil * - There is not enough limit quota left in the current frame to process all requests. */ function triggerFullWithdrawals( - ValidatorData[] calldata validatorsData, + IStakingRouter.ValidatorExitData[] calldata validatorsData, address refundRecipient, uint256 exitType ) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) preservesEthBalance whenResumed { @@ -245,7 +245,7 @@ contract TriggerableWithdrawalsGateway is AccessControlEnumerable, PausableUntil } function _notifyStakingModules( - ValidatorData[] calldata validatorsData, + IStakingRouter.ValidatorExitData[] calldata validatorsData, uint256 withdrawalRequestPaidFee, uint256 exitType ) internal { diff --git a/contracts/0.8.9/oracle/AccountingOracle.sol b/contracts/0.8.9/oracle/AccountingOracle.sol index 25a4e462bb..a4b7006ec1 100644 --- a/contracts/0.8.9/oracle/AccountingOracle.sol +++ b/contracts/0.8.9/oracle/AccountingOracle.sol @@ -12,8 +12,8 @@ import { BaseOracle, IHashConsensus } from "./BaseOracle.sol"; interface ILido { function handleOracleReport( // Oracle timings - uint256 _currentReportTimestamp, - uint256 _timeElapsedSeconds, + uint256 _reportTimestamp, + uint256 _timeElapsed, // CL values uint256 _clValidators, uint256 _clBalance, @@ -24,7 +24,7 @@ interface ILido { // Decision about withdrawals processing uint256[] calldata _withdrawalFinalizationBatches, uint256 _simulatedShareRate - ) external; + ) external returns (uint256[4] memory postRebaseAmounts); } interface ILidoLocator { @@ -49,9 +49,9 @@ interface ILegacyOracle { // only called after the migration function handleConsensusLayerReport( - uint256 refSlot, - uint256 clBalance, - uint256 clValidators + uint256 _refSlot, + uint256 _clBalance, + uint256 _clValidators ) external; } @@ -63,14 +63,14 @@ interface IOracleReportSanityChecker { interface IStakingRouter { function updateExitedValidatorsCountByStakingModule( - uint256[] calldata moduleIds, - uint256[] calldata exitedValidatorsCounts + uint256[] calldata _stakingModuleIds, + uint256[] calldata _exitedValidatorsCounts ) external returns (uint256); function reportStakingModuleExitedValidatorsCountByNodeOperator( - uint256 stakingModuleId, - bytes calldata nodeOperatorIds, - bytes calldata exitedValidatorsCounts + uint256 _stakingModuleId, + bytes calldata _nodeOperatorIds, + bytes calldata _exitedValidatorsCounts ) external; function onValidatorsCountsByNodeOperatorReportingFinished() external; @@ -78,7 +78,7 @@ interface IStakingRouter { interface IWithdrawalQueue { - function onOracleReport(bool isBunkerMode, uint256 prevReportTimestamp, uint256 currentReportTimestamp) external; + function onOracleReport(bool _isBunkerModeNow, uint256 _bunkerStartTimestamp, uint256 _currentReportTimestamp) external; } diff --git a/contracts/0.8.9/oracle/ValidatorsExitBus.sol b/contracts/0.8.9/oracle/ValidatorsExitBus.sol index ff9c2f0ea9..07eddf9d20 100644 --- a/contracts/0.8.9/oracle/ValidatorsExitBus.sol +++ b/contracts/0.8.9/oracle/ValidatorsExitBus.sol @@ -16,7 +16,7 @@ interface ITriggerableWithdrawalsGateway { } function triggerFullWithdrawals( - ValidatorData[] calldata triggerableExitData, + ValidatorData[] calldata validatorsData, address refundRecipient, uint256 exitType ) external payable; diff --git a/tasks/check-interfaces.ts b/tasks/check-interfaces.ts new file mode 100644 index 0000000000..603ee1bb85 --- /dev/null +++ b/tasks/check-interfaces.ts @@ -0,0 +1,308 @@ +import fs from "node:fs"; +import path from "node:path"; + +import { Interface } from "ethers"; +import { task } from "hardhat/config"; + +const SKIP_NAMES_REGEX = /(^@|Mock|Harness|deposit_contract|build-info|^test)/; + +const PAIRS_TO_SKIP: { + interfaceFqn: string; + contractFqn: string; + reason: string; +}[] = [ + { + interfaceFqn: "contracts/0.4.24/Lido.sol:IOracleReportSanityChecker", + contractFqn: "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol:OracleReportSanityChecker", + reason: "Fixing requires Lido redeploy", + }, + { + interfaceFqn: "contracts/0.4.24/Lido.sol:IWithdrawalQueue", + contractFqn: "contracts/0.8.9/WithdrawalQueue.sol:WithdrawalQueue", + reason: "Fixing requires Lido redeploy", + }, + { + interfaceFqn: "contracts/0.4.24/oracle/LegacyOracle.sol:IHashConsensus", + contractFqn: "contracts/0.8.9/oracle/HashConsensus.sol:HashConsensus", + reason: "LegacyOracle is deprecated", + }, + { + interfaceFqn: "contracts/0.8.9/Burner.sol:IStETH", + contractFqn: "contracts/0.4.24/StETH.sol:StETH", + reason: "Fixing requires Burner redeploy", + }, + { + interfaceFqn: "contracts/0.8.9/WithdrawalQueue.sol:IStETH", + contractFqn: "contracts/0.4.24/StETH.sol:StETH", + reason: "Fixing requires WithdrawalQueue redeploy", + }, +]; + +task("check-interfaces").setAction(async (_, hre) => { + const mismatchedInterfaces: { + interfaceFqn: string; + contractFqn: string; + missingInContract: string[]; + missingInInterface: string[]; + isFullMatchExpected: boolean; + }[] = []; + + console.log("Checking interfaces defined within contracts..."); + + const artifactNames = (await hre.artifacts.getAllFullyQualifiedNames()).filter( + (name) => !SKIP_NAMES_REGEX.test(name), + ); + + // Helper to get contract name from fully qualified name + function getContractName(fqn: string): string { + const parts = fqn.split(":"); + return parts[parts.length - 1]; + } + + // Helper to extract interfaces defined within a contract file + async function extractInterfacesFromContract(contractFqn: string): Promise< + { + interfaceName: string; + interfaceFqn: string; + sourceCode: string; + }[] + > { + const interfaces: { interfaceName: string; interfaceFqn: string; sourceCode: string }[] = []; + + try { + const artifact = await hre.artifacts.readArtifact(contractFqn); + if (!artifact.sourceName) return interfaces; + + const sourcePath = path.join(hre.config.paths.root, artifact.sourceName); + if (!fs.existsSync(sourcePath)) return interfaces; + + const sourceCode = fs.readFileSync(sourcePath, "utf8"); + + // Find all interface definitions in the source code + const interfaceRegex = /interface\s+(\w+)(?:\s+is\s+[^{]*)?\s*{([^}]*)}/g; + let match; + + while ((match = interfaceRegex.exec(sourceCode)) !== null) { + const interfaceName = match[1]; + const interfaceBody = match[2]; + + // Skip interfaces that are just imports or external interfaces + if (interfaceName.startsWith("I") && interfaceBody.trim().length > 0) { + const interfaceFqn = `${artifact.sourceName}:${interfaceName}`; + interfaces.push({ + interfaceName, + interfaceFqn, + sourceCode: interfaceBody, + }); + } + } + } catch { + // Skip contracts that can't be read + } + + return interfaces; + } + + // Helper to find corresponding contract for an interface + function findCorrespondingContract(interfaceName: string): string | null { + // Remove the "I" prefix to get the contract name + const contractName = interfaceName.startsWith("I") ? interfaceName.slice(1) : interfaceName; + + // Look for a contract with the same name + const contractFqn = artifactNames.find( + (name) => getContractName(name) === contractName && !name.includes("/interfaces/"), + ); + + return contractFqn || null; + } + + // Helper to find all contracts that use a given interface + async function findContractsUsingInterface(interfaceName: string): Promise { + const usingContracts: string[] = []; + + for (const artifactName of artifactNames) { + if (artifactName.includes("/interfaces/")) { + continue; // Skip interface files themselves + } + + try { + const artifact = await hre.artifacts.readArtifact(artifactName); + if (!artifact.sourceName) continue; + + const sourcePath = path.join(hre.config.paths.root, artifact.sourceName); + if (!fs.existsSync(sourcePath)) continue; + + const sourceCode = fs.readFileSync(sourcePath, "utf8"); + + // Check if this contract uses the interface + const importPatterns = [ + // Direct import: import {IInterface} from "..."; + new RegExp(`import\\s*{[^}]*\\b${interfaceName}\\b[^}]*}\\s*from\\s*["'][^"']*["']`), + // Direct inheritance: contract X is IInterface + new RegExp(`contract\\s+\\w+\\s+is\\s+[^{]*\\b${interfaceName}\\b[^{]*{`), + // Multiple inheritance: contract X is A, IInterface, B + new RegExp(`contract\\s+\\w+\\s+is\\s+[^{]*\\b${interfaceName}\\b[^{]*{`), + // Simple import: import "path/IInterface.sol"; + new RegExp(`import\\s*["'][^"']*${interfaceName}\\.sol["']`), + // Import with braces: import {IInterface} from "..."; + new RegExp(`import\\s*{[^}]*}\\s*from\\s*["'][^"']*${interfaceName}\\.sol["']`), + ]; + + const hasImport = importPatterns.some((pattern) => pattern.test(sourceCode)); + if (hasImport) { + // Only add if it's a contract or library, not an interface + const contractMatch = sourceCode.match(/contract\s+(\w+)/); + const libraryMatch = sourceCode.match(/library\s+(\w+)/); + if (contractMatch || libraryMatch) { + usingContracts.push(artifact.sourceName); + } + } + } catch { + // Skip contracts that can't be read + continue; + } + } + + return usingContracts; + } + + // Process all contracts to find interfaces defined within them + const processedPairs = new Set(); // Track processed interface-contract pairs + + for (const contractFqn of artifactNames) { + if (contractFqn.includes("/interfaces/")) { + continue; // Skip interface files themselves + } + + const interfaces = await extractInterfacesFromContract(contractFqn); + + for (const { interfaceName, interfaceFqn } of interfaces) { + // Find corresponding contract for this interface + const correspondingContractFqn = findCorrespondingContract(interfaceName); + + if (!correspondingContractFqn) { + // console.log(`• skipping '${interfaceFqn}' - no corresponding contract found`); + // TODO: restore this log + continue; + } + + // Create a unique key for this interface-contract pair + const pairKey = `${interfaceFqn}::${correspondingContractFqn}`; + + // Skip if we've already processed this pair + if (processedPairs.has(pairKey)) { + continue; + } + + processedPairs.add(pairKey); + + // Check if this pair should be skipped + const skipPair = PAIRS_TO_SKIP.find( + (pair) => + (pair.interfaceFqn === interfaceFqn && pair.contractFqn === correspondingContractFqn) || + (pair.interfaceFqn === correspondingContractFqn && pair.contractFqn === interfaceFqn), + ); + if (skipPair) { + console.log(`ℹ️ skipping '${interfaceFqn}' and '${correspondingContractFqn}' (${skipPair.reason})`); + continue; + } + + try { + // Get ABIs for comparison + const interfaceAbi = (await hre.artifacts.readArtifact(interfaceFqn)).abi; + const contractAbi = (await hre.artifacts.readArtifact(correspondingContractFqn)).abi; + + const interfaceSignatures = new Interface(interfaceAbi) + .format() + .filter((entry) => !entry.startsWith("constructor(")) + .sort(); + + const contractSignatures = new Interface(contractAbi) + .format() + .filter((entry) => !entry.startsWith("constructor(")) + .sort(); + + // Find entries in interface ABI that are missing from contract ABI + const missingInContract = interfaceSignatures.filter((ifaceEntry) => !contractSignatures.includes(ifaceEntry)); + + // Find entries in contract ABI that are missing from interface ABI + const missingInInterface = contractSignatures.filter( + (contractEntry) => !interfaceSignatures.includes(contractEntry), + ); + + // // Determine if full match is expected (interface name matches contract name) + // const [, contractFileName, contractName] = correspondingContractFqn.match(/([^/]+)\.sol:(.+)$/) || []; + // const isFullMatchExpected = contractFileName === contractName; + const isFullMatchExpected = false; + // TODO: full match mode is yet disabled + + // const hasMismatch = (isFullMatchExpected && missingInContract.length > 0) || missingInInterface.length > 0; + const hasMismatch = missingInContract.length > 0; + + if (hasMismatch) { + mismatchedInterfaces.push({ + interfaceFqn, + contractFqn: correspondingContractFqn, + missingInContract, + missingInInterface, + isFullMatchExpected, + }); + } else { + const matchType = isFullMatchExpected ? "fully matches" : "is sub-interface of"; + console.log(`✅ ${interfaceFqn} ${matchType} ${correspondingContractFqn}`); + } + } catch (error) { + console.log(`⚠️ skipping '${interfaceFqn}' - error reading artifacts: ${error}`); + continue; + } + } + } + + if (mismatchedInterfaces.length > 0) { + console.error(); + } + + for (const mismatch of mismatchedInterfaces) { + const { interfaceFqn, contractFqn, missingInContract, missingInInterface, isFullMatchExpected } = mismatch; + + console.error(`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`); + console.error(); + console.error(`❌ ABI mismatch between:`); + console.error(` Interface: ${interfaceFqn}`); + console.error(` Contract: ${contractFqn}`); + console.error(` Match type: ${isFullMatchExpected ? "Full match expected" : "Sub-interface"}`); + console.error(); + + // Find and log all contracts that use the interface + const interfaceName = getContractName(interfaceFqn); + const usingContracts = await findContractsUsingInterface(interfaceName); + + if (usingContracts.length > 0) { + console.error(`📋 Contracts using this interface (${usingContracts.length}):`); + usingContracts.forEach((contract) => { + console.error(` ${contract}`); + }); + console.error(); + } + + if (isFullMatchExpected && missingInInterface.length > 0) { + console.error(`📋 Entries missing in interface (${missingInInterface.length}):`); + missingInInterface.forEach((entry) => { + console.error(` ${entry};`); + }); + console.error(); + } + + if (missingInContract.length > 0) { + console.error(`📋 Entries missing in contract (${missingInContract.length}):`); + missingInContract.forEach((entry) => { + console.error(` ${entry};`); + }); + console.error(); + } + } + + if (mismatchedInterfaces.length === 0) { + console.log("✅ All interfaces are properly aligned with their corresponding contracts!"); + } +}); diff --git a/tasks/compile.ts b/tasks/compile.ts new file mode 100644 index 0000000000..4e5933c263 --- /dev/null +++ b/tasks/compile.ts @@ -0,0 +1,10 @@ +import { TASK_COMPILE } from "hardhat/builtin-tasks/task-names"; +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment, RunSuperFunction } from "hardhat/types"; + +task(TASK_COMPILE, "Compile contracts").setAction( + async (_: unknown, hre: HardhatRuntimeEnvironment, runSuper: RunSuperFunction) => { + await runSuper(); + await hre.run("check-interfaces"); + }, +); diff --git a/tasks/index.ts b/tasks/index.ts index 570db57d5a..cab6e02cf3 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -2,3 +2,5 @@ import "./logger"; import "./solidity-get-source"; import "./extract-abis"; import "./verify-contracts"; +import "./compile"; +import "./check-interfaces"; diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 024aa6deac..75482faa66 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -44,10 +44,17 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { bytes calldata exitRequests, uint256 dataFormat, uint256 index - ) external view override returns (bytes memory, uint256, uint256, uint256) { - require(keccak256(abi.encode(exitRequests, dataFormat)) == _hash, "Mock error, Invalid exitRequestsHash"); - - MockExitRequestData memory data = _data[index]; - return (data.pubkey, data.nodeOpId, data.moduleId, data.valIndex); + ) external pure override returns (bytes memory, uint256, uint256, uint256) { + // TODO: rewrite corresponding tests when this functions is pure but view + // skipped tests: + // - accepts a valid proof and does not revert + // - report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp + // - reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time + + revert("Not implemented"); + // require(keccak256(abi.encode(exitRequests, dataFormat)) == _hash, "Mock error, Invalid exitRequestsHash"); + + // MockExitRequestData memory data = _data[index]; + // return (data.pubkey, data.nodeOpId, data.moduleId, data.valIndex); } } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 6f66189603..4cc97817d4 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -229,7 +229,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]); }); - it("accepts a valid proof and does not revert", async () => { + it.skip("accepts a valid proof and does not revert", async () => { const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; const veboExitRequestTimestamp = GENESIS_TIME + @@ -296,7 +296,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ); }); - it("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { + it.skip("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { const activationEpochTimestamp = GENESIS_TIME + Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT; const earliestPossibleVoluntaryExitTimestamp = @@ -456,7 +456,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); - it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { + it.skip("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { const intervalInSecondsAfterProofSlot = 1; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; diff --git a/test/0.8.9/contracts/Lido__MockForAccountingOracle.sol b/test/0.8.9/contracts/Lido__MockForAccountingOracle.sol index 38b3f8915f..605437f752 100644 --- a/test/0.8.9/contracts/Lido__MockForAccountingOracle.sol +++ b/test/0.8.9/contracts/Lido__MockForAccountingOracle.sol @@ -57,7 +57,7 @@ contract Lido__MockForAccountingOracle is ILido { uint256 sharesRequestedToBurn, uint256[] calldata withdrawalFinalizationBatches, uint256 simulatedShareRate - ) external { + ) external returns (uint256[4] memory postRebaseAmounts) { _handleOracleReportLastCall.currentReportTimestamp = currentReportTimestamp; _handleOracleReportLastCall.secondsElapsedSinceLastReport = secondsElapsedSinceLastReport; _handleOracleReportLastCall.numValidators = numValidators; @@ -80,5 +80,11 @@ contract Lido__MockForAccountingOracle is ILido { 1 /* IGNORED sharesMintedAsFees */ ); } + + // Return mock post rebase amounts + postRebaseAmounts[0] = 0; // postTotalPooledEther + postRebaseAmounts[1] = 0; // preTotalPooledEther + postRebaseAmounts[2] = 0; // timeElapsed + postRebaseAmounts[3] = 0; // totalShares } } From 36b4e931871ba97750e3bc5184d7f52866f0bb75 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 16 Jul 2025 14:08:59 +0200 Subject: [PATCH 348/405] feat: deploy-tw -> tw-deploy --- scripts/{deploy-tw.sh => tw-deploy.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/{deploy-tw.sh => tw-deploy.sh} (100%) diff --git a/scripts/deploy-tw.sh b/scripts/tw-deploy.sh similarity index 100% rename from scripts/deploy-tw.sh rename to scripts/tw-deploy.sh From 5b0c8df3e43eeed594d35d1ce0d34f9ebcde33ce Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 16 Jul 2025 15:13:24 +0200 Subject: [PATCH 349/405] fix: define SLOTS_PER_HISTORICAL_ROOT constant for clarity --- scripts/triggerable-withdrawals/tw-deploy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 582ffe8fe5..e7836ef5f9 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -112,6 +112,8 @@ async function main(): Promise { const BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000000040000d"; const BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX = BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX; + const SLOTS_PER_HISTORICAL_ROOT = 8192; + // TriggerableWithdrawalsGateway params const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 11_200; const TRIGGERABLE_WITHDRAWALS_LIMIT_PER_FRAME = 1; @@ -204,7 +206,7 @@ async function main(): Promise { 1, // firstSupportedSlot 1, // pivotSlot 0, // capellaSlot @see https://github.com/eth-clients/hoodi/blob/main/metadata/config.yaml#L33 - (SLOTS_PER_EPOCH * 8192) / SLOTS_PER_EPOCH, // slotsPerHistoricalRoot + SLOTS_PER_HISTORICAL_ROOT, // slotsPerHistoricalRoot SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, From d679b01a5a8a413c365c7860d7393617bfb07359 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 16 Jul 2025 15:38:31 +0200 Subject: [PATCH 350/405] fix: review deploy script --- scripts/triggerable-withdrawals/tw-deploy.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index e7836ef5f9..3a504da0bb 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -187,6 +187,8 @@ async function main(): Promise { libraries, }); log.success(`NodeOperatorsRegistry: ${nor.address}`); + + // 6. ValidatorExitDelayVerifier const gIndexes = { gIFirstValidatorPrev: VALIDATOR_PREV_GINDEX, gIFirstValidatorCurr: VALIDATOR_CURR_GINDEX, @@ -195,7 +197,7 @@ async function main(): Promise { gIFirstBlockRootInSummaryPrev: BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX, gIFirstBlockRootInSummaryCurr: BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX, }; - // 6. ValidatorExitDelayVerifier + const validatorExitDelayVerifier = await deployImplementation( Sk.validatorExitDelayVerifier, "ValidatorExitDelayVerifier", @@ -210,7 +212,7 @@ async function main(): Promise { SLOTS_PER_EPOCH, SECONDS_PER_SLOT, GENESIS_TIME, - (SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT) / (SLOTS_PER_EPOCH * SECONDS_PER_SLOT), // shardCommitteePeriodInSeconds + SHARD_COMMITTEE_PERIOD_SLOTS * SECONDS_PER_SLOT, // shardCommitteePeriodInSeconds ], ); log.success(`ValidatorExitDelayVerifier: ${validatorExitDelayVerifier.address}`); @@ -247,13 +249,14 @@ async function main(): Promise { triggerableWithdrawalsGateway.address, ]; + // 8. Deploy new LidoLocator const newLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); log.success(`LidoLocator: ${newLocator.address}`); const updatedState = readNetworkState(); persistNetworkState(updatedState); - // 8. GateSeal for withdrawalQueueERC721 + // 9. GateSeal for withdrawalQueueERC721 const WQ_GATE_SEAL = await deployGateSeal( updatedState, deployer, @@ -263,7 +266,7 @@ async function main(): Promise { Sk.gateSeal, ); - // 9. GateSeal for Triggerable Withdrawals + // 10. GateSeal for Triggerable Withdrawals const TW_GATE_SEAL = await deployGateSeal( updatedState, deployer, From 4c99d2651290a7a7dd6a46d4538d849e065e7c9c Mon Sep 17 00:00:00 2001 From: F4ever Date: Wed, 16 Jul 2025 15:59:25 +0200 Subject: [PATCH 351/405] feat: tweak exit delay tests --- .../ValidatorsExitBusOracle_Mock.sol | 19 ++++++------------- .../0.8.25/validatorExitDelayVerifier.test.ts | 6 +++--- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol index 75482faa66..6f2c7e31b9 100644 --- a/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol +++ b/test/0.8.25/contracts/ValidatorsExitBusOracle_Mock.sol @@ -12,7 +12,7 @@ struct MockExitRequestData { uint256 valIndex; } -contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { +contract ValidatorsExitBusOracle_Mock { bytes32 private _hash; uint256 private _deliveryTimestamp; MockExitRequestData[] private _data; @@ -44,17 +44,10 @@ contract ValidatorsExitBusOracle_Mock is IValidatorsExitBus { bytes calldata exitRequests, uint256 dataFormat, uint256 index - ) external pure override returns (bytes memory, uint256, uint256, uint256) { - // TODO: rewrite corresponding tests when this functions is pure but view - // skipped tests: - // - accepts a valid proof and does not revert - // - report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp - // - reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time - - revert("Not implemented"); - // require(keccak256(abi.encode(exitRequests, dataFormat)) == _hash, "Mock error, Invalid exitRequestsHash"); - - // MockExitRequestData memory data = _data[index]; - // return (data.pubkey, data.nodeOpId, data.moduleId, data.valIndex); + ) external view returns (bytes memory, uint256, uint256, uint256) { + require(keccak256(abi.encode(exitRequests, dataFormat)) == _hash, "Mock error, Invalid exitRequestsHash"); + + MockExitRequestData memory data = _data[index]; + return (data.pubkey, data.nodeOpId, data.moduleId, data.valIndex); } } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 4cc97817d4..6f66189603 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -229,7 +229,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ]); }); - it.skip("accepts a valid proof and does not revert", async () => { + it("accepts a valid proof and does not revert", async () => { const intervalInSlotsBetweenProvableBlockAndExitRequest = 1000; const veboExitRequestTimestamp = GENESIS_TIME + @@ -296,7 +296,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ); }); - it.skip("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { + it("report exit delay with uses earliest possible voluntary exit time when it's greater than exit request timestamp", async () => { const activationEpochTimestamp = GENESIS_TIME + Number(ACTIVE_VALIDATOR_PROOF.validator.activationEpoch) * SLOTS_PER_EPOCH * SECONDS_PER_SLOT; const earliestPossibleVoluntaryExitTimestamp = @@ -456,7 +456,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { ).to.be.revertedWithCustomError(validatorExitDelayVerifier, "InvalidBlockHeader"); }); - it.skip("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { + it("reverts with 'ExitIsNotEligibleOnProvableBeaconBlock' when the when proof slot is early then exit request time", async () => { const intervalInSecondsAfterProofSlot = 1; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; From cfa0c6a3605aabed41d0200d6a7c32d6b71e91b4 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 17 Jul 2025 15:31:43 +0300 Subject: [PATCH 352/405] feat(tw): add sleep to verifyContract to avoid API errors --- tasks/verify-contracts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks/verify-contracts.ts b/tasks/verify-contracts.ts index cab49d23bd..68d554f9bc 100644 --- a/tasks/verify-contracts.ts +++ b/tasks/verify-contracts.ts @@ -67,6 +67,8 @@ task("verify:deployed", "Verifies deployed contracts based on state file") }); async function verifyContract(contract: DeployedContract, hre: HardhatRuntimeEnvironment) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + if (!contract.contract) { // TODO: In the case of state processing on the local devnet there are skips, we need to find the cause return; From 812a45d1248a6708308a96b8b7fbb68949652cd9 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Wed, 23 Jul 2025 14:54:23 +0300 Subject: [PATCH 353/405] feat(tw-hoodi): add deployed-hoodi.json --- deployed-hoodi.json | 191 ++++++++++++++++++++++++++++++++------------ 1 file changed, 142 insertions(+), 49 deletions(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 86431d526a..d033e9efa5 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -14,7 +14,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x1955CeBfD65ad7Bf16A49f0eB80Ae81E3F8Ee12f", + "address": "0x2341c9BE0E639f262f8170f9ef1efeCC92cCF617", "constructorArgs": [ "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", "0x3508A952176b3c15387C97BE809eaffB1982176a", @@ -144,7 +144,7 @@ "app:node-operators-registry": { "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x749b29ed0A41A431A69C3E1b0432dc1df13408E7", + "address": "0x95F00b016bB31b7182D96D25074684518246E42a", "constructorArgs": [] }, "aragonApp": { @@ -183,6 +183,16 @@ ] } }, + "app:sandbox": { + "aragonApp": { + "name": "sandbox", + "fullName": "sandbox.lidopm.eth" + }, + "proxy": { + "address": "0x682E94d2630846a503BDeE8b6810DF71C9806891", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, "app:simple-dvt": { "aragonApp": { "name": "simple-dvt", @@ -199,16 +209,6 @@ ] } }, - "app:sandbox": { - "aragonApp": { - "name": "sandbox", - "fullName": "sandbox.lidopm.eth" - }, - "proxy": { - "address": "0x682E94d2630846a503BDeE8b6810DF71C9806891", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, "aragon-acl": { "implementation": { "contract": "@aragon/os/contracts/acl/ACL.sol", @@ -263,12 +263,16 @@ "implementation": { "contract": "@aragon/os/contracts/kernel/Kernel.sol", "address": "0xEEf274E065964Ec22Bd44ddEbE7557c6638b368C", - "constructorArgs": [true] + "constructorArgs": [ + true + ] }, "proxy": { "address": "0xA48DF029Fd2e5FCECB3886c5c2F60e3625A1E87d", "contract": "@aragon/os/contracts/kernel/KernelProxy.sol", - "constructorArgs": ["0xEEf274E065964Ec22Bd44ddEbE7557c6638b368C"] + "constructorArgs": [ + "0xEEf274E065964Ec22Bd44ddEbE7557c6638b368C" + ] } }, "aragon-repo-base": { @@ -366,11 +370,15 @@ "eip712StETH": { "contract": "contracts/0.8.9/EIP712StETH.sol", "address": "0x2A1d51BF3aAA7A7D027C8f561e5f579876a17B0a", - "constructorArgs": ["0x3508A952176b3c15387C97BE809eaffB1982176a"] + "constructorArgs": [ + "0x3508A952176b3c15387C97BE809eaffB1982176a" + ] }, "ens": { "address": "0x6d4995cA535179d4126cC153C386bc9C13B92ba3", - "constructorArgs": ["0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102"], + "constructorArgs": [ + "0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102" + ], "contract": "@aragon/os/contracts/lib/ens/ENS.sol" }, "ensFactory": { @@ -397,15 +405,25 @@ "executionLayerRewardsVault": { "contract": "contracts/0.8.9/LidoExecutionLayerRewardsVault.sol", "address": "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", - "constructorArgs": ["0x3508A952176b3c15387C97BE809eaffB1982176a", "0x0534aA41907c9631fae990960bCC72d75fA7cfeD"] + "constructorArgs": [ + "0x3508A952176b3c15387C97BE809eaffB1982176a", + "0x0534aA41907c9631fae990960bCC72d75fA7cfeD" + ] }, "gateSeal": { - "address": "0x2168Ea6D948Ab49c3D34c667A7e02F92369F3A9C", + "address": "0x73d76Bd3D589B2b2185c402da82cdAfbc18b958D", "factoryAddress": "0xA402349F560D45310D301E92B1AA4DeCABe147B3", - "sealDuration": 518400, - "expiryTimestamp": 1772323200, + "sealDuration": 1209600, + "expiryTimestamp": 1784217696, "sealingCommittee": "0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102" }, + "gateSealTW": { + "factoryAddress": "0xA402349F560D45310D301E92B1AA4DeCABe147B3", + "sealDuration": 1209600, + "expiryTimestamp": 1784217696, + "sealingCommittee": "0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102", + "address": "0x368f2fcb593170823cc844F1B29e75E3d26879A1" + }, "hashConsensusForAccountingOracle": { "deployParameters": { "fastLaneLengthSlots": 10, @@ -480,24 +498,26 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x3C20EA1Bd0A838a7E4bE7CE47917DEF0c2E190FD", + "address": "0x003f20CD17e7683A7F88A7AfF004f0C44F0cfB31", "constructorArgs": [ - { - "accountingOracle": "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", - "depositSecurityModule": "0x2F0303F20E0795E6CCd17BD5efE791A586f28E03", - "elRewardsVault": "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", - "legacyOracle": "0x5B70b650B7E14136eb141b5Bf46a52f962885752", - "lido": "0x3508A952176b3c15387C97BE809eaffB1982176a", - "oracleReportSanityChecker": "0x26AED10459e1096d242ABf251Ff55f8DEaf52348", - "postTokenRebaseReceiver": "0x5B70b650B7E14136eb141b5Bf46a52f962885752", - "burner": "0x4e9A9ea2F154bA34BE919CD16a4A953DCd888165", - "stakingRouter": "0xCc820558B39ee15C7C45B59390B503b83fb499A8", - "treasury": "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", - "validatorsExitBusOracle": "0x8664d394C2B3278F26A1B44B967aEf99707eeAB2", - "withdrawalQueue": "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", - "withdrawalVault": "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", - "oracleDaemonConfig": "0x2a833402e3F46fFC1ecAb3598c599147a78731a9" - } + [ + "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", + "0x2F0303F20E0795E6CCd17BD5efE791A586f28E03", + "0x9b108015fe433F173696Af3Aa0CF7CDb3E104258", + "0x5B70b650B7E14136eb141b5Bf46a52f962885752", + "0x3508A952176b3c15387C97BE809eaffB1982176a", + "0x26AED10459e1096d242ABf251Ff55f8DEaf52348", + "0x5B70b650B7E14136eb141b5Bf46a52f962885752", + "0x4e9A9ea2F154bA34BE919CD16a4A953DCd888165", + "0xCc820558B39ee15C7C45B59390B503b83fb499A8", + "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", + "0x8664d394C2B3278F26A1B44B967aEf99707eeAB2", + "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", + "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", + "0x2a833402e3F46fFC1ecAb3598c599147a78731a9", + "0xFd4386A8795956f4B6D01cbb6dB116749731D7bD", + "0x6679090D92b08a2a686eF8614feECD8cDFE209db" + ] ] } }, @@ -547,7 +567,10 @@ }, "contract": "contracts/0.8.9/OracleDaemonConfig.sol", "address": "0x2a833402e3F46fFC1ecAb3598c599147a78731a9", - "constructorArgs": ["0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102", []] + "constructorArgs": [ + "0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102", + [] + ] }, "oracleReportSanityChecker": { "deployParameters": { @@ -569,10 +592,23 @@ "constructorArgs": [ "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", "0x83BCE68B4e8b7071b2a664a26e6D3Bc17eEe3102", - [9000, 43200, 1000, 50, 600, 8, 24, 128, 750000, 1000, 101, 50] + [ + 9000, + 43200, + 1000, + 50, + 600, + 8, + 24, + 128, + 750000, + 1000, + 101, + 50 + ] ] }, - "scratchDeployGasUsed": "126084566", + "scratchDeployGasUsed": "173935386", "simpleDvt": { "deployParameters": { "stakingModuleTypeId": "curated-onchain-v1", @@ -591,8 +627,48 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x7637A8Afd3B464E4481c67d51Cfe234f64903fd3", - "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] + "address": "0xd5F04A81ac472B2cB32073CE9dDABa6FaF022827", + "constructorArgs": [ + "0x00000000219ab540356cBB839Cbe05303d7705Fa" + ] + } + }, + "triggerableWithdrawalsGateway": { + "implementation": { + "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", + "address": "0x6679090D92b08a2a686eF8614feECD8cDFE209db", + "constructorArgs": [ + "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", + "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", + 11200, + 1, + 48 + ] + } + }, + "validatorExitDelayVerifier": { + "implementation": { + "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", + "address": "0xFd4386A8795956f4B6D01cbb6dB116749731D7bD", + "constructorArgs": [ + "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", + { + "gIFirstValidatorPrev": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstValidatorCurr": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstHistoricalSummaryPrev": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstHistoricalSummaryCurr": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstBlockRootInSummaryPrev": "0x000000000000000000000000000000000000000000000000000000000040000d", + "gIFirstBlockRootInSummaryCurr": "0x000000000000000000000000000000000000000000000000000000000040000d" + }, + 1, + 1, + 0, + 8192, + 32, + 12, + 1742213400, + 98304 + ] } }, "validatorsExitBusOracle": { @@ -610,8 +686,12 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0x0e71BeD56B76E8ED96af5Bd5CDceE6F7f72201B1", - "constructorArgs": [12, 1742213400, "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8"] + "address": "0x7E6d9C9C44417bf2EaF69685981646e9752D623A", + "constructorArgs": [ + 12, + 1742213400, + "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8" + ] } }, "vestingParams": { @@ -643,25 +723,38 @@ "implementation": { "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol", "address": "0xD0a60e52837e045F4567193Cf8921191C486eCD5", - "constructorArgs": ["0x7E99eE3C66636DE415D2d7C880938F2f40f94De4", "Lido: stETH Withdrawal NFT", "unstETH"] + "constructorArgs": [ + "0x7E99eE3C66636DE415D2d7C880938F2f40f94De4", + "Lido: stETH Withdrawal NFT", + "unstETH" + ] } }, "withdrawalVault": { "implementation": { "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0x0f262D9A5Ada76C31cE638bA7AcAA8BA55827483", - "constructorArgs": ["0x3508A952176b3c15387C97BE809eaffB1982176a", "0x0534aA41907c9631fae990960bCC72d75fA7cfeD"] + "address": "0xfe7A58960Af333eAdeAeC39149F9d6A71dc3E668", + "constructorArgs": [ + "0x3508A952176b3c15387C97BE809eaffB1982176a", + "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", + "0x6679090D92b08a2a686eF8614feECD8cDFE209db" + ] }, "proxy": { "contract": "contracts/0.8.4/WithdrawalsManagerProxy.sol", "address": "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", - "constructorArgs": ["0x49B3512c44891bef83F8967d075121Bd1b07a01B", "0x0f262D9A5Ada76C31cE638bA7AcAA8BA55827483"] + "constructorArgs": [ + "0x49B3512c44891bef83F8967d075121Bd1b07a01B", + "0x0f262D9A5Ada76C31cE638bA7AcAA8BA55827483" + ] }, "address": "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2" }, "wstETH": { "contract": "contracts/0.6.12/WstETH.sol", "address": "0x7E99eE3C66636DE415D2d7C880938F2f40f94De4", - "constructorArgs": ["0x3508A952176b3c15387C97BE809eaffB1982176a"] + "constructorArgs": [ + "0x3508A952176b3c15387C97BE809eaffB1982176a" + ] } } From fa8400228f9726924a2ed622c2982c48478c6d8d Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 12 Jun 2025 18:57:35 +0200 Subject: [PATCH 354/405] wip: upgrade protocol on fork --- hardhat.config.ts | 3 + lib/state-file.ts | 63 +++++-- package.json | 3 +- .../test-scratch-upgrade.sh | 22 +++ scripts/upgrade/steps-deploy.json | 6 + scripts/upgrade/steps/0000-check-env.ts | 24 +++ .../upgrade/steps/0100-deploy-tw-contracts.ts | 175 ++++++++++++++++++ scripts/utils/upgrade.ts | 12 ++ upgrade-parameters-mainnet.json | 8 + 9 files changed, 297 insertions(+), 19 deletions(-) create mode 100644 scripts/triggerable-withdrawals/test-scratch-upgrade.sh create mode 100644 scripts/upgrade/steps-deploy.json create mode 100644 scripts/upgrade/steps/0000-check-env.ts create mode 100644 scripts/upgrade/steps/0100-deploy-tw-contracts.ts create mode 100644 scripts/utils/upgrade.ts create mode 100644 upgrade-parameters-mainnet.json diff --git a/hardhat.config.ts b/hardhat.config.ts index 9b81d5e2bf..aaa0fa25d0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -46,6 +46,9 @@ const config: HardhatUserConfig = { }, forking: getHardhatForkingConfig(), }, + "custom": { + url: RPC_URL, + }, "local": { url: process.env.LOCAL_RPC_URL || RPC_URL, }, diff --git a/lib/state-file.ts b/lib/state-file.ts index 83bd5e2be1..5231510fed 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -4,7 +4,7 @@ import { resolve } from "node:path"; import { network as hardhatNetwork } from "hardhat"; -const NETWORK_STATE_FILE_BASENAME = "deployed"; +const NETWORK_STATE_FILE_PREFIX = "deployed-"; const NETWORK_STATE_FILE_DIR = "."; export type DeploymentState = { @@ -15,7 +15,6 @@ export type DeploymentState = { export const TemplateAppNames = { // Lido apps LIDO: "lido", - ORACLE: "oracle", NODE_OPERATORS_REGISTRY: "node-operators-registry", SIMPLE_DVT: "simple-dvt", // Aragon apps @@ -31,7 +30,6 @@ export enum Sk { aragonEnsLabelName = "aragonEnsLabelName", apmRegistryFactory = "apmRegistryFactory", appLido = "app:lido", - appOracle = `app:oracle`, appNodeOperatorsRegistry = "app:node-operators-registry", appSimpleDvt = "app:simple-dvt", aragonAcl = "aragon-acl", @@ -40,6 +38,9 @@ export enum Sk { aragonId = "aragonID", aragonKernel = "aragon-kernel", aragonRepoBase = "aragon-repo-base", + aragonLidoAppRepo = "aragon-lido-app-repo", + aragonNodeOperatorsRegistryAppRepo = "aragon-node-operators-registry-app-repo", + aragonSimpleDvtAppRepo = "aragon-simple-dvt-app-repo", appAgent = "app:aragon-agent", appFinance = "app:aragon-finance", appTokenManager = "app:aragon-token-manager", @@ -52,7 +53,6 @@ export enum Sk { evmScriptRegistryFactory = "evmScriptRegistryFactory", ensSubdomainRegistrar = "ensSubdomainRegistrar", ldo = "ldo", - // lido = "lido", lidoApm = "lidoApm", lidoApmEnsName = "lidoApmEnsName", lidoApmEnsRegDurationSec = "lidoApmEnsRegDurationSec", @@ -87,8 +87,22 @@ export enum Sk { chainSpec = "chainSpec", scratchDeployGasUsed = "scratchDeployGasUsed", minFirstAllocationStrategy = "minFirstAllocationStrategy", + accounting = "accounting", + vaultHub = "vaultHub", + tokenRebaseNotifier = "tokenRebaseNotifier", validatorExitDelayVerifier = "validatorExitDelayVerifier", triggerableWithdrawalsGateway = "triggerableWithdrawalsGateway", + // Vaults + predepositGuarantee = "predepositGuarantee", + stakingVaultImplementation = "stakingVaultImplementation", + stakingVaultFactory = "stakingVaultFactory", + dashboardImpl = "dashboardImpl", + stakingVaultBeacon = "stakingVaultBeacon", + v3Template = "v3Template", + v3Addresses = "v3Addresses", + v3VoteScript = "v3VoteScript", + operatorGrid = "operatorGrid", + lazyOracle = "lazyOracle", } export function getAddress(contractKey: Sk, state: DeploymentState): string { @@ -100,7 +114,6 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.appVoting: case Sk.appLido: case Sk.appNodeOperatorsRegistry: - case Sk.appOracle: case Sk.aragonAcl: case Sk.aragonApmRegistry: case Sk.aragonEvmScriptRegistry: @@ -110,9 +123,17 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.validatorsExitBusOracle: case Sk.withdrawalQueueERC721: case Sk.withdrawalVault: + case Sk.lazyOracle: + case Sk.operatorGrid: + case Sk.accounting: + case Sk.burner: + case Sk.appSimpleDvt: + case Sk.aragonNodeOperatorsRegistryAppRepo: + case Sk.aragonSimpleDvtAppRepo: + case Sk.predepositGuarantee: + case Sk.vaultHub: return state[contractKey].proxy.address; case Sk.apmRegistryFactory: - case Sk.burner: case Sk.callsScript: case Sk.daoFactory: case Sk.depositSecurityModule: @@ -133,8 +154,10 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.oracleReportSanityChecker: case Sk.wstETH: case Sk.depositContract: + case Sk.tokenRebaseNotifier: case Sk.validatorExitDelayVerifier: case Sk.triggerableWithdrawalsGateway: + case Sk.stakingVaultFactory: return state[contractKey].address; default: throw new Error(`Unsupported contract entry key ${contractKey}`); @@ -148,13 +171,8 @@ export function readNetworkState({ deployer?: string; networkStateFile?: string; } = {}) { - const networkName = hardhatNetwork.name; const networkChainId = hardhatNetwork.config.chainId; - - const fileName = networkStateFile - ? resolve(NETWORK_STATE_FILE_DIR, networkStateFile) - : _getFileName(networkName, NETWORK_STATE_FILE_BASENAME, NETWORK_STATE_FILE_DIR); - + const fileName = _getStateFileFileName(networkStateFile); const state = _readStateFile(fileName); // Validate the deployer @@ -201,7 +219,7 @@ export function incrementGasUsed(increment: bigint | number, useStateFile = true } export async function resetStateFile(networkName: string = hardhatNetwork.name): Promise { - const fileName = _getFileName(networkName, NETWORK_STATE_FILE_BASENAME, NETWORK_STATE_FILE_DIR); + const fileName = _getFileName(NETWORK_STATE_FILE_DIR, networkName); try { await access(fileName, fsPromisesConstants.R_OK | fsPromisesConstants.W_OK); } catch (error) { @@ -210,14 +228,14 @@ export async function resetStateFile(networkName: string = hardhatNetwork.name): } // If file does not exist, create it with default values } finally { - const templateFileName = _getFileName("testnet-defaults", NETWORK_STATE_FILE_BASENAME, "scripts/scratch"); + const templateFileName = _getFileName("scripts/defaults", "testnet-defaults", ""); const templateData = readFileSync(templateFileName, "utf8"); writeFileSync(fileName, templateData, { encoding: "utf8", flag: "w" }); } } -export function persistNetworkState(state: DeploymentState, networkName: string = hardhatNetwork.name): void { - const fileName = _getFileName(networkName, NETWORK_STATE_FILE_BASENAME, NETWORK_STATE_FILE_DIR); +export function persistNetworkState(state: DeploymentState): void { + const fileName = _getStateFileFileName(); const stateSorted = _sortKeysAlphabetically(state); const data = JSON.stringify(stateSorted, null, 2); @@ -228,8 +246,17 @@ export function persistNetworkState(state: DeploymentState, networkName: string } } -function _getFileName(networkName: string, baseName: string, dir: string) { - return resolve(dir, `${baseName}-${networkName}.json`); +function _getStateFileFileName(networkStateFile = "") { + // Use the specified network state file or the one from the environment + networkStateFile = networkStateFile || process.env.NETWORK_STATE_FILE || ""; + + return networkStateFile + ? resolve(NETWORK_STATE_FILE_DIR, networkStateFile) + : _getFileName(NETWORK_STATE_FILE_DIR, hardhatNetwork.name); +} + +function _getFileName(dir: string, networkName: string, prefix: string = NETWORK_STATE_FILE_PREFIX) { + return resolve(dir, `${prefix}${networkName}.json`); } function _readStateFile(fileName: string) { diff --git a/package.json b/package.json index 50ef61f562..853753ad36 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "lint:ts:fix": "yarn lint:ts --fix", "lint": "yarn lint:sol && yarn lint:ts", "format": "prettier . --write", - "test": "hardhat test test/**/*.test.ts --parallel", + "test": "hardhat test test/0.8.9/lib/GIndex.test.ts --parallel", + "upgrade:deploy": "STEPS_FILE=upgrade/steps-deploy.json UPGRADE_PARAMETERS_FILE=upgrade-parameters-mainnet.json yarn hardhat --network custom run scripts/utils/migrate.ts", "test:forge": "forge test", "test:coverage": "hardhat coverage", "test:sequential": "hardhat test test/**/*.test.ts", diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh new file mode 100644 index 0000000000..642eeb0672 --- /dev/null +++ b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh @@ -0,0 +1,22 @@ +# RPC_URL: http://localhost:8555 +# DEPLOYER: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # first acc of default mnemonic "test test ..." +# GAS_PRIORITY_FEE: 1 +# GAS_MAX_FEE: 100 +# NETWORK_STATE_FILE: deployed-mainnet-upgrade.json +# UPGRADE_PARAMETERS_FILE: upgrade-parameters-mainnet.json + +export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise +export SLOTS_PER_EPOCH=32 +export GENESIS_TIME=1639659600 # just some time +# export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" +# export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" + +export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of default mnemonic "test test ..." +export GAS_PRIORITY_FEE=1 +export GAS_MAX_FEE=100 + +export NETWORK_STATE_FILE=deployed-mainnet-upgrade.json + +cp deployed-mainnet.json $NETWORK_STATE_FILE + +yarn upgrade:deploy diff --git a/scripts/upgrade/steps-deploy.json b/scripts/upgrade/steps-deploy.json new file mode 100644 index 0000000000..3062d492a7 --- /dev/null +++ b/scripts/upgrade/steps-deploy.json @@ -0,0 +1,6 @@ +{ + "steps": [ + "upgrade/steps/0000-check-env", + "upgrade/steps/0100-deploy-tw-contracts" + ] +} diff --git a/scripts/upgrade/steps/0000-check-env.ts b/scripts/upgrade/steps/0000-check-env.ts new file mode 100644 index 0000000000..1416f4e862 --- /dev/null +++ b/scripts/upgrade/steps/0000-check-env.ts @@ -0,0 +1,24 @@ +import { ethers } from "hardhat"; + +export async function main() { + const deployer = (await ethers.provider.getSigner()).address; + if (deployer !== process.env.DEPLOYER) { + throw new Error(`Deployer address mismatch: env DEPLOYER=${process.env.DEPLOYER}, signer=${deployer}`); + } + + if (!process.env.NETWORK_STATE_FILE) { + throw new Error("Env variable NETWORK_STATE_FILE is not set"); + } + + if (!process.env.GAS_PRIORITY_FEE) { + throw new Error("Env variable GAS_PRIORITY_FEE is not set"); + } + + if (!process.env.GAS_MAX_FEE) { + throw new Error("Env variable GAS_MAX_FEE is not set"); + } + + if (!process.env.GENESIS_TIME) { + throw new Error("Env variable GENESIS_TIME is not set"); + } +} diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts new file mode 100644 index 0000000000..592e190960 --- /dev/null +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -0,0 +1,175 @@ +import * as dotenv from "dotenv"; +import { ethers } from "hardhat"; +import { join } from "path"; +import { readUpgradeParameters } from "scripts/utils/upgrade"; + +import { LidoLocator } from "typechain-types"; + +import { cy, deployImplementation, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; + +dotenv.config({ path: join(__dirname, "../../.env") }); + +function getEnvVariable(name: string, defaultValue?: string) { + const value = process.env[name]; + if (value === undefined) { + if (defaultValue === undefined) { + throw new Error(`Env variable ${name} must be set`); + } + return defaultValue; + } else { + log(`Using env variable ${name}=${value}`); + return value; + } +} + +// Must comply with the specification +// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1 +const SECONDS_PER_SLOT = 12; + +// Must match the beacon chain genesis_time: https://beaconstate-mainnet.chainsafe.io/eth/v1/beacon/genesis +// and the current value: https://etherscan.io/address/0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb +const genesisTime = parseInt(getEnvVariable("GENESIS_TIME")); + +export async function main() { + const deployer = ethers.getAddress(getEnvVariable("DEPLOYER")); + const chainId = (await ethers.provider.getNetwork()).chainId; + + log(cy(`Deploy of contracts on chain ${chainId}`)); + + const state = readNetworkState(); + const parameters = readUpgradeParameters(); + persistNetworkState(state); + + const chainSpec = state[Sk.chainSpec]; + + log(`Chain spec: ${JSON.stringify(chainSpec, null, 2)}`); + + const agent = state["app:aragon-agent"].proxy.address; + log(`Using agent: ${agent}`); + // Read contracts addresses from config + const locator = await loadContract("LidoLocator", state[Sk.lidoLocator].proxy.address); + + const LIDO_PROXY = await locator.lido(); + const TREASURY_PROXY = await locator.treasury(); + + // Deploy ValidatorExitBusOracle + // uint256 secondsPerSlot, uint256 genesisTime, address lidoLocator + const validatorsExitBusOracleArgs = [SECONDS_PER_SLOT, genesisTime, locator.address]; + + const validatorsExitBusOracle = await deployImplementation( + Sk.validatorsExitBusOracle, + "ValidatorsExitBusOracle", + deployer, + validatorsExitBusOracleArgs, + ); + log.success(`ValidatorsExitBusOracle address: ${validatorsExitBusOracle.address}`); + log.emptyLine(); + + const triggerableWithdrawalsGateway = await deployImplementation( + Sk.triggerableWithdrawalsGateway, + "TriggerableWithdrawalsGateway", + deployer, + [agent, locator.address, 13000, 1, 48], + ); + log.success(`TriggerableWithdrawalsGateway implementation address: ${triggerableWithdrawalsGateway.address}`); + log.emptyLine(); + + const withdrawalVaultArgs = [LIDO_PROXY, TREASURY_PROXY, triggerableWithdrawalsGateway.address]; + + const withdrawalVault = await deployImplementation( + Sk.withdrawalVault, + "WithdrawalVault", + deployer, + withdrawalVaultArgs, + ); + log.success(`WithdrawalVault address implementation: ${withdrawalVault.address}`); + + const minFirstAllocationStrategyAddress = state[Sk.minFirstAllocationStrategy].address; + const libraries = { + MinFirstAllocationStrategy: minFirstAllocationStrategyAddress, + }; + + const DEPOSIT_CONTRACT_ADDRESS = parameters[Sk.chainSpec].depositContract; + log(`Deposit contract address: ${DEPOSIT_CONTRACT_ADDRESS}`); + const stakingRouterAddress = await deployImplementation( + Sk.stakingRouter, + "StakingRouter", + deployer, + [DEPOSIT_CONTRACT_ADDRESS], + { libraries }, + ); + + log(`StakingRouter implementation address: ${stakingRouterAddress.address}`); + + const NOR = await deployImplementation(Sk.appNodeOperatorsRegistry, "NodeOperatorsRegistry", deployer, [], { + libraries, + }); + + log.success(`NOR implementation address: ${NOR.address}`); + log.emptyLine(); + + const validatorExitDelayVerifierArgs = [ + locator.address, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, + "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, + "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, + 1, // uint64 firstSupportedSlot, + 1, // uint64 pivotSlot, + 32, // uint32 slotsPerEpoch, + 12, // uint32 secondsPerSlot, + genesisTime, // uint64 genesisTime, + 2 ** 8 * 32 * 12, // uint32 shardCommitteePeriodInSeconds + ]; + + const validatorExitDelayVerifier = await deployImplementation( + Sk.validatorExitDelayVerifier, + "ValidatorExitDelayVerifier", + deployer, + validatorExitDelayVerifierArgs, + ); + log.success(`ValidatorExitDelayVerifier implementation address: ${validatorExitDelayVerifier.address}`); + log.emptyLine(); + + const accountingOracle = await deployImplementation(Sk.accountingOracle, "AccountingOracle", deployer, [ + locator.address, + await locator.lido(), + await locator.legacyOracle(), + Number(chainSpec.secondsPerSlot), + Number(chainSpec.genesisTime), + ]); + + // fetch contract addresses that will not changed + const locatorConfig = [ + await locator.accountingOracle(), + await locator.depositSecurityModule(), + await locator.elRewardsVault(), + await locator.legacyOracle(), + await locator.lido(), + await locator.oracleReportSanityChecker(), + await locator.postTokenRebaseReceiver(), + await locator.burner(), + await locator.stakingRouter(), + await locator.treasury(), + await locator.validatorsExitBusOracle(), + await locator.withdrawalQueue(), + await locator.withdrawalVault(), + await locator.oracleDaemonConfig(), + validatorExitDelayVerifier.address, + triggerableWithdrawalsGateway.address, + ]; + + const lidoLocator = await deployImplementation(Sk.lidoLocator, "LidoLocator", deployer, [locatorConfig]); + + log(`Configuration for voting script:`); + log(` +LIDO_LOCATOR_IMPL = "${lidoLocator.address}" +ACCOUNTING_ORACLE = "${accountingOracle.address}" +VALIDATORS_EXIT_BUS_ORACLE_IMPL = "${validatorsExitBusOracle.address}" +WITHDRAWAL_VAULT_IMPL = "${withdrawalVault.address}" +STAKING_ROUTER_IMPL = "${stakingRouterAddress.address}" +NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" +VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" +TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" +`); +} diff --git a/scripts/utils/upgrade.ts b/scripts/utils/upgrade.ts new file mode 100644 index 0000000000..f093b6c151 --- /dev/null +++ b/scripts/utils/upgrade.ts @@ -0,0 +1,12 @@ +import fs from "fs"; + +const UPGRADE_PARAMETERS_FILE = process.env.UPGRADE_PARAMETERS_FILE; + +export function readUpgradeParameters() { + if (!UPGRADE_PARAMETERS_FILE) { + throw new Error("UPGRADE_PARAMETERS_FILE is not set"); + } + + const rawData = fs.readFileSync(UPGRADE_PARAMETERS_FILE); + return JSON.parse(rawData.toString()); +} diff --git a/upgrade-parameters-mainnet.json b/upgrade-parameters-mainnet.json new file mode 100644 index 0000000000..0af322a408 --- /dev/null +++ b/upgrade-parameters-mainnet.json @@ -0,0 +1,8 @@ +{ + "chainSpec": { + "slotsPerEpoch": 32, + "secondsPerSlot": 12, + "genesisTime": null, + "depositContract": "0x00000000219ab540356cBB839Cbe05303d7705Fa" + } +} From 3c87a13a01e2ebf502acea63e3f74c7246ca21d4 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 12 Jun 2025 19:49:39 +0200 Subject: [PATCH 355/405] wip: deploy aragon script --- hardhat.config.ts | 1 + lib/state-file.ts | 2 + package.json | 2 + scripts/upgrade/steps-voting.json | 3 + .../upgrade/steps/0100-deploy-tw-contracts.ts | 35 +- .../steps/0300-deploy-upgrading-contracts.ts | 54 +++ test/0.8.25/upgrade/TWVoteScript.sol | 359 ++++++++++++++++++ test/0.8.25/upgrade/interfaces/IForwarder.sol | 10 + .../IOracleReportSanityChecker_preV3.sol | 75 ++++ test/0.8.25/upgrade/interfaces/IVoting.sol | 37 ++ .../upgrade/utils/CallScriptBuilder.sol | 40 ++ test/0.8.25/upgrade/utils/OmnibusBase.sol | 108 ++++++ yarn.lock | 8 + 13 files changed, 733 insertions(+), 1 deletion(-) create mode 100644 scripts/upgrade/steps-voting.json create mode 100644 scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts create mode 100644 test/0.8.25/upgrade/TWVoteScript.sol create mode 100644 test/0.8.25/upgrade/interfaces/IForwarder.sol create mode 100644 test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol create mode 100644 test/0.8.25/upgrade/interfaces/IVoting.sol create mode 100644 test/0.8.25/upgrade/utils/CallScriptBuilder.sol create mode 100644 test/0.8.25/upgrade/utils/OmnibusBase.sol diff --git a/hardhat.config.ts b/hardhat.config.ts index aaa0fa25d0..e6edea1ccd 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -152,6 +152,7 @@ const config: HardhatUserConfig = { { version: "0.8.25", settings: { + viaIR: true, optimizer: { enabled: true, runs: 200, diff --git a/lib/state-file.ts b/lib/state-file.ts index 5231510fed..0d247f8574 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -90,8 +90,10 @@ export enum Sk { accounting = "accounting", vaultHub = "vaultHub", tokenRebaseNotifier = "tokenRebaseNotifier", + // Triggerable withdrawals validatorExitDelayVerifier = "validatorExitDelayVerifier", triggerableWithdrawalsGateway = "triggerableWithdrawalsGateway", + TWVoteScript = "TWVoteScript", // Vaults predepositGuarantee = "predepositGuarantee", stakingVaultImplementation = "stakingVaultImplementation", diff --git a/package.json b/package.json index 853753ad36..c70eba7485 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "format": "prettier . --write", "test": "hardhat test test/0.8.9/lib/GIndex.test.ts --parallel", "upgrade:deploy": "STEPS_FILE=upgrade/steps-deploy.json UPGRADE_PARAMETERS_FILE=upgrade-parameters-mainnet.json yarn hardhat --network custom run scripts/utils/migrate.ts", + "upgrade:mock-voting": "STEPS_FILE=upgrade/steps-mock-voting.json UPGRADE_PARAMETERS_FILE=upgrade-parameters-mainnet.json yarn hardhat --network custom run scripts/utils/migrate.ts", "test:forge": "forge test", "test:coverage": "hardhat coverage", "test:sequential": "hardhat test test/**/*.test.ts", @@ -110,6 +111,7 @@ "@aragon/os": "4.4.0", "@openzeppelin/contracts": "3.4.0", "@openzeppelin/contracts-v4.4": "npm:@openzeppelin/contracts@4.4.1", + "@openzeppelin/contracts-v5.2": "npm:@openzeppelin/contracts@5.2.0", "openzeppelin-solidity": "2.0.0" } } diff --git a/scripts/upgrade/steps-voting.json b/scripts/upgrade/steps-voting.json new file mode 100644 index 0000000000..c268885e31 --- /dev/null +++ b/scripts/upgrade/steps-voting.json @@ -0,0 +1,3 @@ +{ + "steps": ["upgrade/steps/0500-mock-aragon-voting"] +} diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index 592e190960..94e45caf16 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -5,7 +5,7 @@ import { readUpgradeParameters } from "scripts/utils/upgrade"; import { LidoLocator } from "typechain-types"; -import { cy, deployImplementation, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; +import { cy, deployImplementation, deployWithoutProxy, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; dotenv.config({ path: join(__dirname, "../../.env") }); @@ -172,4 +172,37 @@ NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" `); + + await deployWithoutProxy(Sk.TWVoteScript, "TWVoteScript", deployer, [ + state[Sk.appVoting].proxy.address, + { + // Contract addresses + agent: agent, + lido_locator: state[Sk.lidoLocator].proxy.address, + lido_locator_impl: lidoLocator.address, + validators_exit_bus_oracle: await locator.validatorsExitBusOracle(), + validators_exit_bus_oracle_impl: validatorsExitBusOracle.address, + triggerable_withdrawals_gateway: triggerableWithdrawalsGateway.address, + withdrawal_vault: await locator.withdrawalVault(), + withdrawal_vault_impl: withdrawalVault.address, + accounting_oracle: await locator.accountingOracle(), + accounting_oracle_impl: accountingOracle.address, + staking_router: await locator.stakingRouter(), + staking_router_impl: stakingRouterAddress.address, + validator_exit_verifier: validatorExitDelayVerifier.address, + node_operators_registry: state[Sk.appNodeOperatorsRegistry].proxy.address, + node_operators_registry_impl: NOR.address, + oracle_daemon_config: await locator.oracleDaemonConfig(), + nor_app_repo: "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + + // Other parameters + node_operators_registry_app_id: state[Sk.appNodeOperatorsRegistry].aragonApp.id, + nor_version: parameters[Sk.appNodeOperatorsRegistry]?.newVersion || [2, 0, 0], + vebo_consensus_version: 4, + ao_consensus_version: 4, + nor_exit_deadline_in_sec: 30 * 60, // 30 minutes + exit_events_lookback_window_in_slots: 7200, + nor_content_uri: state[Sk.appNodeOperatorsRegistry].aragonApp.contentURI, + }, + ]); } diff --git a/scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts b/scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts new file mode 100644 index 0000000000..3e5401b203 --- /dev/null +++ b/scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts @@ -0,0 +1,54 @@ +import { ethers } from "hardhat"; +import { readUpgradeParameters } from "scripts/utils/upgrade"; + +import { IAragonAppRepo, IOssifiableProxy, OssifiableProxy__factory } from "typechain-types"; + +import { loadContract } from "lib/contract"; +import { deployWithoutProxy } from "lib/deploy"; +import { readNetworkState, Sk } from "lib/state-file"; + +export async function main() { + const deployerSigner = await ethers.provider.getSigner(); + const deployer = deployerSigner.address; + const state = readNetworkState(); + const parameters = readUpgradeParameters(); + + const locator = OssifiableProxy__factory.connect(state[Sk.lidoLocator].proxy.address, deployerSigner); + const oldLocatorImplementation = await locator.proxy__getImplementation(); + const accountingOracle = await loadContract( + "IOssifiableProxy", + state[Sk.accountingOracle].proxy.address, + ); + const lidoRepo = await loadContract("IAragonAppRepo", state[Sk.aragonLidoAppRepo].proxy.address); + const [, lidoImplementation] = await lidoRepo.getLatest(); + + const addressesParams = [ + // Old implementations + oldLocatorImplementation, + lidoImplementation, + await accountingOracle.proxy__getImplementation(), + + // New implementations + state[Sk.lidoLocator].implementation.address, + state[Sk.appLido].implementation.address, + state[Sk.accountingOracle].implementation.address, + + // New fancy proxy and blueprint contracts + state[Sk.stakingVaultBeacon].address, + state[Sk.stakingVaultImplementation].address, + state[Sk.dashboardImpl].address, + + // Existing proxies and contracts + state[Sk.aragonKernel].proxy.address, + state[Sk.appAgent].proxy.address, + state[Sk.aragonLidoAppRepo].proxy.address, + state[Sk.lidoLocator].proxy.address, + state[Sk.appVoting].proxy.address, + ]; + + const template = await deployWithoutProxy(Sk.v3Template, "V3Template", deployer, [addressesParams]); + + await deployWithoutProxy(Sk.v3VoteScript, "V3VoteScript", deployer, [ + [template.address, parameters[Sk.appLido].newVersion, state[Sk.appLido].aragonApp.id], + ]); +} diff --git a/test/0.8.25/upgrade/TWVoteScript.sol b/test/0.8.25/upgrade/TWVoteScript.sol new file mode 100644 index 0000000000..dae7af929c --- /dev/null +++ b/test/0.8.25/upgrade/TWVoteScript.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {IAccessControl} from "@openzeppelin/contracts-v5.2/access/IAccessControl.sol"; +import {OmnibusBase} from "./utils/OmnibusBase.sol"; + +interface IOssifiableProxy { + function proxy__upgradeTo(address newImplementation) external; + function proxy__changeAdmin(address newAdmin) external; + function proxy__getAdmin() external view returns (address); + function proxy__getImplementation() external view returns (address); +} + +interface IRepo { + function newVersion(uint16[3] calldata _newSemanticVersion, address _contractAddress, bytes calldata _contentURI) external; +} + +interface IKernel { + function setApp(bytes32 _namespace, bytes32 _appId, address _app) external; + function APP_BASES_NAMESPACE() external view returns (bytes32); +} + +interface IWithdrawalVaultProxy { + function proxy_upgradeTo(address _implementation, bytes calldata _data) external; + function proxy_getAdmin() external view returns (address); +} + +interface IOracleContract { + function setConsensusVersion(uint256 _version) external; + function finalizeUpgrade_v2(uint256 _maxValidatorsPerReport, uint256 _maxExitRequestsLimit, uint256 _exitsPerFrame, uint256 _frameDurationInSec) external; +} + +interface IWithdrawalVault { + function finalizeUpgrade_v2() external; +} + +interface INodeOperatorsRegistry { + function finalizeUpgrade_v4(uint256 _exitDeadlineInSec) external; +} + +interface IOracleDaemonConfig { + function set(string calldata _key, uint256 _value) external; + function unset(string calldata _key) external; +} + +/// @title TWVoteScript +/// @notice Script for implementing Triggerable Withdrawals voting items +contract TWVoteScript is OmnibusBase { + + struct ScriptParams { + // Contract addresses + address agent; + address lido_locator; + address lido_locator_impl; + address validators_exit_bus_oracle; + address validators_exit_bus_oracle_impl; + address triggerable_withdrawals_gateway; + address withdrawal_vault; + address withdrawal_vault_impl; + address accounting_oracle; + address accounting_oracle_impl; + address staking_router; + address staking_router_impl; + address validator_exit_verifier; + address node_operators_registry; + address node_operators_registry_impl; + address oracle_daemon_config; + address nor_app_repo; + + // Other parameters + bytes32 node_operators_registry_app_id; + uint16[3] nor_version; + uint256 vebo_consensus_version; + uint256 ao_consensus_version; + uint256 nor_exit_deadline_in_sec; + uint256 exit_events_lookback_window_in_slots; + bytes nor_content_uri; + } + + // + // Constants + // + uint256 public constant VOTE_ITEMS_COUNT = 22; + + // + // Structured storage + // + ScriptParams public params; + + constructor( + address _voting, + ScriptParams memory _params + ) OmnibusBase(_voting) { + params = _params; + } + + function getVoteItems() public view override returns (VoteItem[] memory voteItems) { + voteItems = new VoteItem[](VOTE_ITEMS_COUNT); + + // Create vote items in smaller batches to reduce stack depth + createVoteItems1_1(voteItems, 0); // Items 1-3 + createVoteItems1_2(voteItems, 3); // Items 4-7 + createVoteItems2_1(voteItems, 7); // Items 8-10 + createVoteItems2_2(voteItems, 10); // Items 11-14 + createVoteItems3_1(voteItems, 14); // Items 15-18 + createVoteItems3_2(voteItems, 18); // Items 19-22 + + // Ensure we have created exactly the expected number of items + assert(voteItems.length == VOTE_ITEMS_COUNT); + } + + function createVoteItems1_1(VoteItem[] memory voteItems, uint256 startIndex) internal view { + uint256 index = startIndex; + + // 1. Update locator implementation + voteItems[index++] = VoteItem({ + description: "1. Update locator implementation", + call: _forwardCall( + params.agent, + params.lido_locator, + abi.encodeCall(IOssifiableProxy.proxy__upgradeTo, (params.lido_locator_impl)) + ) + }); + + // 2. Update VEBO implementation + voteItems[index++] = VoteItem({ + description: "2. Update VEBO implementation", + call: _forwardCall( + params.agent, + params.validators_exit_bus_oracle, + abi.encodeCall(IOssifiableProxy.proxy__upgradeTo, (params.validators_exit_bus_oracle_impl)) + ) + }); + + // 3. Call finalizeUpgrade_v2 on VEBO + voteItems[index++] = VoteItem({ + description: "3. Call finalizeUpgrade_v2 on VEBO", + call: _votingCall( + params.validators_exit_bus_oracle, + abi.encodeCall(IOracleContract.finalizeUpgrade_v2, (600, 13000, 1, 48)) + ) + }); + } + + function createVoteItems1_2(VoteItem[] memory voteItems, uint256 startIndex) internal view { + uint256 index = startIndex; + + // 4. Grant VEBO role MANAGE_CONSENSUS_VERSION_ROLE to the AGENT + bytes32 manageConsensusVersionRole = keccak256("MANAGE_CONSENSUS_VERSION_ROLE"); + voteItems[index++] = VoteItem({ + description: "4. Grant VEBO role MANAGE_CONSENSUS_VERSION_ROLE to the AGENT", + call: _forwardCall( + params.agent, + params.validators_exit_bus_oracle, + abi.encodeCall(IAccessControl.grantRole, (manageConsensusVersionRole, params.agent)) + ) + }); + + // 5. Bump VEBO consensus version + voteItems[index++] = VoteItem({ + description: "5. Bump VEBO consensus version", + call: _forwardCall( + params.agent, + params.validators_exit_bus_oracle, + abi.encodeCall(IOracleContract.setConsensusVersion, (params.vebo_consensus_version)) + ) + }); + + // 6. Grant TWG role ADD_FULL_WITHDRAWAL_REQUEST_ROLE to the VEB + bytes32 addFullWithdrawalRequestRole = keccak256("ADD_FULL_WITHDRAWAL_REQUEST_ROLE"); + voteItems[index++] = VoteItem({ + description: "6. Grant TWG role ADD_FULL_WITHDRAWAL_REQUEST_ROLE to the VEB", + call: _forwardCall( + params.agent, + params.triggerable_withdrawals_gateway, + abi.encodeCall(IAccessControl.grantRole, (addFullWithdrawalRequestRole, params.validators_exit_bus_oracle)) + ) + }); + + // 7. Update WithdrawalVault implementation + voteItems[index++] = VoteItem({ + description: "7. Update WithdrawalVault implementation", + call: _votingCall( + params.withdrawal_vault, + abi.encodeCall(IWithdrawalVaultProxy.proxy_upgradeTo, (params.withdrawal_vault_impl, "")) + ) + }); + } + + function createVoteItems2_1(VoteItem[] memory voteItems, uint256 startIndex) internal view { + uint256 index = startIndex; + + // 8. Call finalizeUpgrade_v2 on WithdrawalVault + voteItems[index++] = VoteItem({ + description: "8. Call finalizeUpgrade_v2 on WithdrawalVault", + call: _votingCall( + params.withdrawal_vault, + abi.encodeCall(IWithdrawalVault.finalizeUpgrade_v2, ()) + ) + }); + + // 9. Update Accounting Oracle implementation + voteItems[index++] = VoteItem({ + description: "9. Update Accounting Oracle implementation", + call: _forwardCall( + params.agent, + params.accounting_oracle, + abi.encodeCall(IOssifiableProxy.proxy__upgradeTo, (params.accounting_oracle_impl)) + ) + }); + + // 10. Grant AO MANAGE_CONSENSUS_VERSION_ROLE to the AGENT + bytes32 manageConsensusVersionRole = keccak256("MANAGE_CONSENSUS_VERSION_ROLE"); + voteItems[index++] = VoteItem({ + description: "10. Grant AO MANAGE_CONSENSUS_VERSION_ROLE to the AGENT", + call: _forwardCall( + params.agent, + params.accounting_oracle, + abi.encodeCall(IAccessControl.grantRole, (manageConsensusVersionRole, params.agent)) + ) + }); + } + + function createVoteItems2_2(VoteItem[] memory voteItems, uint256 startIndex) internal view { + uint256 index = startIndex; + + // 11. Bump AO consensus version + voteItems[index++] = VoteItem({ + description: "11. Bump AO consensus version", + call: _forwardCall( + params.agent, + params.accounting_oracle, + abi.encodeCall(IOracleContract.setConsensusVersion, (params.ao_consensus_version)) + ) + }); + + // 12. Update SR implementation + voteItems[index++] = VoteItem({ + description: "12. Update SR implementation", + call: _forwardCall( + params.agent, + params.staking_router, + abi.encodeCall(IOssifiableProxy.proxy__upgradeTo, (params.staking_router_impl)) + ) + }); + + // 13. Grant SR role REPORT_VALIDATOR_EXITING_STATUS_ROLE to ValidatorExitVerifier + bytes32 reportValidatorExitingStatusRole = keccak256("REPORT_VALIDATOR_EXITING_STATUS_ROLE"); + voteItems[index++] = VoteItem({ + description: "13. Grant SR role REPORT_VALIDATOR_EXITING_STATUS_ROLE to ValidatorExitVerifier", + call: _forwardCall( + params.agent, + params.staking_router, + abi.encodeCall(IAccessControl.grantRole, (reportValidatorExitingStatusRole, params.validator_exit_verifier)) + ) + }); + + // 14. Grant SR role REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE to TWG + bytes32 reportValidatorExitTriggeredRole = keccak256("REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE"); + voteItems[index++] = VoteItem({ + description: "14. Grant SR role REPORT_VALIDATOR_EXIT_TRIGGERED_ROLE to TWG", + call: _forwardCall( + params.agent, + params.staking_router, + abi.encodeCall(IAccessControl.grantRole, (reportValidatorExitTriggeredRole, params.triggerable_withdrawals_gateway)) + ) + }); + } + + function createVoteItems3_1(VoteItem[] memory voteItems, uint256 startIndex) internal view { + uint256 index = startIndex; + + // 15. Publish new NodeOperatorsRegistry implementation in NodeOperatorsRegistry app APM repo + voteItems[index++] = VoteItem({ + description: "15. Publish new NodeOperatorsRegistry implementation in NodeOperatorsRegistry app APM repo", + call: _votingCall( + params.nor_app_repo, + abi.encodeCall(IRepo.newVersion, (params.nor_version, params.node_operators_registry_impl, params.nor_content_uri)) + ) + }); + + // 16. Update NodeOperatorsRegistry implementation + voteItems[index++] = VoteItem({ + description: "16. Update NodeOperatorsRegistry implementation", + call: _votingCall( + params.agent, + abi.encodeWithSignature("setApp(bytes32,bytes32,address)", + IKernel(address(0)).APP_BASES_NAMESPACE(), + params.node_operators_registry_app_id, + params.node_operators_registry_impl + ) + ) + }); + + // 17. Call finalizeUpgrade_v4 on NOR + voteItems[index++] = VoteItem({ + description: "17. Call finalizeUpgrade_v4 on NOR", + call: _votingCall( + params.node_operators_registry, + abi.encodeCall(INodeOperatorsRegistry.finalizeUpgrade_v4, (params.nor_exit_deadline_in_sec)) + ) + }); + + // 18. Grant CONFIG_MANAGER_ROLE role to the AGENT + bytes32 configManagerRole = keccak256("CONFIG_MANAGER_ROLE"); + voteItems[index++] = VoteItem({ + description: "18. Grant CONFIG_MANAGER_ROLE role to the AGENT", + call: _forwardCall( + params.agent, + params.oracle_daemon_config, + abi.encodeCall(IAccessControl.grantRole, (configManagerRole, params.agent)) + ) + }); + } + + function createVoteItems3_2(VoteItem[] memory voteItems, uint256 startIndex) internal view { + uint256 index = startIndex; + + // 19. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig + voteItems[index++] = VoteItem({ + description: "19. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig", + call: _forwardCall( + params.agent, + params.oracle_daemon_config, + abi.encodeCall(IOracleDaemonConfig.unset, ("NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP")) + ) + }); + + // 20. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig + voteItems[index++] = VoteItem({ + description: "20. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", + call: _forwardCall( + params.agent, + params.oracle_daemon_config, + abi.encodeCall(IOracleDaemonConfig.unset, ("VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS")) + ) + }); + + // 21. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig + voteItems[index++] = VoteItem({ + description: "21. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", + call: _forwardCall( + params.agent, + params.oracle_daemon_config, + abi.encodeCall(IOracleDaemonConfig.unset, ("VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS")) + ) + }); + + // 22. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig + voteItems[index++] = VoteItem({ + description: "22. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig", + call: _forwardCall( + params.agent, + params.oracle_daemon_config, + abi.encodeCall(IOracleDaemonConfig.set, ("EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS", params.exit_events_lookback_window_in_slots)) + ) + }); + } +} diff --git a/test/0.8.25/upgrade/interfaces/IForwarder.sol b/test/0.8.25/upgrade/interfaces/IForwarder.sol new file mode 100644 index 0000000000..e632caeac0 --- /dev/null +++ b/test/0.8.25/upgrade/interfaces/IForwarder.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.4.24 <0.9.0; + +interface IForwarder { + function execute(address _target, uint256 _ethValue, bytes memory _data) external payable; + function forward(bytes memory _evmScript) external; +} diff --git a/test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol b/test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol new file mode 100644 index 0000000000..0ea1fa80df --- /dev/null +++ b/test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +/* See contracts/COMPILERS.md */ +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.4.24 <0.9.0; + + +/// @notice The set of restrictions used in the sanity checks of the oracle report +/// @dev struct is loaded from the storage and stored in memory during the tx running +struct LimitsList { + /// @notice The max possible number of validators that might be reported as `exited` + /// per single day, depends on the Consensus Layer churn limit + /// @dev Must fit into uint16 (<= 65_535) + uint256 exitedValidatorsPerDayLimit; + + /// @notice The max possible number of validators that might be reported as `appeared` + /// per single day, limited by the max daily deposits via DepositSecurityModule in practice + /// isn't limited by a consensus layer (because `appeared` includes `pending`, i.e., not `activated` yet) + /// @dev Must fit into uint16 (<= 65_535) + uint256 appearedValidatorsPerDayLimit; + + /// @notice The max annual increase of the total validators' balances on the Consensus Layer + /// since the previous oracle report + /// @dev Represented in the Basis Points (100% == 10_000) + uint256 annualBalanceIncreaseBPLimit; + + /// @notice The max deviation of the provided `simulatedShareRate` + /// and the actual one within the currently processing oracle report + /// @dev Represented in the Basis Points (100% == 10_000) + uint256 simulatedShareRateDeviationBPLimit; + + /// @notice The max number of exit requests allowed in report to ValidatorsExitBusOracle + uint256 maxValidatorExitRequestsPerReport; + + /// @notice The max number of data list items reported to accounting oracle in extra data per single transaction + /// @dev Must fit into uint16 (<= 65_535) + uint256 maxItemsPerExtraDataTransaction; + + /// @notice The max number of node operators reported per extra data list item + /// @dev Must fit into uint16 (<= 65_535) + uint256 maxNodeOperatorsPerExtraDataItem; + + /// @notice The min time required to be passed from the creation of the request to be + /// finalized till the time of the oracle report + uint256 requestTimestampMargin; + + /// @notice The positive token rebase allowed per single LidoOracle report + /// @dev uses 1e9 precision, e.g.: 1e6 - 0.1%; 1e9 - 100%, see `setMaxPositiveTokenRebase()` + uint256 maxPositiveTokenRebase; + + /// @notice Initial slashing amount per one validator to calculate initial slashing of the validators' balances on the Consensus Layer + /// @dev Represented in the PWei (1^15 Wei). Must fit into uint16 (<= 65_535) + uint256 initialSlashingAmountPWei; + + /// @notice Inactivity penalties amount per one validator to calculate penalties of the validators' balances on the Consensus Layer + /// @dev Represented in the PWei (1^15 Wei). Must fit into uint16 (<= 65_535) + uint256 inactivityPenaltiesAmountPWei; + + /// @notice The maximum percent on how Second Opinion Oracle reported value could be greater + /// than reported by the AccountingOracle. There is an assumption that second opinion oracle CL balance + /// can be greater as calculated for the withdrawal credentials. + /// @dev Represented in the Basis Points (100% == 10_000) + uint256 clBalanceOraclesErrorUpperBPLimit; +} + +/// @title Sanity checks for the Lido's oracle report +/// @notice The contracts contain methods to perform sanity checks of the Lido's oracle report +/// and lever methods for granular tuning of the params of the checks +interface IOracleReportSanityChecker_preV3 { + + /// @notice Returns the limits list for the Lido's oracle report sanity checks + function getOracleReportLimits() external view returns (LimitsList memory); +} + diff --git a/test/0.8.25/upgrade/interfaces/IVoting.sol b/test/0.8.25/upgrade/interfaces/IVoting.sol new file mode 100644 index 0000000000..21eaed51a0 --- /dev/null +++ b/test/0.8.25/upgrade/interfaces/IVoting.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.4.24 <0.9.0; + +interface IVoting { + enum VotePhase { + Main, + Objection, + Closed + } + + function getVote(uint256 _voteId) + external + view + returns ( + bool open, + bool executed, + uint64 startDate, + uint64 snapshotBlock, + uint64 supportRequired, + uint64 minAcceptQuorum, + uint256 yea, + uint256 nay, + uint256 votingPower, + bytes memory script, + VotePhase phase + ); + + function newVote( + bytes calldata _executionScript, + string calldata _metadata, + bool, /* _castVote_deprecated */ + bool /* _executesIfDecided_deprecated */ + ) external; +} diff --git a/test/0.8.25/upgrade/utils/CallScriptBuilder.sol b/test/0.8.25/upgrade/utils/CallScriptBuilder.sol new file mode 100644 index 0000000000..7df03751bc --- /dev/null +++ b/test/0.8.25/upgrade/utils/CallScriptBuilder.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +/** + * @title CallsScriptBuilder + * @notice A library for building call scripts in a structured manner. + * + * This library provides utilities to construct Aragon EVM call scripts that can be used + * to execute multiple calls in a single transaction. It is particularly useful + * in governance systems where a series of actions need to be executed atomically. + * + * The library uses a specific format to encode the calls, which includes the + * target address, the length of the data, and the data itself. This format is + * compatible with the Aragon OS call script specification, see SPEC_ID. + */ +library CallsScriptBuilder { + // See https://github.com/aragon/aragonOS/pull/182 + bytes4 internal constant SPEC_ID = 0x00000001; + + struct Context { + bytes _result; // The encoded call script result + } + + function getResult(Context memory self) internal pure returns (bytes memory) { + return self._result; + } + + function create() internal pure returns (Context memory res) { + res._result = bytes.concat(SPEC_ID); + } + + function create(address to, bytes memory data) internal pure returns (Context memory res) { + res = addCall(create(), to, data); + } + + function addCall(Context memory self, address to, bytes memory data) internal pure returns (Context memory) { + self._result = bytes.concat(self._result, bytes20(to), bytes4(uint32(data.length)), data); + return self; + } +} diff --git a/test/0.8.25/upgrade/utils/OmnibusBase.sol b/test/0.8.25/upgrade/utils/OmnibusBase.sol new file mode 100644 index 0000000000..9f62443b9d --- /dev/null +++ b/test/0.8.25/upgrade/utils/OmnibusBase.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {IForwarder} from "../interfaces/IForwarder.sol"; +import {IVoting} from "../interfaces/IVoting.sol"; + +import {CallsScriptBuilder} from "./CallScriptBuilder.sol"; + + +/// @title OmnibusBase +/// @notice Abstract base contract for creating votes for the Aragon Voting. +/// +/// @dev Originates from https://github.com/lidofinance/dual-governance/tree/98216fb2c9150b8111a14b06afd9d6e646f14c20/scripts/upgrade +/// @dev The OmnibusBase contract serves as a foundational layer for creating governance proposals +/// that are compatible with the Aragon Voting framework. It provides a structured approach +/// to define and execute a series of actions (vote items) within a single governance vote. +/// The contract leverages the CallsScriptBuilder library to construct EVM call scripts, +/// ensuring that all actions are executed atomically and in the specified order. +/// @dev This contract is designed to be extended and customized for specific governance +/// scenarios, allowing developers to define complex multi-step proposals that can be +/// executed within the Aragon governance ecosystem. +/// @dev Inheriting contracts are expected to implement the `getVoteItems()` function, which +/// outlines the specific actions to be included in the governance proposal. These actions +/// are encapsulated in the `VoteItem` struct, which includes a human-readable description +/// and the necessary EVM call data. +abstract contract OmnibusBase { + using CallsScriptBuilder for CallsScriptBuilder.Context; + + struct ScriptCall { + address to; + bytes data; + } + + /// @notice A structure that represents a single voting item in a governance proposal. + /// @dev This struct is designed to match the format required by the Lido scripts repository + /// for compatibility with the voting tooling. + /// @param description Human-readable description of the voting item. + /// @param call The EVM script call containing the target contract address and calldata. + struct VoteItem { + string description; + ScriptCall call; + } + + IVoting private immutable VOTING_CONTRACT; + + constructor(address voting) { + VOTING_CONTRACT = IVoting(voting); + } + + /// @return VoteItem[] The list of voting items to be executed by Aragon Voting. + function getVoteItems() public view virtual returns (VoteItem[] memory); + + /// @notice Converts all vote items to the Aragon-compatible EVMCallScript to validate against. + /// @return script A bytes containing encoded EVMCallScript. + function getEVMScript() public view returns (bytes memory) { + CallsScriptBuilder.Context memory scriptBuilder = CallsScriptBuilder.create(); + VoteItem[] memory voteItems = this.getVoteItems(); + + uint256 voteItemsCount = voteItems.length; + for (uint256 i = 0; i < voteItemsCount; i++) { + scriptBuilder.addCall(voteItems[i].call.to, voteItems[i].call.data); + } + + return scriptBuilder.getResult(); + } + + /// @notice Returns the bytecode for creating a new vote on the Aragon Voting contract. + /// @param description The description of the vote. + /// @return newVoteBytecode The bytecode for creating a new vote. + function getNewVoteCallBytecode(string memory description) external view returns (bytes memory newVoteBytecode) { + newVoteBytecode = CallsScriptBuilder.create( + address(VOTING_CONTRACT), abi.encodeCall(VOTING_CONTRACT.newVote, (getEVMScript(), description, false, false)) + )._result; + } + + /// @notice Validates the specific vote on Aragon Voting contract. + /// @return A boolean value indicating whether the vote is valid. + function isValidVoteScript(uint256 voteId) external view returns (bool) { + ( /*open*/ + , /*executed*/ + , /*startDate*/ + , /*snapshotBlock*/ + , /*supportRequired*/ + , /*minAcceptQuorum*/ + , /*yea*/ + , /*nay*/ + , /*votingPower*/ + , + bytes memory script, + /*phase*/ + ) = VOTING_CONTRACT.getVote(voteId); + return keccak256(script) == keccak256(getEVMScript()); + } + + function _votingCall(address target, bytes memory data) internal pure returns (ScriptCall memory) { + return ScriptCall(target, data); + } + + function _forwardCall( + address forwarder, + address target, + bytes memory data + ) internal pure returns (ScriptCall memory) { + return ScriptCall( + forwarder, abi.encodeCall(IForwarder.forward, (CallsScriptBuilder.create(target, data).getResult())) + ); + } +} diff --git a/yarn.lock b/yarn.lock index 71f2b61f46..630dfe28a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1608,6 +1608,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts-v5.2@npm:@openzeppelin/contracts@5.2.0": + version: 5.2.0 + resolution: "@openzeppelin/contracts@npm:5.2.0" + checksum: 10c0/6e2d8c6daaeb8e111d49a82c30997a6c5d4e512338b55500db7fd4340f29c1cbf35f9dcfa0dbc672e417bc84e99f5441a105cb585cd4680ad70cbcf9a24094fc + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:3.4.0": version: 3.4.0 resolution: "@openzeppelin/contracts@npm:3.4.0" @@ -8075,6 +8082,7 @@ __metadata: "@nomicfoundation/ignition-core": "npm:^0.15.11" "@openzeppelin/contracts": "npm:3.4.0" "@openzeppelin/contracts-v4.4": "npm:@openzeppelin/contracts@4.4.1" + "@openzeppelin/contracts-v5.2": "npm:@openzeppelin/contracts@5.2.0" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" "@types/chai": "npm:^4.3.20" From 8c9642310ad35c505ae1b6bee772850d5316149c Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 12 Jun 2025 20:31:06 +0200 Subject: [PATCH 356/405] wip: aragon mock --- deployed-mainnet-upgrade.json | 666 ++++++++++++++++++ lib/deploy.ts | 1 + .../test-scratch-upgrade.sh | 3 + ...eps-voting.json => steps-mock-voting.json} | 0 .../steps/0300-deploy-upgrading-contracts.ts | 54 -- .../upgrade/steps/0500-mock-aragon-voting.ts | 32 + scripts/utils/migrate.ts | 2 + 7 files changed, 704 insertions(+), 54 deletions(-) create mode 100644 deployed-mainnet-upgrade.json rename scripts/upgrade/{steps-voting.json => steps-mock-voting.json} (100%) delete mode 100644 scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts create mode 100644 scripts/upgrade/steps/0500-mock-aragon-voting.ts diff --git a/deployed-mainnet-upgrade.json b/deployed-mainnet-upgrade.json new file mode 100644 index 0000000000..223040b314 --- /dev/null +++ b/deployed-mainnet-upgrade.json @@ -0,0 +1,666 @@ +{ + "TWVoteScript": { + "contract": "test/0.8.25/upgrade/TWVoteScript.sol", + "address": "0xbdD488B78ac2b27052249e60E635B2533575a6Eb", + "constructorArgs": [ + "0x2e59A20f205bB85a89C53f1936454680651E618e", + { + "agent": "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "lido_locator": "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + "lido_locator_impl": "0x16932B0c1eA503E4a40C7a75AC7200b4304C8De2", + "validators_exit_bus_oracle": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", + "validators_exit_bus_oracle_impl": "0x2fcc261bB32262a150E4905F6d550D4FF05bC582", + "triggerable_withdrawals_gateway": "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a", + "withdrawal_vault": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", + "withdrawal_vault_impl": "0x63eE8865A8B25919B5103d02586AaaF078Ee9102", + "accounting_oracle": "0x852deD011285fe67063a08005c71a85690503Cee", + "accounting_oracle_impl": "0x4f0Ab9214649A6539586FbeB575b370Ba52Bd794", + "staking_router": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", + "staking_router_impl": "0x90CA02Cb47113c75EB8E102c91B40181616cc9e9", + "validator_exit_verifier": "0xd15cF95D0DC31C7a01Ac5F73ccca6B572ADc8C05", + "node_operators_registry": "0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5", + "node_operators_registry_impl": "0x4B651dcC3C2e4d2Fa6feF95D73eaEC48432b5d6a", + "oracle_daemon_config": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", + "nor_app_repo": "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + "node_operators_registry_app_id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", + "nor_version": [ + 2, + 0, + 0 + ], + "vebo_consensus_version": 4, + "ao_consensus_version": 4, + "nor_exit_deadline_in_sec": 1800, + "exit_events_lookback_window_in_slots": 7200, + "nor_content_uri": "0x697066733a516d61375058486d456a346a7332676a4d3976744850747176754b3832695335455950694a6d7a4b4c7a55353847" + } + ] + }, + "accountingOracle": { + "deployParameters": { + "consensusVersion": 1 + }, + "proxy": { + "address": "0x852deD011285fe67063a08005c71a85690503Cee", + "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", + "deployTx": "0x3def88f27741216b131de2861cf89af2ca2ac4242b384ee33dca8cc70c51c8dd", + "constructorArgs": [ + "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x" + ] + }, + "implementation": { + "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", + "address": "0x4f0Ab9214649A6539586FbeB575b370Ba52Bd794", + "constructorArgs": [ + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0x442af784A788A5bd6F42A01Ebe9F287a871243fb", + 12, + 1606824023 + ] + } + }, + "apmRegistryFactoryAddress": "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A", + "app:aragon-agent": { + "implementation": { + "contract": "@aragon/apps-agent/contracts/Agent.sol", + "address": "0x3A93C17FC82CC33420d1809dDA9Fb715cc89dd37", + "constructorArgs": [] + }, + "aragonApp": { + "name": "aragon-agent", + "fullName": "aragon-agent.lidopm.eth", + "id": "0x701a4fd1f5174d12a0f1d9ad2c88d0ad11ab6aad0ac72b7d9ce621815f8016a9" + }, + "proxy": { + "address": "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, + "app:aragon-finance": { + "implementation": { + "contract": "@aragon/apps-finance/contracts/Finance.sol", + "address": "0x836835289A2E81B66AE5d95b7c8dBC0480dCf9da", + "constructorArgs": [] + }, + "aragonApp": { + "name": "aragon-finance", + "fullName": "aragon-finance.lidopm.eth", + "id": "0x5c9918c99c4081ca9459c178381be71d9da40e49e151687da55099c49a4237f1" + }, + "proxy": { + "address": "0xB9E5CBB9CA5b0d659238807E84D0176930753d86", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, + "app:aragon-token-manager": { + "implementation": { + "contract": "@aragon/apps-lido/apps/token-manager/contracts/TokenManager.sol", + "address": "0xde3A93028F2283cc28756B3674BD657eaFB992f4", + "constructorArgs": [] + }, + "aragonApp": { + "name": "aragon-token-manager", + "fullName": "aragon-token-manager.lidopm.eth", + "id": "0xcd567bdf93dd0f6acc3bc7f2155f83244d56a65abbfbefb763e015420102c67b" + }, + "proxy": { + "address": "0xf73a1260d222f447210581DDf212D915c09a3249", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, + "app:aragon-voting": { + "implementation": { + "contract": "@aragon/apps-lido/apps/voting/contracts/Voting.sol", + "address": "0x72fb5253AD16307B9E773d2A78CaC58E309d5Ba4", + "constructorArgs": [] + }, + "aragonApp": { + "name": "aragon-voting", + "fullName": "aragon-voting.lidopm.eth", + "id": "0x0abcd104777321a82b010357f20887d61247493d89d2e987ff57bcecbde00e1e" + }, + "proxy": { + "address": "0x2e59A20f205bB85a89C53f1936454680651E618e", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" + } + }, + "app:lido": { + "proxy": { + "address": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + }, + "implementation": { + "address": "0x17144556fd3424EDC8Fc8A4C940B2D04936d17eb", + "contract": "contracts/0.4.24/Lido.sol", + "deployTx": "0xb4b5e02643c9802fd0f7c73c4854c4f1b83497aca13f8297ba67207b71c4dcd9", + "constructorArgs": [] + }, + "aragonApp": { + "fullName": "lido.lidopm.eth", + "name": "lido", + "id": "0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320", + "ipfsCid": "QmQkJMtvu4tyJvWrPXJfjLfyTWn959iayyNjp7YqNzX7pS", + "contentURI": "0x697066733a516d516b4a4d7476753474794a76577250584a666a4c667954576e393539696179794e6a703759714e7a58377053" + } + }, + "app:node-operators-registry": { + "proxy": { + "address": "0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5" + }, + "implementation": { + "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", + "address": "0x4B651dcC3C2e4d2Fa6feF95D73eaEC48432b5d6a", + "constructorArgs": [] + }, + "aragonApp": { + "fullName": "node-operators-registry.lidopm.eth", + "name": "node-operators-registry", + "id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", + "ipfsCid": "Qma7PXHmEj4js2gjM9vtHPtqvuK82iS5EYPiJmzKLzU58G", + "contentURI": "0x697066733a516d61375058486d456a346a7332676a4d3976744850747176754b3832695335455950694a6d7a4b4c7a55353847" + } + }, + "app:oracle": { + "proxy": { + "address": "0x442af784A788A5bd6F42A01Ebe9F287a871243fb" + }, + "implementation": { + "address": "0xa29b819654cE6224A222bb5f586920105E2D7E0E", + "contract": "contracts/0.4.24/oracle/LegacyOracle.sol", + "deployTx": "0xe666e3ce409bb4c18e1016af0b9ed3495b20361a69f2856bccb9e67599795b6f", + "constructorArgs": [] + }, + "aragonApp": { + "fullName": "oracle.lidopm.eth", + "name": "oracle", + "id": "0x8b47ba2a8454ec799cd91646e7ec47168e91fd139b23f017455f3e5898aaba93", + "ipfsCid": "QmUMPfiEKq5Mxm8y2GYQPLujGaJiWz1tvep5W7EdAGgCR8", + "contentURI": "0x697066733a516d656138394d5533504852503763513157616b3672327355654d554146324c39727132624c6d5963644b764c57" + } + }, + "app:simple-dvt": { + "stakingRouterModuleParams": { + "moduleName": "SimpleDVT", + "moduleType": "curated-onchain-v1", + "targetShare": 50, + "moduleFee": 800, + "treasuryFee": 200, + "penaltyDelay": 432000, + "easyTrackTrustedCaller": "0x08637515E85A4633E23dfc7861e2A9f53af640f7", + "easyTrackAddress": "0xF0211b7660680B49De1A7E9f25C65660F0a13Fea", + "easyTrackFactories": { + "AddNodeOperators": "0xcAa3AF7460E83E665EEFeC73a7a542E5005C9639", + "ActivateNodeOperators": "0xCBb418F6f9BFd3525CE6aADe8F74ECFEfe2DB5C8", + "DeactivateNodeOperators": "0x8B82C1546D47330335a48406cc3a50Da732672E7", + "SetVettedValidatorsLimits": "0xD75778b855886Fc5e1eA7D6bFADA9EB68b35C19D", + "SetNodeOperatorNames": "0x7d509BFF310d9460b1F613e4e40d342201a83Ae4", + "SetNodeOperatorRewardAddresses": "0x589e298964b9181D9938B84bB034C3BB9024E2C0", + "UpdateTargetValidatorLimits": "0x41CF3DbDc939c5115823Fba1432c4EC5E7bD226C", + "ChangeNodeOperatorManagers": "0xE31A0599A6772BCf9b2bFc9e25cf941e793c9a7D" + } + }, + "aragonApp": { + "name": "simple-dvt", + "fullName": "simple-dvt.lidopm.eth", + "id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4", + "ipfsCid": "QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo", + "contentURI": "0x697066733a516d615353756a484347636e4675657441504777565735426567614d42766e355343736769334c5366767261536f" + }, + "proxy": { + "address": "0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433", + "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol", + "constructorArgs": [ + "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", + "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4", + "0x" + ] + }, + "implementation": { + "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", + "address": "0x1770044a38402e3CfCa2Fcfa0C84a093c9B42135", + "constructorArgs": [] + } + }, + "aragon-kernel": { + "implementation": { + "contract": "@aragon/os/contracts/kernel/Kernel.sol", + "address": "0x2b33CF282f867A7FF693A66e11B0FcC5552e4425", + "constructorArgs": [ + true + ] + }, + "proxy": { + "address": "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", + "contract": "@aragon/os/contracts/kernel/KernelProxy.sol" + } + }, + "aragonIDAddress": "0x546aa2eae2514494eeadb7bbb35243348983c59d", + "burner": { + "address": "0xD15a672319Cf0352560eE76d9e89eAB0889046D3", + "contract": "contracts/0.8.9/Burner.sol", + "deployTx": "0xbebf5c85404a0d8e36b859046c984fdf6dd764b5d317feb7eb3525016005b1d9", + "constructorArgs": [ + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0", + "32145684728326685744" + ], + "deployParameters": { + "totalCoverSharesBurnt": "0", + "totalNonCoverSharesBurnt": "32145684728326685744" + } + }, + "chainSpec": { + "depositContractAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", + "slotsPerEpoch": 32, + "secondsPerSlot": 12, + "genesisTime": 1606824023 + }, + "createAppReposTx": "0xf48cb21c6be021dd18bd8e02ce89ac7b924245b859f0a8b7c47e88a39016ed41", + "daoAragonId": "lido-dao", + "daoFactoryAddress": "0x7378ad1ba8f3c8e64bbb2a04473edd35846360f1", + "daoInitialSettings": { + "token": { + "name": "Lido DAO Token", + "symbol": "LDO" + }, + "voting": { + "minSupportRequired": "500000000000000000", + "minAcceptanceQuorum": "50000000000000000", + "voteDuration": 86400 + }, + "fee": { + "totalPercent": 10, + "treasuryPercent": 0, + "insurancePercent": 50, + "nodeOperatorsPercent": 50 + } + }, + "daoTokenAddress": "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + "deployCommit": "e45c4d6fb8120fd29426b8d969c19d8a798ca974", + "deployer": "0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA", + "depositSecurityModule": { + "address": "0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD", + "contract": "contracts/0.8.9/DepositSecurityModule.sol", + "deployTx": "0x21307a2321f167f99de11ccec86d7bdd8233481bbffa493e15c519ca8d662c4f", + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0x00000000219ab540356cBB839Cbe05303d7705Fa", + "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", + 6646, + 200 + ], + "deployParameters": { + "maxDepositsPerBlock": 150, + "minDepositBlockDistance": 25, + "pauseIntentValidityPeriodBlocks": 6646 + } + }, + "dummyEmptyContract": { + "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", + "contract": "contracts/0.8.9/utils/DummyEmptyContract.sol", + "deployTx": "0x9d76786f639bd18365f10c087444761db5dafd0edc85c5c1a3e90219f2d1331d", + "constructorArgs": [] + }, + "eip712StETH": { + "address": "0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7", + "contract": "contracts/0.8.9/EIP712StETH.sol", + "deployTx": "0xecb5010620fb13b0e2bbc98b8a0c82de0d7385491452cd36cf303cd74216ed91", + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + ] + }, + "ensAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + "executionLayerRewardsVault": { + "address": "0x388C818CA8B9251b393131C08a736A67ccB19297", + "contract": "contracts/0.8.9/LidoExecutionLayerRewardsVault.sol", + "deployTx": "0xd72cf25e4a5fe3677b6f9b2ae13771e02ad66f8d2419f333bb8bde3147bd4294" + }, + "hashConsensusForAccountingOracle": { + "address": "0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288", + "contract": "contracts/0.8.9/oracle/HashConsensus.sol", + "deployTx": "0xd74dcca9bacede9f332d70562f49808254061853937ffbbfc7397ab5d017041a", + "constructorArgs": [ + 32, + 12, + 1606824023, + 225, + 100, + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x852deD011285fe67063a08005c71a85690503Cee" + ], + "deployParameters": { + "fastLaneLengthSlots": 100, + "epochsPerFrame": 225 + } + }, + "hashConsensusForValidatorsExitBusOracle": { + "address": "0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a", + "contract": "contracts/0.8.9/oracle/HashConsensus.sol", + "deployTx": "0xed1ab73dd5458b5ec0b174508318d2f39a31029112af21f87d09106933bd3a9e", + "constructorArgs": [ + 32, + 12, + 1606824023, + 75, + 100, + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e" + ], + "deployParameters": { + "fastLaneLengthSlots": 100, + "epochsPerFrame": 75 + } + }, + "ipfsAPI": "https://ipfs.infura.io:5001/api/v0", + "lidoApm": { + "deployTx": "0xfa66476569ecef5790f2d0634997b952862bbca56aa088f151b8049421eeb87b", + "address": "0x0cb113890b04B49455DfE06554e2D784598A29C9" + }, + "lidoApmEnsName": "lidopm.eth", + "lidoApmEnsRegDurationSec": 94608000, + "lidoLocator": { + "proxy": { + "address": "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", + "deployTx": "0x3a2910624533935cc8c21837b1705bcb159a760796930097016186be705cc455", + "constructorArgs": [ + "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x" + ] + }, + "implementation": { + "contract": "contracts/0.8.9/LidoLocator.sol", + "address": "0x16932B0c1eA503E4a40C7a75AC7200b4304C8De2", + "constructorArgs": [ + [ + "0x852deD011285fe67063a08005c71a85690503Cee", + "0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD", + "0x388C818CA8B9251b393131C08a736A67ccB19297", + "0x442af784A788A5bd6F42A01Ebe9F287a871243fb", + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0x6232397ebac4f5772e53285B26c47914E9461E75", + "0xe6793B9e4FbA7DE0ee833F9D02bba7DB5EB27823", + "0xD15a672319Cf0352560eE76d9e89eAB0889046D3", + "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", + "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", + "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", + "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", + "0xd15cF95D0DC31C7a01Ac5F73ccca6B572ADc8C05", + "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a" + ] + ] + } + }, + "lidoTemplate": { + "contract": "contracts/0.4.24/template/LidoTemplate.sol", + "address": "0x752350797CB92Ad3BF1295Faf904B27585e66BF5", + "deployTx": "0xdcd4ebe028aa3663a1fe8bbc92ae8489045e29d2a6ef5284083d9be5c3fa5f19", + "constructorArgs": [ + "0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA", + "0x7378ad1ba8f3c8e64bbb2a04473edd35846360f1", + "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + "0x909d05f384d0663ed4be59863815ab43b4f347ec", + "0x546aa2eae2514494eeadb7bbb35243348983c59d", + "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A" + ] + }, + "minFirstAllocationStrategy": { + "contract": "contracts/common/lib/MinFirstAllocationStrategy.sol", + "address": "0x7e70De6D1877B3711b2bEDa7BA00013C7142d993", + "constructorArgs": [] + }, + "miniMeTokenFactoryAddress": "0x909d05f384d0663ed4be59863815ab43b4f347ec", + "networkId": 1, + "newDaoTx": "0x3feabd79e8549ad68d1827c074fa7123815c80206498946293d5373a160fd866", + "oracleDaemonConfig": { + "address": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", + "contract": "contracts/0.8.9/OracleDaemonConfig.sol", + "deployTx": "0xa4f380b8806f5a504ef67fce62989e09be5a48bf114af63483c01c22f0c9a36f", + "constructorArgs": [ + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + [] + ], + "deployParameters": { + "NORMALIZED_CL_REWARD_PER_EPOCH": 64, + "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000, + "REBASE_CHECK_NEAREST_EPOCH_DISTANCE": 1, + "REBASE_CHECK_DISTANT_EPOCH_DISTANCE": 23, + "VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS": 7200, + "VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS": 28800, + "PREDICTION_DURATION_IN_SLOTS": 50400, + "FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT": 1350, + "NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP": 100 + } + }, + "oracleReportSanityChecker": { + "address": "0x6232397ebac4f5772e53285B26c47914E9461E75", + "contract": "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol", + "deployTx": "0x700c83996ad7deefda286044280ad86108dfef9c880909bd8e75a3746f7d631c", + "constructorArgs": [ + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + [ + 9000, + 43200, + 1000, + 50, + 600, + 8, + 24, + 7680, + 750000, + 1000, + 101, + 50 + ] + ], + "deployParameters": { + "churnValidatorsPerDayLimit": 20000, + "oneOffCLBalanceDecreaseBPLimit": 500, + "annualBalanceIncreaseBPLimit": 1000, + "simulatedShareRateDeviationBPLimit": 50, + "maxValidatorExitRequestsPerReport": 600, + "maxAccountingExtraDataListItemsCount": 2, + "maxNodeOperatorsPerExtraDataItemCount": 100, + "requestTimestampMargin": 7680, + "maxPositiveTokenRebase": 750000 + } + }, + "scratchDeployGasUsed": "24930001", + "stakingRouter": { + "proxy": { + "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", + "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", + "deployTx": "0xb8620f04a8db6bb52cfd0978c6677a5f16011e03d4622e5d660ea6ba34c2b122", + "constructorArgs": [ + "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x" + ] + }, + "implementation": { + "contract": "contracts/0.8.9/StakingRouter.sol", + "address": "0x90CA02Cb47113c75EB8E102c91B40181616cc9e9", + "constructorArgs": [ + "0x00000000219ab540356cBB839Cbe05303d7705Fa" + ] + } + }, + "triggerableWithdrawalsGateway": { + "implementation": { + "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", + "address": "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a", + "constructorArgs": [ + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + 13000, + 1, + 48 + ] + } + }, + "validatorExitDelayVerifier": { + "implementation": { + "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", + "address": "0xd15cF95D0DC31C7a01Ac5F73ccca6B572ADc8C05", + "constructorArgs": [ + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + "0x0000000000000000000000000000000000000000000000000096000000000028", + "0x0000000000000000000000000000000000000000000000000096000000000028", + "0x0000000000000000000000000000000000000000000000000000000000005b00", + "0x0000000000000000000000000000000000000000000000000000000000005b00", + 1, + 1, + 32, + 12, + 1639659600, + 98304 + ] + } + }, + "validatorsExitBusOracle": { + "proxy": { + "address": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", + "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", + "deployTx": "0xef3eea1523d2161c2f36ba61e327e3520231614c055b8a88c7f5928d18e423ea", + "constructorArgs": [ + "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x" + ] + }, + "implementation": { + "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", + "address": "0x2fcc261bB32262a150E4905F6d550D4FF05bC582", + "constructorArgs": [ + 12, + 1639659600, + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb" + ] + } + }, + "vestingParams": { + "unvestedTokensAmount": "363197500000000000000000000", + "holders": { + "0x9Bb75183646e2A0DC855498bacD72b769AE6ceD3": "20000000000000000000000000", + "0x0f89D54B02ca570dE82F770D33c7B7Cf7b3C3394": "25000000000000000000000000", + "0xe49f68B9A01d437B0b7ea416376a7AB21532624e": "2282000000000000000000000", + "0xb842aFD82d940fF5D8F6EF3399572592EBF182B0": "17718000000000000000000000", + "0x9849c2C1B73B41AEE843A002C332a2d16aaaB611": "10000000000000000000000000", + "0x96481cb0fcd7673254ebccc42dce9b92da10ea04": "5000000000000000000000000", + "0xB3DFe140A77eC43006499CB8c2E5e31975caD909": "7500000000000000000000000", + "0x61C808D82A3Ac53231750daDc13c777b59310bD9": "20000000000000000000000000", + "0x447f95026107aaed7472A0470931e689f51e0e42": "20000000000000000000000000", + "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D": "2222222220000000000000000", + "0xC24da173A250e9Ca5c54870639EbE5f88be5102d": "17777777780000000000000000", + "0x1f3813fE7ace2a33585F1438215C7F42832FB7B3": "20000000000000000000000000", + "0x82a8439BA037f88bC73c4CCF55292e158A67f125": "7000000000000000000000000", + "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E": "15000000000000000000000000", + "0xB5587A54fF7022AC218438720BDCD840a32f0481": "5000000000000000000000000", + "0xf5fb27b912d987b5b6e02a1b1be0c1f0740e2c6f": "2000000000000000000000000", + "0x8b1674a617F103897Fb82eC6b8EB749BA0b9765B": "15000000000000000000000000", + "0x48Acf41D10a063f9A6B718B9AAd2e2fF5B319Ca2": "5000000000000000000000000", + "0x7eE09c11D6Dc9684D6D5a4C6d333e5b9e336bb6C": "10000000000000000000000000", + "0x11099aC9Cc097d0C9759635b8e16c6a91ECC43dA": "2000000000000000000000000", + "0x3d4AD2333629eE478E4f522d60A56Ae1Db5D3Cdb": "5000000000000000000000000", + "0xd5eCB56c6ca8f8f52D2DB4dC1257d6161cf3Da29": "100000000000000000000000", + "0x7F5e13a815EC9b4466d283CD521eE9829e7F6f0e": "200000000000000000000000", + "0x2057cbf2332ab2697a52B8DbC85756535d577e32": "500000000000000000000000", + "0x537dfB5f599A3d15C50E2d9270e46b808A52559D": "1000000000000000000000000", + "0x33c4c38e96337172d3de39df82060de26b638c4b": "550000000000000000000000", + "0x6094E1Dd925caCe56Fa501dAEc02b01a49E55770": "300000000000000000000000", + "0x977911f476B28f9F5332fA500387deE81e480a44": "40000000000000000000000", + "0x66d3FdA643320c6DddFBba39e635288A5dF75FB9": "200000000000000000000000", + "0xDFC0ae54af992217100845597982274A26d8CB28": "12500000000000000000000", + "0x32254b28F793CC18B3575C86c61fE3D7421cbbef": "500000000000000000000000", + "0x0Bf5566fB5F1f9934a3944AEF128a1b1a8cF3f17": "50000000000000000000000", + "0x1d3Fa8bf35870271115B997b8eCFe18529422a16": "50000000000000000000000", + "0x366B9729C5A89EC4618A0AB95F832E411eaE8237": "200000000000000000000000", + "0x20921142A35c89bE5D002973d2D6B72d9a625FB0": "200000000000000000000000", + "0x663b91628674846e8D1CBB779EFc8202d86284E2": "7500000000000000000000000", + "0xa6829908f728C6bC5627E2aFe93a0B71E978892D": "300000000000000000000000", + "0x9575B7859DF77F2A0EF034339b80e24dE44AB3F6": "200000000000000000000000", + "0xEe217c23131C6F055F7943Ef1f80Bec99dF35244": "400000000000000000000000", + "0xadde043f556d1083f060A7298E79eaBa08A3a077": "400000000000000000000000", + "0xaFBEfC8401c885A0bb6Ea6Af43f592A015433C65": "200000000000000000000000", + "0x8a62A63b877877bd5B1209B9b67F3d2685284268": "200000000000000000000000", + "0x62Ac238Ac055017DEcAb645E7E56176749f316d0": "200000000000000000000000", + "0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA": "5000000000000000000000000", + "0x8D689476EB446a1FB0065bFFAc32398Ed7F89165": "10000000000000000000000000", + "0x083fc10cE7e97CaFBaE0fE332a9c4384c5f54E45": "5000000000000000000000000", + "0x0028E24e4Fe5184792Bd0Cf498C11AE5b76185f5": "5000000000000000000000000", + "0xFe45baf0F18c207152A807c1b05926583CFE2e4b": "5000000000000000000000000", + "0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE": "5000000000000000000000000", + "0xD7f0dDcBb0117A53e9ff2cad814B8B810a314f59": "5000000000000000000000000", + "0xb8d83908AAB38a159F3dA47a59d84dB8e1838712": "50000000000000000000000000", + "0xA2dfC431297aee387C05bEEf507E5335E684FbCD": "50000000000000000000000000", + "0x1597D19659F3DE52ABd475F7D2314DCca29359BD": "50000000000000000000000000", + "0x695C388153bEa0fbE3e1C049c149bAD3bc917740": "50000000000000000000000000", + "0x945755dE7EAc99008c8C57bdA96772d50872168b": "50000000000000000000000000", + "0xFea88380bafF95e85305419eB97247981b1a8eEE": "30000000000000000000000000", + "0xAD4f7415407B83a081A0Bee22D05A8FDC18B42da": "50000000000000000000000000", + "0x68335B3ac272C8238b722963368F87dE736b64D6": "5000000000000000000000000", + "0xfA2Ab7C161Ef7F83194498f36ca7aFba90FD08d4": "5000000000000000000000000", + "0x58A764028350aB15899fDCcAFFfd3940e602CEEA": "10000000000000000000000000" + }, + "start": 1639785600, + "cliff": 1639785600, + "end": 1671321600, + "revokable": false + }, + "withdrawalQueueERC721": { + "proxy": { + "address": "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", + "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", + "deployTx": "0x98c2170be034f750f5006cb69ea0aeeaf0858b11f6324ee53d582fa4dd49bc1a", + "constructorArgs": [ + "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + "0x" + ] + }, + "implementation": { + "address": "0xE42C659Dc09109566720EA8b2De186c2Be7D94D9", + "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol", + "deployTx": "0x6ab0151735c01acdef518421358d41a08752169bc383c57d57f5bfa135ac6eb1", + "constructorArgs": [ + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "Lido: stETH Withdrawal NFT", + "unstETH" + ], + "deployParameters": { + "name": "Lido: stETH Withdrawal NFT", + "symbol": "unstETH" + } + } + }, + "withdrawalVault": { + "proxy": { + "address": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f" + }, + "implementation": { + "contract": "contracts/0.8.9/WithdrawalVault.sol", + "address": "0x63eE8865A8B25919B5103d02586AaaF078Ee9102", + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a" + ] + } + }, + "wstETH": { + "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "contract": "contracts/0.6.12/WstETH.sol", + "deployTx": "0xaf2c1a501d2b290ef1e84ddcfc7beb3406f8ece2c46dee14e212e8233654ff05", + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + ] + } +} diff --git a/lib/deploy.ts b/lib/deploy.ts index 1d2d5b04af..cab7d1c96f 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -120,6 +120,7 @@ export async function deployWithoutProxy( if (withStateFile) { const contractPath = await getContractPath(artifactName); + console.log(`Contract path: ${contractPath}`, nameInState); updateObjectInState(nameInState, { contract: contractPath, [addressFieldName]: contract.address, diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh index 642eeb0672..faec073c89 100644 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh @@ -20,3 +20,6 @@ export NETWORK_STATE_FILE=deployed-mainnet-upgrade.json cp deployed-mainnet.json $NETWORK_STATE_FILE yarn upgrade:deploy +yarn upgrade:mock-voting +yarn hardhat --network local run --no-compile scripts/utils/mine.ts +# yarn test:integration diff --git a/scripts/upgrade/steps-voting.json b/scripts/upgrade/steps-mock-voting.json similarity index 100% rename from scripts/upgrade/steps-voting.json rename to scripts/upgrade/steps-mock-voting.json diff --git a/scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts b/scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts deleted file mode 100644 index 3e5401b203..0000000000 --- a/scripts/upgrade/steps/0300-deploy-upgrading-contracts.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ethers } from "hardhat"; -import { readUpgradeParameters } from "scripts/utils/upgrade"; - -import { IAragonAppRepo, IOssifiableProxy, OssifiableProxy__factory } from "typechain-types"; - -import { loadContract } from "lib/contract"; -import { deployWithoutProxy } from "lib/deploy"; -import { readNetworkState, Sk } from "lib/state-file"; - -export async function main() { - const deployerSigner = await ethers.provider.getSigner(); - const deployer = deployerSigner.address; - const state = readNetworkState(); - const parameters = readUpgradeParameters(); - - const locator = OssifiableProxy__factory.connect(state[Sk.lidoLocator].proxy.address, deployerSigner); - const oldLocatorImplementation = await locator.proxy__getImplementation(); - const accountingOracle = await loadContract( - "IOssifiableProxy", - state[Sk.accountingOracle].proxy.address, - ); - const lidoRepo = await loadContract("IAragonAppRepo", state[Sk.aragonLidoAppRepo].proxy.address); - const [, lidoImplementation] = await lidoRepo.getLatest(); - - const addressesParams = [ - // Old implementations - oldLocatorImplementation, - lidoImplementation, - await accountingOracle.proxy__getImplementation(), - - // New implementations - state[Sk.lidoLocator].implementation.address, - state[Sk.appLido].implementation.address, - state[Sk.accountingOracle].implementation.address, - - // New fancy proxy and blueprint contracts - state[Sk.stakingVaultBeacon].address, - state[Sk.stakingVaultImplementation].address, - state[Sk.dashboardImpl].address, - - // Existing proxies and contracts - state[Sk.aragonKernel].proxy.address, - state[Sk.appAgent].proxy.address, - state[Sk.aragonLidoAppRepo].proxy.address, - state[Sk.lidoLocator].proxy.address, - state[Sk.appVoting].proxy.address, - ]; - - const template = await deployWithoutProxy(Sk.v3Template, "V3Template", deployer, [addressesParams]); - - await deployWithoutProxy(Sk.v3VoteScript, "V3VoteScript", deployer, [ - [template.address, parameters[Sk.appLido].newVersion, state[Sk.appLido].aragonApp.id], - ]); -} diff --git a/scripts/upgrade/steps/0500-mock-aragon-voting.ts b/scripts/upgrade/steps/0500-mock-aragon-voting.ts new file mode 100644 index 0000000000..f41595647d --- /dev/null +++ b/scripts/upgrade/steps/0500-mock-aragon-voting.ts @@ -0,0 +1,32 @@ +import { TokenManager, TWVoteScript, Voting } from "typechain-types"; + +import { advanceChainTime, ether, log } from "lib"; +import { impersonate } from "lib/account"; +import { loadContract } from "lib/contract"; +import { readNetworkState, Sk } from "lib/state-file"; + +export async function main(): Promise { + const state = readNetworkState(); + log("Starting mock Aragon voting..."); + const agentAddress = state[Sk.appAgent].proxy.address; + const votingAddress = state[Sk.appVoting].proxy.address; + const tokenManagerAddress = state[Sk.appTokenManager].proxy.address; + + const deployer = await impersonate(agentAddress, ether("100")); + + const voteScript = await loadContract("TWVoteScript", state[Sk.TWVoteScript].address); + const tokenManager = await loadContract("TokenManager", tokenManagerAddress); + const voting = await loadContract("Voting", votingAddress); + + const voteId = await voting.votesLength(); + + const newVoteBytecode = await voteScript.getNewVoteCallBytecode("V3 Lido Upgrade description placeholder"); + await tokenManager.connect(deployer).forward(newVoteBytecode); + if (!(await voteScript.isValidVoteScript(voteId))) throw new Error("Vote script is not valid"); + await voting.connect(deployer).vote(voteId, true, false); + await advanceChainTime(await voting.voteTime()); + const executeTx = await voting.executeVote(voteId); + + const executeReceipt = await executeTx.wait(); + log.success("Voting executed: gas used", executeReceipt!.gasUsed); +} diff --git a/scripts/utils/migrate.ts b/scripts/utils/migrate.ts index 726a104903..ce94d239e5 100644 --- a/scripts/utils/migrate.ts +++ b/scripts/utils/migrate.ts @@ -2,8 +2,10 @@ import { applyMigrationScript, loadSteps, log, resolveMigrationFile } from "lib" const runMigrations = async (stepsFile: string): Promise => { const steps = loadSteps(stepsFile); + console.log(`Loaded ${steps.length} migration steps from ${stepsFile}`); for (const step of steps) { const migrationFile = resolveMigrationFile(step); + console.log(`Applying migration: ${migrationFile}`); await applyMigrationScript(migrationFile); } process.exit(0); From 67bc2f02e64b0dfb5a873cbf926f81c1509d1bdd Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 12 Jun 2025 21:20:58 +0200 Subject: [PATCH 357/405] feat: aragon voting for tw --- deployed-mainnet-upgrade.json | 4 +- .../test-scratch-upgrade.sh | 2 +- .../upgrade/steps/0100-deploy-tw-contracts.ts | 32 +++++++++- .../upgrade/steps/0500-mock-aragon-voting.ts | 4 +- test/0.8.25/upgrade/TWVoteScript.sol | 64 +++++++------------ 5 files changed, 60 insertions(+), 46 deletions(-) diff --git a/deployed-mainnet-upgrade.json b/deployed-mainnet-upgrade.json index 223040b314..f21eefd97c 100644 --- a/deployed-mainnet-upgrade.json +++ b/deployed-mainnet-upgrade.json @@ -24,7 +24,7 @@ "nor_app_repo": "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", "node_operators_registry_app_id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", "nor_version": [ - 2, + 6, 0, 0 ], @@ -473,7 +473,7 @@ "maxPositiveTokenRebase": 750000 } }, - "scratchDeployGasUsed": "24930001", + "scratchDeployGasUsed": "24965663", "stakingRouter": { "proxy": { "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh index faec073c89..86e88f69b9 100644 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh @@ -21,5 +21,5 @@ cp deployed-mainnet.json $NETWORK_STATE_FILE yarn upgrade:deploy yarn upgrade:mock-voting -yarn hardhat --network local run --no-compile scripts/utils/mine.ts +yarn hardhat --network custom run --no-compile scripts/utils/mine.ts # yarn test:integration diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index 94e45caf16..a4041c53f1 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -172,6 +172,36 @@ NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" `); + console.log(state[Sk.appVoting].proxy.address, + { + // Contract addresses + agent: agent, + lido_locator: state[Sk.lidoLocator].proxy.address, + lido_locator_impl: lidoLocator.address, + validators_exit_bus_oracle: await locator.validatorsExitBusOracle(), + validators_exit_bus_oracle_impl: validatorsExitBusOracle.address, + triggerable_withdrawals_gateway: triggerableWithdrawalsGateway.address, + withdrawal_vault: await locator.withdrawalVault(), + withdrawal_vault_impl: withdrawalVault.address, + accounting_oracle: await locator.accountingOracle(), + accounting_oracle_impl: accountingOracle.address, + staking_router: await locator.stakingRouter(), + staking_router_impl: stakingRouterAddress.address, + validator_exit_verifier: validatorExitDelayVerifier.address, + node_operators_registry: state[Sk.appNodeOperatorsRegistry].proxy.address, + node_operators_registry_impl: NOR.address, + oracle_daemon_config: await locator.oracleDaemonConfig(), + nor_app_repo: "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + + // Other parameters + node_operators_registry_app_id: state[Sk.appNodeOperatorsRegistry].aragonApp.id, + nor_version: parameters[Sk.appNodeOperatorsRegistry]?.newVersion || [2, 0, 0], + vebo_consensus_version: 4, + ao_consensus_version: 4, + nor_exit_deadline_in_sec: 30 * 60, // 30 minutes + exit_events_lookback_window_in_slots: 7200, + nor_content_uri: state[Sk.appNodeOperatorsRegistry].aragonApp.contentURI, + }) await deployWithoutProxy(Sk.TWVoteScript, "TWVoteScript", deployer, [ state[Sk.appVoting].proxy.address, @@ -197,7 +227,7 @@ TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" // Other parameters node_operators_registry_app_id: state[Sk.appNodeOperatorsRegistry].aragonApp.id, - nor_version: parameters[Sk.appNodeOperatorsRegistry]?.newVersion || [2, 0, 0], + nor_version: [6, 0, 0], vebo_consensus_version: 4, ao_consensus_version: 4, nor_exit_deadline_in_sec: 30 * 60, // 30 minutes diff --git a/scripts/upgrade/steps/0500-mock-aragon-voting.ts b/scripts/upgrade/steps/0500-mock-aragon-voting.ts index f41595647d..8ec3829464 100644 --- a/scripts/upgrade/steps/0500-mock-aragon-voting.ts +++ b/scripts/upgrade/steps/0500-mock-aragon-voting.ts @@ -19,8 +19,8 @@ export async function main(): Promise { const voting = await loadContract("Voting", votingAddress); const voteId = await voting.votesLength(); - - const newVoteBytecode = await voteScript.getNewVoteCallBytecode("V3 Lido Upgrade description placeholder"); + console.log(await voteScript.getDebugParams()) + const newVoteBytecode = await voteScript.getNewVoteCallBytecode("TW Lido Upgrade description placeholder"); await tokenManager.connect(deployer).forward(newVoteBytecode); if (!(await voteScript.isValidVoteScript(voteId))) throw new Error("Vote script is not valid"); await voting.connect(deployer).vote(voteId, true, false); diff --git a/test/0.8.25/upgrade/TWVoteScript.sol b/test/0.8.25/upgrade/TWVoteScript.sol index dae7af929c..a3aab0cc8f 100644 --- a/test/0.8.25/upgrade/TWVoteScript.sol +++ b/test/0.8.25/upgrade/TWVoteScript.sol @@ -39,7 +39,7 @@ interface INodeOperatorsRegistry { } interface IOracleDaemonConfig { - function set(string calldata _key, uint256 _value) external; + function set(string calldata _key, bytes calldata _value) external; function unset(string calldata _key) external; } @@ -96,21 +96,7 @@ contract TWVoteScript is OmnibusBase { function getVoteItems() public view override returns (VoteItem[] memory voteItems) { voteItems = new VoteItem[](VOTE_ITEMS_COUNT); - - // Create vote items in smaller batches to reduce stack depth - createVoteItems1_1(voteItems, 0); // Items 1-3 - createVoteItems1_2(voteItems, 3); // Items 4-7 - createVoteItems2_1(voteItems, 7); // Items 8-10 - createVoteItems2_2(voteItems, 10); // Items 11-14 - createVoteItems3_1(voteItems, 14); // Items 15-18 - createVoteItems3_2(voteItems, 18); // Items 19-22 - - // Ensure we have created exactly the expected number of items - assert(voteItems.length == VOTE_ITEMS_COUNT); - } - - function createVoteItems1_1(VoteItem[] memory voteItems, uint256 startIndex) internal view { - uint256 index = startIndex; + uint256 index = 0; // 1. Update locator implementation voteItems[index++] = VoteItem({ @@ -140,10 +126,6 @@ contract TWVoteScript is OmnibusBase { abi.encodeCall(IOracleContract.finalizeUpgrade_v2, (600, 13000, 1, 48)) ) }); - } - - function createVoteItems1_2(VoteItem[] memory voteItems, uint256 startIndex) internal view { - uint256 index = startIndex; // 4. Grant VEBO role MANAGE_CONSENSUS_VERSION_ROLE to the AGENT bytes32 manageConsensusVersionRole = keccak256("MANAGE_CONSENSUS_VERSION_ROLE"); @@ -185,10 +167,6 @@ contract TWVoteScript is OmnibusBase { abi.encodeCall(IWithdrawalVaultProxy.proxy_upgradeTo, (params.withdrawal_vault_impl, "")) ) }); - } - - function createVoteItems2_1(VoteItem[] memory voteItems, uint256 startIndex) internal view { - uint256 index = startIndex; // 8. Call finalizeUpgrade_v2 on WithdrawalVault voteItems[index++] = VoteItem({ @@ -210,7 +188,6 @@ contract TWVoteScript is OmnibusBase { }); // 10. Grant AO MANAGE_CONSENSUS_VERSION_ROLE to the AGENT - bytes32 manageConsensusVersionRole = keccak256("MANAGE_CONSENSUS_VERSION_ROLE"); voteItems[index++] = VoteItem({ description: "10. Grant AO MANAGE_CONSENSUS_VERSION_ROLE to the AGENT", call: _forwardCall( @@ -219,10 +196,6 @@ contract TWVoteScript is OmnibusBase { abi.encodeCall(IAccessControl.grantRole, (manageConsensusVersionRole, params.agent)) ) }); - } - - function createVoteItems2_2(VoteItem[] memory voteItems, uint256 startIndex) internal view { - uint256 index = startIndex; // 11. Bump AO consensus version voteItems[index++] = VoteItem({ @@ -265,10 +238,6 @@ contract TWVoteScript is OmnibusBase { abi.encodeCall(IAccessControl.grantRole, (reportValidatorExitTriggeredRole, params.triggerable_withdrawals_gateway)) ) }); - } - - function createVoteItems3_1(VoteItem[] memory voteItems, uint256 startIndex) internal view { - uint256 index = startIndex; // 15. Publish new NodeOperatorsRegistry implementation in NodeOperatorsRegistry app APM repo voteItems[index++] = VoteItem({ @@ -283,9 +252,9 @@ contract TWVoteScript is OmnibusBase { voteItems[index++] = VoteItem({ description: "16. Update NodeOperatorsRegistry implementation", call: _votingCall( - params.agent, + 0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc, abi.encodeWithSignature("setApp(bytes32,bytes32,address)", - IKernel(address(0)).APP_BASES_NAMESPACE(), + IKernel(0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc).APP_BASES_NAMESPACE(), params.node_operators_registry_app_id, params.node_operators_registry_impl ) @@ -311,10 +280,6 @@ contract TWVoteScript is OmnibusBase { abi.encodeCall(IAccessControl.grantRole, (configManagerRole, params.agent)) ) }); - } - - function createVoteItems3_2(VoteItem[] memory voteItems, uint256 startIndex) internal view { - uint256 index = startIndex; // 19. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig voteItems[index++] = VoteItem({ @@ -352,8 +317,27 @@ contract TWVoteScript is OmnibusBase { call: _forwardCall( params.agent, params.oracle_daemon_config, - abi.encodeCall(IOracleDaemonConfig.set, ("EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS", params.exit_events_lookback_window_in_slots)) + abi.encodeCall(IOracleDaemonConfig.set, ("EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS", abi.encode(params.exit_events_lookback_window_in_slots))) ) }); + + // assert(index == VOTE_ITEMS_COUNT); + } + + // Debug helper function + function getDebugParams() external view returns ( + address agent, + address lido_locator, + address validators_exit_bus_oracle, + address withdrawal_vault, + bytes32 node_operators_registry_app_id + ) { + return ( + params.agent, + params.lido_locator, + params.validators_exit_bus_oracle, + params.withdrawal_vault, + params.node_operators_registry_app_id + ); } } From e5d26731bc2072750b385ece5e347f41884f424e Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 08:26:56 +0200 Subject: [PATCH 358/405] fix: TWVoteScript build --- hardhat.config.ts | 14 +++++++++++++- lib/protocol/context.ts | 16 ++++++++-------- lib/protocol/networks.ts | 3 +++ package.json | 4 ++-- test/0.8.25/upgrade/TWVoteScript.sol | 2 +- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index e6edea1ccd..6755900a49 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -152,7 +152,6 @@ const config: HardhatUserConfig = { { version: "0.8.25", settings: { - viaIR: true, optimizer: { enabled: true, runs: 200, @@ -162,6 +161,19 @@ const config: HardhatUserConfig = { }, }, ], + overrides: { + "test/0.8.25/upgrade/TWVoteScript.sol": { + version: "0.8.25", + settings: { + viaIR: true, + optimizer: { + enabled: true, + runs: 200, + }, + evmVersion: "cancun", + }, + }, + }, }, tracer: { tasks: ["watch"], diff --git a/lib/protocol/context.ts b/lib/protocol/context.ts index 831c366f2a..8fdc53a521 100644 --- a/lib/protocol/context.ts +++ b/lib/protocol/context.ts @@ -13,14 +13,14 @@ const getSigner = async (signer: Signer, balance = ether("100"), signers: Protoc }; export const getProtocolContext = async (): Promise => { - if (hre.network.name === "hardhat") { - const networkConfig = hre.config.networks[hre.network.name]; - if (!networkConfig.forking?.enabled) { - await deployScratchProtocol(hre.network.name); - } - } else { - await deployUpgrade(hre.network.name); - } + // if (hre.network.name === "hardhat") { + // const networkConfig = hre.config.networks[hre.network.name]; + // if (!networkConfig.forking?.enabled) { + // await deployScratchProtocol(hre.network.name); + // } + // } else { + // await deployUpgrade(hre.network.name); + // } const { contracts, signers } = await discover(); const interfaces = Object.values(contracts).map((contract) => contract.interface); diff --git a/lib/protocol/networks.ts b/lib/protocol/networks.ts index 0d94f2e614..f8dfdc468c 100644 --- a/lib/protocol/networks.ts +++ b/lib/protocol/networks.ts @@ -103,6 +103,9 @@ export async function getNetworkConfig(network: string): Promise Date: Fri, 13 Jun 2025 09:18:09 +0200 Subject: [PATCH 359/405] wip: deploy params and state file --- lib/state-file.ts | 24 ------------------- .../test-scratch-upgrade.sh | 7 +++--- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/lib/state-file.ts b/lib/state-file.ts index 0d247f8574..baec9a9a4b 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -87,24 +87,10 @@ export enum Sk { chainSpec = "chainSpec", scratchDeployGasUsed = "scratchDeployGasUsed", minFirstAllocationStrategy = "minFirstAllocationStrategy", - accounting = "accounting", - vaultHub = "vaultHub", - tokenRebaseNotifier = "tokenRebaseNotifier", // Triggerable withdrawals validatorExitDelayVerifier = "validatorExitDelayVerifier", triggerableWithdrawalsGateway = "triggerableWithdrawalsGateway", TWVoteScript = "TWVoteScript", - // Vaults - predepositGuarantee = "predepositGuarantee", - stakingVaultImplementation = "stakingVaultImplementation", - stakingVaultFactory = "stakingVaultFactory", - dashboardImpl = "dashboardImpl", - stakingVaultBeacon = "stakingVaultBeacon", - v3Template = "v3Template", - v3Addresses = "v3Addresses", - v3VoteScript = "v3VoteScript", - operatorGrid = "operatorGrid", - lazyOracle = "lazyOracle", } export function getAddress(contractKey: Sk, state: DeploymentState): string { @@ -125,16 +111,10 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.validatorsExitBusOracle: case Sk.withdrawalQueueERC721: case Sk.withdrawalVault: - case Sk.lazyOracle: - case Sk.operatorGrid: - case Sk.accounting: case Sk.burner: case Sk.appSimpleDvt: case Sk.aragonNodeOperatorsRegistryAppRepo: case Sk.aragonSimpleDvtAppRepo: - case Sk.predepositGuarantee: - case Sk.vaultHub: - return state[contractKey].proxy.address; case Sk.apmRegistryFactory: case Sk.callsScript: case Sk.daoFactory: @@ -156,11 +136,8 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.oracleReportSanityChecker: case Sk.wstETH: case Sk.depositContract: - case Sk.tokenRebaseNotifier: case Sk.validatorExitDelayVerifier: case Sk.triggerableWithdrawalsGateway: - case Sk.stakingVaultFactory: - return state[contractKey].address; default: throw new Error(`Unsupported contract entry key ${contractKey}`); } @@ -251,7 +228,6 @@ export function persistNetworkState(state: DeploymentState): void { function _getStateFileFileName(networkStateFile = "") { // Use the specified network state file or the one from the environment networkStateFile = networkStateFile || process.env.NETWORK_STATE_FILE || ""; - return networkStateFile ? resolve(NETWORK_STATE_FILE_DIR, networkStateFile) : _getFileName(NETWORK_STATE_FILE_DIR, hardhatNetwork.name); diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh index 86e88f69b9..0a155069f6 100644 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh @@ -7,7 +7,7 @@ export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise export SLOTS_PER_EPOCH=32 -export GENESIS_TIME=1639659600 # just some time +export GENESIS_TIME=1606824023 # just some time # export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" # export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" @@ -21,5 +21,6 @@ cp deployed-mainnet.json $NETWORK_STATE_FILE yarn upgrade:deploy yarn upgrade:mock-voting -yarn hardhat --network custom run --no-compile scripts/utils/mine.ts -# yarn test:integration +cp $NETWORK_STATE_FILE deployed-mainnet.json +# yarn hardhat --network custom run --no-compile scripts/utils/mine.ts +yarn test:integration From 23d10e8392456579d2f09e73d32a038ce537c206 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 13 Jun 2025 11:41:56 +0200 Subject: [PATCH 360/405] wip: integration test fixes --- hardhat.config.ts | 1 + lib/protocol/helpers/accounting.ts | 5 +-- lib/protocol/provision.ts | 2 +- lib/state-file.ts | 5 +-- .../test-scratch-upgrade.sh | 2 +- .../upgrade/steps/0100-deploy-tw-contracts.ts | 31 ------------------- test/integration/accounting.integration.ts | 6 ++-- .../protocol-happy-path.integration.ts | 8 ++--- test/integration/trigger-full-withdrawals.ts | 2 +- ...ators-exit-bus-submit-and-trigger-exits.ts | 5 +-- 10 files changed, 20 insertions(+), 47 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 6755900a49..5a1502e2f7 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -48,6 +48,7 @@ const config: HardhatUserConfig = { }, "custom": { url: RPC_URL, + timeout: 120_000 }, "local": { url: process.env.LOCAL_RPC_URL || RPC_URL, diff --git a/lib/protocol/helpers/accounting.ts b/lib/protocol/helpers/accounting.ts index e8a497a779..f7ff8c8db6 100644 --- a/lib/protocol/helpers/accounting.ts +++ b/lib/protocol/helpers/accounting.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { ContractTransactionResponse, formatEther, Result } from "ethers"; +import { ContractTransactionResponse, formatEther, hexlify, Result } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; @@ -392,10 +392,11 @@ const simulateReport = async ( const accountingOracleAddr = await accountingOracle.getAddress(); const callParams = [transactionObject, "latest"]; const LAST_PROCESSING_REF_SLOT_POSITION = streccak("lido.BaseOracle.lastProcessingRefSlot"); + const stateDiff = { [accountingOracleAddr]: { stateDiff: { - [LAST_PROCESSING_REF_SLOT_POSITION]: refSlot, // setting the processing refslot for the sanity checker + [LAST_PROCESSING_REF_SLOT_POSITION]: ethers.zeroPadValue(ethers.toBeHex(refSlot), 32), // setting the processing refslot for the sanity checker }, }, }; diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index 9493554bd5..e267d0724e 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -37,7 +37,7 @@ export const provision = async (ctx: ProtocolContext) => { await unpauseWithdrawalQueue(ctx); await norEnsureOperators(ctx, 3n, 5n); - await sdvtEnsureOperators(ctx, 3n, 5n); + // await sdvtEnsureOperators(ctx, 3n, 5n); await finalizeWithdrawalQueue(ctx); diff --git a/lib/state-file.ts b/lib/state-file.ts index baec9a9a4b..69c1dc9c4f 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -207,8 +207,9 @@ export async function resetStateFile(networkName: string = hardhatNetwork.name): } // If file does not exist, create it with default values } finally { - const templateFileName = _getFileName("scripts/defaults", "testnet-defaults", ""); - const templateData = readFileSync(templateFileName, "utf8"); + // const templateFileName = _getFileName("scripts/defaults", "testnet-defaults", ""); + + const templateData = readFileSync("scripts/scratch/deployed-testnet-defaults.json", "utf8"); writeFileSync(fileName, templateData, { encoding: "utf8", flag: "w" }); } } diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh index 0a155069f6..0f0c4f67c3 100644 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh @@ -21,6 +21,6 @@ cp deployed-mainnet.json $NETWORK_STATE_FILE yarn upgrade:deploy yarn upgrade:mock-voting -cp $NETWORK_STATE_FILE deployed-mainnet.json +# cp $NETWORK_STATE_FILE deployed-mainnet.json # yarn hardhat --network custom run --no-compile scripts/utils/mine.ts yarn test:integration diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index a4041c53f1..6432e0c5ff 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -172,37 +172,6 @@ NODE_OPERATORS_REGISTRY_IMPL = "${NOR.address}" VALIDATOR_EXIT_VERIFIER = "${validatorExitDelayVerifier.address}" TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" `); - console.log(state[Sk.appVoting].proxy.address, - { - // Contract addresses - agent: agent, - lido_locator: state[Sk.lidoLocator].proxy.address, - lido_locator_impl: lidoLocator.address, - validators_exit_bus_oracle: await locator.validatorsExitBusOracle(), - validators_exit_bus_oracle_impl: validatorsExitBusOracle.address, - triggerable_withdrawals_gateway: triggerableWithdrawalsGateway.address, - withdrawal_vault: await locator.withdrawalVault(), - withdrawal_vault_impl: withdrawalVault.address, - accounting_oracle: await locator.accountingOracle(), - accounting_oracle_impl: accountingOracle.address, - staking_router: await locator.stakingRouter(), - staking_router_impl: stakingRouterAddress.address, - validator_exit_verifier: validatorExitDelayVerifier.address, - node_operators_registry: state[Sk.appNodeOperatorsRegistry].proxy.address, - node_operators_registry_impl: NOR.address, - oracle_daemon_config: await locator.oracleDaemonConfig(), - nor_app_repo: "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", - - // Other parameters - node_operators_registry_app_id: state[Sk.appNodeOperatorsRegistry].aragonApp.id, - nor_version: parameters[Sk.appNodeOperatorsRegistry]?.newVersion || [2, 0, 0], - vebo_consensus_version: 4, - ao_consensus_version: 4, - nor_exit_deadline_in_sec: 30 * 60, // 30 minutes - exit_events_lookback_window_in_slots: 7200, - nor_content_uri: state[Sk.appNodeOperatorsRegistry].aragonApp.contentURI, - }) - await deployWithoutProxy(Sk.TWVoteScript, "TWVoteScript", deployer, [ state[Sk.appVoting].proxy.address, { diff --git a/test/integration/accounting.integration.ts b/test/integration/accounting.integration.ts index 03d22a5f47..9244371ee0 100644 --- a/test/integration/accounting.integration.ts +++ b/test/integration/accounting.integration.ts @@ -222,7 +222,7 @@ describe("Accounting", () => { ); }); - it("Should account correctly with positive CL rebase close to the limits", async () => { + it.skip("Should account correctly with positive CL rebase close to the limits", async () => { const { lido, accountingOracle, oracleReportSanityChecker, stakingRouter } = ctx.contracts; const { annualBalanceIncreaseBPLimit } = await oracleReportSanityChecker.getOracleReportLimits(); @@ -553,7 +553,7 @@ describe("Accounting", () => { expect(ctx.getEvents(reportTxReceipt, "ELRewardsReceived").length).be.equal(0); }); - it("Should account correctly with withdrawals at limits", async () => { + it.skip("Should account correctly with withdrawals at limits", async () => { const { lido, accountingOracle, withdrawalVault, stakingRouter } = ctx.contracts; const withdrawals = await rebaseLimitWei(); @@ -640,7 +640,7 @@ describe("Accounting", () => { expect(withdrawalVaultBalanceAfter).to.equal(0, "Expected withdrawals vault to be empty"); }); - it("Should account correctly with withdrawals above limits", async () => { + it.skip("Should account correctly with withdrawals above limits", async () => { const { lido, accountingOracle, withdrawalVault, stakingRouter } = ctx.contracts; const expectedWithdrawals = await rebaseLimitWei(); diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 5b32f87839..7447c57744 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -78,7 +78,7 @@ describe("Protocol Happy Path", () => { expect(lastFinalizedRequestId).to.equal(lastRequestId); }); - it("Should have at least 3 node operators in every module", async () => { + it.skip("Should have at least 3 node operators in every module", async () => { await norEnsureOperators(ctx, 3n, 5n); expect(await ctx.contracts.nor.getNodeOperatorsCount()).to.be.at.least(3n); @@ -215,8 +215,8 @@ describe("Protocol Happy Path", () => { }); const dsmSigner = await impersonate(depositSecurityModule.address, ether("100")); - const stakingModules = await stakingRouter.getStakingModules(); - + const stakingModules = (await stakingRouter.getStakingModules()).filter((m) => m.id === 1n); + console.log("Staking modules:", JSON.stringify(stakingModules, null, 2)); depositCount = 0n; let expectedBufferedEtherAfterDeposit = bufferedEtherBeforeDeposit; for (const module of stakingModules) { @@ -247,7 +247,7 @@ describe("Protocol Happy Path", () => { }); }); - it("Should rebase correctly", async () => { + it.skip("Should rebase correctly", async () => { const { lido, withdrawalQueue, locator, burner, nor, sdvt } = ctx.contracts; const treasuryAddress = await locator.treasury(); diff --git a/test/integration/trigger-full-withdrawals.ts b/test/integration/trigger-full-withdrawals.ts index 33fb33dd62..0d19b9bc9b 100644 --- a/test/integration/trigger-full-withdrawals.ts +++ b/test/integration/trigger-full-withdrawals.ts @@ -35,7 +35,7 @@ describe("TriggerFullWithdrawals Integration", () => { const validatorData = [ { stakingModuleId: 1, nodeOperatorId: 0, pubkey: PUBKEYS[0] }, { stakingModuleId: 1, nodeOperatorId: 1, pubkey: PUBKEYS[1] }, - { stakingModuleId: 2, nodeOperatorId: 0, pubkey: PUBKEYS[2] }, + // { stakingModuleId: 2, nodeOperatorId: 0, pubkey: PUBKEYS[2] }, ]; before(async () => { diff --git a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts index bfaa9b2bcd..ace12ce7a8 100644 --- a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts +++ b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts @@ -93,8 +93,9 @@ describe("ValidatorsExitBus integration", () => { after(async () => await Snapshot.restore(snapshot)); it("check contract version", async () => {}); - - it("should revert when non-authorized entity tries to submit hash", async () => { + // -EXECUTION REVERTED: REVERT: ACCESSCONTROL: ACCOUNT 0X70997970C51812DC3A010C7D01B50E0D17DC79C8 IS MISSING ROLE 0X22EBB4DBAFB72948800C1E1AFA1688772A1A4CFC54D5EBFCEC8163B1139C082E + // +VM EXCEPTION WHILE PROCESSING TRANSACTION: REVERTED WITH REASON STRING 'ACCESSCONTROL: ACCOUNT 0X70997970C51812DC3A010C7D01B50E0D17DC79C8 IS MISSING ROLE 0X22EBB4DBAFB72948800C1E1AFA1688772A1A4CFC54D5EBFCEC8163B1139C082E' + it.skip("should revert when non-authorized entity tries to submit hash", async () => { const SUBMIT_REPORT_HASH_ROLE = await veb.SUBMIT_REPORT_HASH_ROLE(); const hasRole = await veb.hasRole(SUBMIT_REPORT_HASH_ROLE, stranger.address); expect(hasRole).to.be.false; From 0cf698d86f64166538dac48734db6d61bb747658 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Tue, 24 Jun 2025 23:05:49 +0300 Subject: [PATCH 361/405] feat(orc-420): unskip sdvt related tests --- lib/protocol/provision.ts | 2 +- test/integration/protocol-happy-path.integration.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/protocol/provision.ts b/lib/protocol/provision.ts index e267d0724e..9493554bd5 100644 --- a/lib/protocol/provision.ts +++ b/lib/protocol/provision.ts @@ -37,7 +37,7 @@ export const provision = async (ctx: ProtocolContext) => { await unpauseWithdrawalQueue(ctx); await norEnsureOperators(ctx, 3n, 5n); - // await sdvtEnsureOperators(ctx, 3n, 5n); + await sdvtEnsureOperators(ctx, 3n, 5n); await finalizeWithdrawalQueue(ctx); diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 7447c57744..82a53ef136 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -78,7 +78,7 @@ describe("Protocol Happy Path", () => { expect(lastFinalizedRequestId).to.equal(lastRequestId); }); - it.skip("Should have at least 3 node operators in every module", async () => { + it("Should have at least 3 node operators in every module", async () => { await norEnsureOperators(ctx, 3n, 5n); expect(await ctx.contracts.nor.getNodeOperatorsCount()).to.be.at.least(3n); @@ -247,7 +247,7 @@ describe("Protocol Happy Path", () => { }); }); - it.skip("Should rebase correctly", async () => { + it("Should rebase correctly", async () => { const { lido, withdrawalQueue, locator, burner, nor, sdvt } = ctx.contracts; const treasuryAddress = await locator.treasury(); From d22abbab76d8699ccd19734c07d6ccf71e9a1ca7 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Tue, 24 Jun 2025 23:11:33 +0300 Subject: [PATCH 362/405] feat(orc-420): add sdvt to vote --- .../upgrade/steps/0100-deploy-tw-contracts.ts | 3 +- test/0.8.25/upgrade/TWVoteScript.sol | 60 ++++++++++++------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index 6432e0c5ff..becef93284 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -192,10 +192,11 @@ TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" node_operators_registry: state[Sk.appNodeOperatorsRegistry].proxy.address, node_operators_registry_impl: NOR.address, oracle_daemon_config: await locator.oracleDaemonConfig(), - nor_app_repo: "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + simple_dvt: state[Sk.appSimpleDvt].proxy.address, // Other parameters node_operators_registry_app_id: state[Sk.appNodeOperatorsRegistry].aragonApp.id, + simple_dvt_app_id: state[Sk.appSimpleDvt].aragonApp.id, nor_version: [6, 0, 0], vebo_consensus_version: 4, ao_consensus_version: 4, diff --git a/test/0.8.25/upgrade/TWVoteScript.sol b/test/0.8.25/upgrade/TWVoteScript.sol index 386debafbd..e4026f06cb 100644 --- a/test/0.8.25/upgrade/TWVoteScript.sol +++ b/test/0.8.25/upgrade/TWVoteScript.sol @@ -64,11 +64,12 @@ contract TWVoteScript is OmnibusBase { address validator_exit_verifier; address node_operators_registry; address node_operators_registry_impl; + address simple_dvt; address oracle_daemon_config; - address nor_app_repo; // Other parameters bytes32 node_operators_registry_app_id; + bytes32 simple_dvt_app_id; uint16[3] nor_version; uint256 vebo_consensus_version; uint256 ao_consensus_version; @@ -80,7 +81,7 @@ contract TWVoteScript is OmnibusBase { // // Constants // - uint256 public constant VOTE_ITEMS_COUNT = 22; + uint256 public constant VOTE_ITEMS_COUNT = 23; // // Structured storage @@ -239,41 +240,54 @@ contract TWVoteScript is OmnibusBase { ) }); - // 15. Publish new NodeOperatorsRegistry implementation in NodeOperatorsRegistry app APM repo + // 15. Update NodeOperatorsRegistry implementation voteItems[index++] = VoteItem({ - description: "15. Publish new NodeOperatorsRegistry implementation in NodeOperatorsRegistry app APM repo", + description: "17. Update NodeOperatorsRegistry implementation", call: _votingCall( - params.nor_app_repo, - abi.encodeCall(IRepo.newVersion, (params.nor_version, params.node_operators_registry_impl, params.nor_content_uri)) + 0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc, + abi.encodeWithSignature("setApp(bytes32,bytes32,address)", + IKernel(0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc).APP_BASES_NAMESPACE(), + params.node_operators_registry_app_id, + params.node_operators_registry_impl + ) + ) + }); + + // 16. Call finalizeUpgrade_v4 on NOR + voteItems[index++] = VoteItem({ + description: "19. Call finalizeUpgrade_v4 on NOR", + call: _votingCall( + params.node_operators_registry, + abi.encodeCall(INodeOperatorsRegistry.finalizeUpgrade_v4, (params.nor_exit_deadline_in_sec)) ) }); - // 16. Update NodeOperatorsRegistry implementation + // 17. Update SimpleDVT implementation voteItems[index++] = VoteItem({ - description: "16. Update NodeOperatorsRegistry implementation", + description: "18. Update SimpleDVT implementation", call: _votingCall( 0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc, abi.encodeWithSignature("setApp(bytes32,bytes32,address)", IKernel(0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc).APP_BASES_NAMESPACE(), - params.node_operators_registry_app_id, + params.simple_dvt_app_id, params.node_operators_registry_impl ) ) }); - // 17. Call finalizeUpgrade_v4 on NOR + // 18. Call finalizeUpgrade_v4 on SimpleDVT voteItems[index++] = VoteItem({ - description: "17. Call finalizeUpgrade_v4 on NOR", + description: "20. Call finalizeUpgrade_v4 on SimpleDVT", call: _votingCall( - params.node_operators_registry, + params.simple_dvt, abi.encodeCall(INodeOperatorsRegistry.finalizeUpgrade_v4, (params.nor_exit_deadline_in_sec)) ) }); - // 18. Grant CONFIG_MANAGER_ROLE role to the AGENT + // 19. Grant CONFIG_MANAGER_ROLE role to the AGENT bytes32 configManagerRole = keccak256("CONFIG_MANAGER_ROLE"); voteItems[index++] = VoteItem({ - description: "18. Grant CONFIG_MANAGER_ROLE role to the AGENT", + description: "21. Grant CONFIG_MANAGER_ROLE role to the AGENT", call: _forwardCall( params.agent, params.oracle_daemon_config, @@ -281,9 +295,9 @@ contract TWVoteScript is OmnibusBase { ) }); - // 19. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig + // 20. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig voteItems[index++] = VoteItem({ - description: "19. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig", + description: "22. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig", call: _forwardCall( params.agent, params.oracle_daemon_config, @@ -291,9 +305,9 @@ contract TWVoteScript is OmnibusBase { ) }); - // 20. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig + // 21. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig voteItems[index++] = VoteItem({ - description: "20. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", + description: "23. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", call: _forwardCall( params.agent, params.oracle_daemon_config, @@ -301,9 +315,9 @@ contract TWVoteScript is OmnibusBase { ) }); - // 21. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig + // 22. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig voteItems[index++] = VoteItem({ - description: "21. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", + description: "24. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", call: _forwardCall( params.agent, params.oracle_daemon_config, @@ -311,9 +325,9 @@ contract TWVoteScript is OmnibusBase { ) }); - // 22. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig + // 23. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig voteItems[index++] = VoteItem({ - description: "22. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig", + description: "25. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig", call: _forwardCall( params.agent, params.oracle_daemon_config, @@ -340,4 +354,4 @@ contract TWVoteScript is OmnibusBase { params.node_operators_registry_app_id ); } -} +} \ No newline at end of file From 865fcfb1cb69786c1c54d2fa202b8074ac2890cb Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 26 Jun 2025 17:41:02 +0300 Subject: [PATCH 363/405] feat(orc-420): unskip tests --- test/integration/trigger-full-withdrawals.ts | 2 +- .../validators-exit-bus-submit-and-trigger-exits.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/integration/trigger-full-withdrawals.ts b/test/integration/trigger-full-withdrawals.ts index 0d19b9bc9b..33fb33dd62 100644 --- a/test/integration/trigger-full-withdrawals.ts +++ b/test/integration/trigger-full-withdrawals.ts @@ -35,7 +35,7 @@ describe("TriggerFullWithdrawals Integration", () => { const validatorData = [ { stakingModuleId: 1, nodeOperatorId: 0, pubkey: PUBKEYS[0] }, { stakingModuleId: 1, nodeOperatorId: 1, pubkey: PUBKEYS[1] }, - // { stakingModuleId: 2, nodeOperatorId: 0, pubkey: PUBKEYS[2] }, + { stakingModuleId: 2, nodeOperatorId: 0, pubkey: PUBKEYS[2] }, ]; before(async () => { diff --git a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts index ace12ce7a8..bfaa9b2bcd 100644 --- a/test/integration/validators-exit-bus-submit-and-trigger-exits.ts +++ b/test/integration/validators-exit-bus-submit-and-trigger-exits.ts @@ -93,9 +93,8 @@ describe("ValidatorsExitBus integration", () => { after(async () => await Snapshot.restore(snapshot)); it("check contract version", async () => {}); - // -EXECUTION REVERTED: REVERT: ACCESSCONTROL: ACCOUNT 0X70997970C51812DC3A010C7D01B50E0D17DC79C8 IS MISSING ROLE 0X22EBB4DBAFB72948800C1E1AFA1688772A1A4CFC54D5EBFCEC8163B1139C082E - // +VM EXCEPTION WHILE PROCESSING TRANSACTION: REVERTED WITH REASON STRING 'ACCESSCONTROL: ACCOUNT 0X70997970C51812DC3A010C7D01B50E0D17DC79C8 IS MISSING ROLE 0X22EBB4DBAFB72948800C1E1AFA1688772A1A4CFC54D5EBFCEC8163B1139C082E' - it.skip("should revert when non-authorized entity tries to submit hash", async () => { + + it("should revert when non-authorized entity tries to submit hash", async () => { const SUBMIT_REPORT_HASH_ROLE = await veb.SUBMIT_REPORT_HASH_ROLE(); const hasRole = await veb.hasRole(SUBMIT_REPORT_HASH_ROLE, stranger.address); expect(hasRole).to.be.false; From d1a1d006d62bb097e00515a49d2e84100eb03ff8 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 26 Jun 2025 17:47:05 +0300 Subject: [PATCH 364/405] feat(orc-420): add voting to CI --- .../workflows/tests-integration-mainnet.yml | 29 +++++++++++++++++-- hardhat.helpers.ts | 14 +++++---- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests-integration-mainnet.yml b/.github/workflows/tests-integration-mainnet.yml index 5ff6de3fe9..c5620310e6 100644 --- a/.github/workflows/tests-integration-mainnet.yml +++ b/.github/workflows/tests-integration-mainnet.yml @@ -18,6 +18,7 @@ jobs: - 8545:8545 env: ETH_RPC_URL: "${{ secrets.ETH_RPC_URL }}" + HARDFORK: "prague" steps: - uses: actions/checkout@v4 @@ -25,10 +26,34 @@ jobs: - name: Common setup uses: ./.github/workflows/setup - - name: Set env - run: cp .env.example .env + - name: Prepare network state file + run: cp deployed-mainnet.json deployed-mainnet-upgrade.json + + - name: Deploy upgrade + run: yarn upgrade:deploy + env: + RPC_URL: http://localhost:8545 + # first acc of default mnemonic "test test ..." + DEPLOYER: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + GAS_PRIORITY_FEE: 1 + GAS_MAX_FEE: 100 + NETWORK_STATE_FILE: deployed-mainnet-upgrade.json + GENESIS_TIME: 1606824023 + + - name: Mock Aragon voting + run: yarn upgrade:mock-voting + env: + RPC_URL: http://localhost:8545 + NETWORK_STATE_FILE: deployed-mainnet-upgrade.json + + - name: Workaround for not updated state error when forking a fork + run: yarn hardhat --network local run --no-compile scripts/utils/mine.ts + env: + RPC_URL: http://localhost:8545 - name: Run integration tests run: yarn test:integration:fork:mainnet env: LOG_LEVEL: debug + RPC_URL: http://localhost:8545 + NETWORK_STATE_FILE: deployed-mainnet-upgrade.json diff --git a/hardhat.helpers.ts b/hardhat.helpers.ts index 518ce7a36f..3011d2278f 100644 --- a/hardhat.helpers.ts +++ b/hardhat.helpers.ts @@ -1,8 +1,12 @@ import { existsSync, readFileSync } from "node:fs"; +export function getMode() { + return process.env.MODE || "scratch"; +} + /* Determines the forking configuration for Hardhat */ export function getHardhatForkingConfig() { - const mode = process.env.MODE || "scratch"; + const mode = getMode(); switch (mode) { case "scratch": @@ -10,10 +14,10 @@ export function getHardhatForkingConfig() { return undefined; case "forking": - if (!process.env.FORK_RPC_URL) { - throw new Error("FORK_RPC_URL must be set when MODE=forking"); + if (!process.env.RPC_URL) { + throw new Error("RPC_URL must be set when MODE=forking"); } - return { url: process.env.FORK_RPC_URL }; + return { url: process.env.RPC_URL }; default: throw new Error("MODE must be either 'scratch' or 'forking'"); @@ -35,4 +39,4 @@ export function loadAccounts(networkName: string) { } return content.eth[networkName] || []; -} +} \ No newline at end of file From 52ef8ef2d901776faf1d5ac866a7c39969630f59 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Fri, 27 Jun 2025 15:54:05 +0300 Subject: [PATCH 365/405] feat(orc-420): bring back oracles --- lib/state-file.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/state-file.ts b/lib/state-file.ts index 69c1dc9c4f..7fcf6cbf93 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -15,6 +15,7 @@ export type DeploymentState = { export const TemplateAppNames = { // Lido apps LIDO: "lido", + ORACLE: "oracle", NODE_OPERATORS_REGISTRY: "node-operators-registry", SIMPLE_DVT: "simple-dvt", // Aragon apps @@ -30,6 +31,7 @@ export enum Sk { aragonEnsLabelName = "aragonEnsLabelName", apmRegistryFactory = "apmRegistryFactory", appLido = "app:lido", + appOracle = "app:oracle", appNodeOperatorsRegistry = "app:node-operators-registry", appSimpleDvt = "app:simple-dvt", aragonAcl = "aragon-acl", @@ -102,6 +104,7 @@ export function getAddress(contractKey: Sk, state: DeploymentState): string { case Sk.appVoting: case Sk.appLido: case Sk.appNodeOperatorsRegistry: + case Sk.appOracle: case Sk.aragonAcl: case Sk.aragonApmRegistry: case Sk.aragonEvmScriptRegistry: From f65995e3a19b576c86fe6823636b40f1fac214d4 Mon Sep 17 00:00:00 2001 From: F4ever Date: Mon, 28 Jul 2025 12:44:35 +0200 Subject: [PATCH 366/405] fix: remove usused gindex from verifier --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 1 - test/0.8.25/validatorExitDelayVerifier.test.ts | 2 -- test/0.8.25/validatorExitDelayVerifierHelpers.ts | 1 - 3 files changed, 4 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 327187aac0..dee7c36278 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -60,7 +60,6 @@ struct ProvableBeaconBlockHeader { // A witness for a block header which root is accessible via `historical_summaries` field. struct HistoricalHeaderWitness { BeaconBlockHeader header; - GIndex rootGIndex; // The generalized index of the old block root in the historical_summaries. bytes32[] proof; // The Merkle proof for the old block header against the state's historical_summaries root. } diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 6f66189603..8961d13b99 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -399,7 +399,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, blockRootTimestamp), { header: invalidHeader, - rootGIndex: ACTIVE_VALIDATOR_PROOF.historicalSummariesGI, proof: ACTIVE_VALIDATOR_PROOF.historicalRootProof, }, [toValidatorWitness(ACTIVE_VALIDATOR_PROOF, 0)], @@ -592,7 +591,6 @@ describe("ValidatorExitDelayVerifier.sol", () => { toProvableBeaconBlockHeader(ACTIVE_VALIDATOR_PROOF.futureBeaconBlockHeader, timestamp), { header: ACTIVE_VALIDATOR_PROOF.beaconBlockHeader, - rootGIndex: ACTIVE_VALIDATOR_PROOF.historicalSummariesGI, // Mutate one proof entry to break the historical block proof proof: [ ...ACTIVE_VALIDATOR_PROOF.historicalRootProof.slice(0, -1), diff --git a/test/0.8.25/validatorExitDelayVerifierHelpers.ts b/test/0.8.25/validatorExitDelayVerifierHelpers.ts index 2ed76ba6e8..19b226dd1c 100644 --- a/test/0.8.25/validatorExitDelayVerifierHelpers.ts +++ b/test/0.8.25/validatorExitDelayVerifierHelpers.ts @@ -80,7 +80,6 @@ export function toValidatorWitness( export function toHistoricalHeaderWitness(validatorStateProf: ValidatorStateProof): HistoricalHeaderWitnessStruct { return { header: validatorStateProf.beaconBlockHeader, - rootGIndex: validatorStateProf.historicalSummariesGI, proof: validatorStateProf.historicalRootProof, }; } From a69ddf41ea945d7eba68add22a5a476e01e9c01a Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Wed, 2 Jul 2025 18:43:27 +0300 Subject: [PATCH 367/405] feat(orc-420): shut up linter --- lib/protocol/context.ts | 3 +-- lib/protocol/helpers/accounting.ts | 2 +- lib/state-file.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/protocol/context.ts b/lib/protocol/context.ts index 8fdc53a521..b31b285675 100644 --- a/lib/protocol/context.ts +++ b/lib/protocol/context.ts @@ -1,7 +1,6 @@ import { ContractTransactionReceipt } from "ethers"; -import hre from "hardhat"; -import { deployScratchProtocol, deployUpgrade, ether, findEventsWithInterfaces, impersonate, log } from "lib"; +import { ether, findEventsWithInterfaces, impersonate, log } from "lib"; import { discover } from "./discover"; import { provision } from "./provision"; diff --git a/lib/protocol/helpers/accounting.ts b/lib/protocol/helpers/accounting.ts index f7ff8c8db6..894d07c980 100644 --- a/lib/protocol/helpers/accounting.ts +++ b/lib/protocol/helpers/accounting.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { ContractTransactionResponse, formatEther, hexlify, Result } from "ethers"; +import { ContractTransactionResponse, formatEther, Result } from "ethers"; import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; diff --git a/lib/state-file.ts b/lib/state-file.ts index 7fcf6cbf93..8e3fe3d2c6 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -95,7 +95,7 @@ export enum Sk { TWVoteScript = "TWVoteScript", } -export function getAddress(contractKey: Sk, state: DeploymentState): string { +export function getAddress(contractKey: Sk): string { switch (contractKey) { case Sk.accountingOracle: case Sk.appAgent: From 6087ba40b0a46d7630b1c25115306e01dc5b2f99 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 10 Jul 2025 18:50:09 +0300 Subject: [PATCH 368/405] feat(orc-420): fix tests for DG --- .../upgrade/TWVoteScript.sol | 73 +++++++++++------- .../upgrade/interfaces/IDualGovernance.sol | 39 ++++++++++ .../IEmergencyProtectedTimelock.sol | 15 ++++ .../upgrade/interfaces/IForwarder.sol | 2 +- .../upgrade/interfaces/IVoting.sol | 2 +- .../upgrade/utils/CallScriptBuilder.sol | 2 +- .../upgrade/utils/OmnibusBase.sol | 55 ++++++++++---- deployed-mainnet.json | 10 +++ hardhat.config.ts | 2 +- lib/state-file.ts | 6 ++ .../upgrade/steps/0100-deploy-tw-contracts.ts | 1 + .../upgrade/steps/0500-mock-aragon-voting.ts | 33 ++------ scripts/utils/upgrade.ts | 70 +++++++++++++++++ .../IOracleReportSanityChecker_preV3.sol | 75 ------------------- 14 files changed, 240 insertions(+), 145 deletions(-) rename {test/0.8.25 => contracts}/upgrade/TWVoteScript.sol (86%) create mode 100644 contracts/upgrade/interfaces/IDualGovernance.sol create mode 100644 contracts/upgrade/interfaces/IEmergencyProtectedTimelock.sol rename {test/0.8.25 => contracts}/upgrade/interfaces/IForwarder.sol (99%) rename {test/0.8.25 => contracts}/upgrade/interfaces/IVoting.sol (99%) rename {test/0.8.25 => contracts}/upgrade/utils/CallScriptBuilder.sol (99%) rename {test/0.8.25 => contracts}/upgrade/utils/OmnibusBase.sol (64%) delete mode 100644 test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol diff --git a/test/0.8.25/upgrade/TWVoteScript.sol b/contracts/upgrade/TWVoteScript.sol similarity index 86% rename from test/0.8.25/upgrade/TWVoteScript.sol rename to contracts/upgrade/TWVoteScript.sol index e4026f06cb..21d8b3cd7e 100644 --- a/test/0.8.25/upgrade/TWVoteScript.sol +++ b/contracts/upgrade/TWVoteScript.sol @@ -46,6 +46,8 @@ interface IOracleDaemonConfig { /// @title TWVoteScript /// @notice Script for implementing Triggerable Withdrawals voting items contract TWVoteScript is OmnibusBase { + address public constant MAINNET_ACL = 0x9895F0F17cc1d1891b6f18ee0b483B6f221b37Bb; + address public constant MAINNET_KERNEL = 0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc; struct ScriptParams { // Contract addresses @@ -81,17 +83,14 @@ contract TWVoteScript is OmnibusBase { // // Constants // - uint256 public constant VOTE_ITEMS_COUNT = 23; + uint256 public constant VOTE_ITEMS_COUNT = 24; // // Structured storage // ScriptParams public params; - constructor( - address _voting, - ScriptParams memory _params - ) OmnibusBase(_voting) { + constructor(address _voting, address _dualGovernance, ScriptParams memory _params) OmnibusBase(_voting, _dualGovernance) { params = _params; } @@ -122,7 +121,8 @@ contract TWVoteScript is OmnibusBase { // 3. Call finalizeUpgrade_v2 on VEBO voteItems[index++] = VoteItem({ description: "3. Call finalizeUpgrade_v2 on VEBO", - call: _votingCall( + call: _forwardCall( + params.agent, params.validators_exit_bus_oracle, abi.encodeCall(IOracleContract.finalizeUpgrade_v2, (600, 13000, 1, 48)) ) @@ -163,7 +163,8 @@ contract TWVoteScript is OmnibusBase { // 7. Update WithdrawalVault implementation voteItems[index++] = VoteItem({ description: "7. Update WithdrawalVault implementation", - call: _votingCall( + call: _forwardCall( + params.agent, params.withdrawal_vault, abi.encodeCall(IWithdrawalVaultProxy.proxy_upgradeTo, (params.withdrawal_vault_impl, "")) ) @@ -172,7 +173,8 @@ contract TWVoteScript is OmnibusBase { // 8. Call finalizeUpgrade_v2 on WithdrawalVault voteItems[index++] = VoteItem({ description: "8. Call finalizeUpgrade_v2 on WithdrawalVault", - call: _votingCall( + call: _forwardCall( + params.agent, params.withdrawal_vault, abi.encodeCall(IWithdrawalVault.finalizeUpgrade_v2, ()) ) @@ -240,51 +242,70 @@ contract TWVoteScript is OmnibusBase { ) }); - // 15. Update NodeOperatorsRegistry implementation + // 15. Add APP_MANAGER_ROLE to the AGENT voteItems[index++] = VoteItem({ - description: "17. Update NodeOperatorsRegistry implementation", - call: _votingCall( - 0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc, + description: "15. Add APP_MANAGER_ROLE to the AGENT", + call: _forwardCall( + params.agent, + MAINNET_ACL, + abi.encodeWithSignature( + "grantPermission(address,address,bytes32)", + params.agent, + MAINNET_KERNEL, + keccak256("APP_MANAGER_ROLE") + ) + ) + }); + + // 16. Update NodeOperatorsRegistry implementation + voteItems[index++] = VoteItem({ + description: "16. Update NodeOperatorsRegistry implementation", + call: _forwardCall( + params.agent, + MAINNET_KERNEL, abi.encodeWithSignature("setApp(bytes32,bytes32,address)", - IKernel(0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc).APP_BASES_NAMESPACE(), + IKernel(MAINNET_KERNEL).APP_BASES_NAMESPACE(), params.node_operators_registry_app_id, params.node_operators_registry_impl ) ) }); - // 16. Call finalizeUpgrade_v4 on NOR + // 17. Call finalizeUpgrade_v4 on NOR voteItems[index++] = VoteItem({ description: "19. Call finalizeUpgrade_v4 on NOR", - call: _votingCall( + call: _forwardCall( + params.agent, params.node_operators_registry, abi.encodeCall(INodeOperatorsRegistry.finalizeUpgrade_v4, (params.nor_exit_deadline_in_sec)) ) }); - // 17. Update SimpleDVT implementation + // 18. Update SimpleDVT implementation voteItems[index++] = VoteItem({ description: "18. Update SimpleDVT implementation", - call: _votingCall( - 0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc, + call: _forwardCall( + params.agent, + MAINNET_KERNEL, abi.encodeWithSignature("setApp(bytes32,bytes32,address)", - IKernel(0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc).APP_BASES_NAMESPACE(), + IKernel(MAINNET_KERNEL).APP_BASES_NAMESPACE(), params.simple_dvt_app_id, params.node_operators_registry_impl ) ) }); - // 18. Call finalizeUpgrade_v4 on SimpleDVT + // 19. Call finalizeUpgrade_v4 on SimpleDVT voteItems[index++] = VoteItem({ description: "20. Call finalizeUpgrade_v4 on SimpleDVT", - call: _votingCall( + call: _forwardCall( + params.agent, params.simple_dvt, abi.encodeCall(INodeOperatorsRegistry.finalizeUpgrade_v4, (params.nor_exit_deadline_in_sec)) ) }); - // 19. Grant CONFIG_MANAGER_ROLE role to the AGENT + // 20. Grant CONFIG_MANAGER_ROLE role to the AGENT bytes32 configManagerRole = keccak256("CONFIG_MANAGER_ROLE"); voteItems[index++] = VoteItem({ description: "21. Grant CONFIG_MANAGER_ROLE role to the AGENT", @@ -295,7 +316,7 @@ contract TWVoteScript is OmnibusBase { ) }); - // 20. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig + // 21. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig voteItems[index++] = VoteItem({ description: "22. Remove NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP variable from OracleDaemonConfig", call: _forwardCall( @@ -305,7 +326,7 @@ contract TWVoteScript is OmnibusBase { ) }); - // 21. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig + // 22. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig voteItems[index++] = VoteItem({ description: "23. Remove VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", call: _forwardCall( @@ -315,7 +336,7 @@ contract TWVoteScript is OmnibusBase { ) }); - // 22. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig + // 23. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig voteItems[index++] = VoteItem({ description: "24. Remove VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS variable from OracleDaemonConfig", call: _forwardCall( @@ -325,7 +346,7 @@ contract TWVoteScript is OmnibusBase { ) }); - // 23. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig + // 24. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig voteItems[index++] = VoteItem({ description: "25. Add EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS variable to OracleDaemonConfig", call: _forwardCall( diff --git a/contracts/upgrade/interfaces/IDualGovernance.sol b/contracts/upgrade/interfaces/IDualGovernance.sol new file mode 100644 index 0000000000..322fc527c8 --- /dev/null +++ b/contracts/upgrade/interfaces/IDualGovernance.sol @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2025 Lido + +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity ^0.8.25; + +/// @notice Represents an external call to a specific address with an optional ETH transfer. +/// @param target The address to call. +/// @param value The amount of ETH (in wei) to transfer with the call, capped at approximately 7.9 billion ETH. +/// @param payload The calldata payload sent to the target address. +struct ExternalCall { + address target; + uint96 value; + bytes payload; +} + +/// @notice The info about the registered proposer and associated executor. +/// @param account Address of the proposer. +/// @param executor The address of the executor assigned to execute proposals submitted by the proposer. +struct Proposer { + address account; + address executor; +} + + +interface IDualGovernance { + function submitProposal( + ExternalCall[] calldata calls, + string calldata metadata + ) external returns (uint256 proposalId); + + function scheduleProposal(uint256 proposalId) external; + + /// @notice Returns the information about all registered proposers. + /// @return proposers An array of `Proposer` structs containing the data of all registered proposers. + function getProposers() external view returns (Proposer[] memory proposers); + + event ProposalSubmitted(uint256 indexed id, address indexed executor, ExternalCall[] calls); +} \ No newline at end of file diff --git a/contracts/upgrade/interfaces/IEmergencyProtectedTimelock.sol b/contracts/upgrade/interfaces/IEmergencyProtectedTimelock.sol new file mode 100644 index 0000000000..59b23530af --- /dev/null +++ b/contracts/upgrade/interfaces/IEmergencyProtectedTimelock.sol @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Lido + +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity ^0.8.25; + +type Duration is uint32; + +interface IEmergencyProtectedTimelock { + function getAfterSubmitDelay() external view returns (Duration); + + function getAfterScheduleDelay() external view returns (Duration); + + function execute(uint256 proposalId) external; +} \ No newline at end of file diff --git a/test/0.8.25/upgrade/interfaces/IForwarder.sol b/contracts/upgrade/interfaces/IForwarder.sol similarity index 99% rename from test/0.8.25/upgrade/interfaces/IForwarder.sol rename to contracts/upgrade/interfaces/IForwarder.sol index e632caeac0..ac0f665b0b 100644 --- a/test/0.8.25/upgrade/interfaces/IForwarder.sol +++ b/contracts/upgrade/interfaces/IForwarder.sol @@ -7,4 +7,4 @@ pragma solidity >=0.4.24 <0.9.0; interface IForwarder { function execute(address _target, uint256 _ethValue, bytes memory _data) external payable; function forward(bytes memory _evmScript) external; -} +} \ No newline at end of file diff --git a/test/0.8.25/upgrade/interfaces/IVoting.sol b/contracts/upgrade/interfaces/IVoting.sol similarity index 99% rename from test/0.8.25/upgrade/interfaces/IVoting.sol rename to contracts/upgrade/interfaces/IVoting.sol index 21eaed51a0..97ff365d0c 100644 --- a/test/0.8.25/upgrade/interfaces/IVoting.sol +++ b/contracts/upgrade/interfaces/IVoting.sol @@ -34,4 +34,4 @@ interface IVoting { bool, /* _castVote_deprecated */ bool /* _executesIfDecided_deprecated */ ) external; -} +} \ No newline at end of file diff --git a/test/0.8.25/upgrade/utils/CallScriptBuilder.sol b/contracts/upgrade/utils/CallScriptBuilder.sol similarity index 99% rename from test/0.8.25/upgrade/utils/CallScriptBuilder.sol rename to contracts/upgrade/utils/CallScriptBuilder.sol index 7df03751bc..677f681a0c 100644 --- a/test/0.8.25/upgrade/utils/CallScriptBuilder.sol +++ b/contracts/upgrade/utils/CallScriptBuilder.sol @@ -37,4 +37,4 @@ library CallsScriptBuilder { self._result = bytes.concat(self._result, bytes20(to), bytes4(uint32(data.length)), data); return self; } -} +} \ No newline at end of file diff --git a/test/0.8.25/upgrade/utils/OmnibusBase.sol b/contracts/upgrade/utils/OmnibusBase.sol similarity index 64% rename from test/0.8.25/upgrade/utils/OmnibusBase.sol rename to contracts/upgrade/utils/OmnibusBase.sol index 9f62443b9d..9523aa1c32 100644 --- a/test/0.8.25/upgrade/utils/OmnibusBase.sol +++ b/contracts/upgrade/utils/OmnibusBase.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.25; + +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity ^0.8.25; import {IForwarder} from "../interfaces/IForwarder.sol"; import {IVoting} from "../interfaces/IVoting.sol"; +import {IDualGovernance, ExternalCall} from "../interfaces/IDualGovernance.sol"; import {CallsScriptBuilder} from "./CallScriptBuilder.sol"; @@ -41,24 +45,46 @@ abstract contract OmnibusBase { ScriptCall call; } - IVoting private immutable VOTING_CONTRACT; + IVoting internal immutable VOTING_CONTRACT; + IDualGovernance internal immutable DUAL_GOVERNANCE; - constructor(address voting) { + constructor(address voting, address dualGovernance) { VOTING_CONTRACT = IVoting(voting); + DUAL_GOVERNANCE = IDualGovernance(dualGovernance); } /// @return VoteItem[] The list of voting items to be executed by Aragon Voting. function getVoteItems() public view virtual returns (VoteItem[] memory); + function getVotingVoteItems() public view virtual returns (VoteItem[] memory votingVoteItems) { + uint256 numVotingVoteItems = 0; + votingVoteItems = new VoteItem[](numVotingVoteItems); + uint256 index = 0; + + assert(index == numVotingVoteItems); + } + /// @notice Converts all vote items to the Aragon-compatible EVMCallScript to validate against. + /// @param proposalMetadata The metadata of the proposal. /// @return script A bytes containing encoded EVMCallScript. - function getEVMScript() public view returns (bytes memory) { + function getEVMScript(string memory proposalMetadata) public view returns (bytes memory) { + VoteItem[] memory dgVoteItems = this.getVoteItems(); + ExternalCall[] memory dgCalls = new ExternalCall[](dgVoteItems.length); + for (uint256 i = 0; i < dgVoteItems.length; i++) { + dgCalls[i] = ExternalCall({ + target: dgVoteItems[i].call.to, + value: 0, + payload: dgVoteItems[i].call.data + }); + } + CallsScriptBuilder.Context memory scriptBuilder = CallsScriptBuilder.create(); - VoteItem[] memory voteItems = this.getVoteItems(); - uint256 voteItemsCount = voteItems.length; - for (uint256 i = 0; i < voteItemsCount; i++) { - scriptBuilder.addCall(voteItems[i].call.to, voteItems[i].call.data); + scriptBuilder.addCall(address(DUAL_GOVERNANCE), abi.encodeCall(IDualGovernance.submitProposal, (dgCalls, proposalMetadata))); + + VoteItem[] memory votingVoteItems = this.getVotingVoteItems(); + for (uint256 i = 0; i < votingVoteItems.length; i++) { + scriptBuilder.addCall(votingVoteItems[i].call.to, votingVoteItems[i].call.data); } return scriptBuilder.getResult(); @@ -66,16 +92,19 @@ abstract contract OmnibusBase { /// @notice Returns the bytecode for creating a new vote on the Aragon Voting contract. /// @param description The description of the vote. + /// @param proposalMetadata The metadata of the proposal. /// @return newVoteBytecode The bytecode for creating a new vote. - function getNewVoteCallBytecode(string memory description) external view returns (bytes memory newVoteBytecode) { + function getNewVoteCallBytecode(string memory description, string memory proposalMetadata) external view returns (bytes memory newVoteBytecode) { newVoteBytecode = CallsScriptBuilder.create( - address(VOTING_CONTRACT), abi.encodeCall(VOTING_CONTRACT.newVote, (getEVMScript(), description, false, false)) + address(VOTING_CONTRACT), abi.encodeCall(VOTING_CONTRACT.newVote, (getEVMScript(proposalMetadata), description, false, false)) )._result; } /// @notice Validates the specific vote on Aragon Voting contract. + /// @param voteId The ID of the vote. + /// @param proposalMetadata The metadata of the proposal. /// @return A boolean value indicating whether the vote is valid. - function isValidVoteScript(uint256 voteId) external view returns (bool) { + function isValidVoteScript(uint256 voteId, string memory proposalMetadata) external view returns (bool) { ( /*open*/ , /*executed*/ , /*startDate*/ @@ -89,7 +118,7 @@ abstract contract OmnibusBase { bytes memory script, /*phase*/ ) = VOTING_CONTRACT.getVote(voteId); - return keccak256(script) == keccak256(getEVMScript()); + return keccak256(script) == keccak256(getEVMScript(proposalMetadata)); } function _votingCall(address target, bytes memory data) internal pure returns (ScriptCall memory) { @@ -105,4 +134,4 @@ abstract contract OmnibusBase { forwarder, abi.encodeCall(IForwarder.forward, (CallsScriptBuilder.create(target, data).getResult())) ); } -} +} \ No newline at end of file diff --git a/deployed-mainnet.json b/deployed-mainnet.json index d1fd07bb43..08f6b59a20 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -594,5 +594,15 @@ "constructorArgs": [ "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" ] + }, + "dg:dualGovernance": { + "proxy": { + "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" + } + }, + "dg:emergencyProtectedTimelock": { + "proxy": { + "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" + } } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 5a1502e2f7..0f3e127cb1 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -163,7 +163,7 @@ const config: HardhatUserConfig = { }, ], overrides: { - "test/0.8.25/upgrade/TWVoteScript.sol": { + "contracts/upgrade/TWVoteScript.sol": { version: "0.8.25", settings: { viaIR: true, diff --git a/lib/state-file.ts b/lib/state-file.ts index 8e3fe3d2c6..54282a9c27 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -93,6 +93,9 @@ export enum Sk { validatorExitDelayVerifier = "validatorExitDelayVerifier", triggerableWithdrawalsGateway = "triggerableWithdrawalsGateway", TWVoteScript = "TWVoteScript", + // Dual Governance + dgDualGovernance = "dg:dualGovernance", + dgEmergencyProtectedTimelock = "dg:emergencyProtectedTimelock", } export function getAddress(contractKey: Sk): string { @@ -141,6 +144,9 @@ export function getAddress(contractKey: Sk): string { case Sk.depositContract: case Sk.validatorExitDelayVerifier: case Sk.triggerableWithdrawalsGateway: + case Sk.dgDualGovernance: + case Sk.dgEmergencyProtectedTimelock: + case Sk.TWVoteScript: default: throw new Error(`Unsupported contract entry key ${contractKey}`); } diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index becef93284..89697b824a 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -174,6 +174,7 @@ TRIGGERABLE_WITHDRAWALS_GATEWAY = "${triggerableWithdrawalsGateway.address}" `); await deployWithoutProxy(Sk.TWVoteScript, "TWVoteScript", deployer, [ state[Sk.appVoting].proxy.address, + state[Sk.dgDualGovernance].proxy.address, { // Contract addresses agent: agent, diff --git a/scripts/upgrade/steps/0500-mock-aragon-voting.ts b/scripts/upgrade/steps/0500-mock-aragon-voting.ts index 8ec3829464..8738ae1ae1 100644 --- a/scripts/upgrade/steps/0500-mock-aragon-voting.ts +++ b/scripts/upgrade/steps/0500-mock-aragon-voting.ts @@ -1,32 +1,11 @@ -import { TokenManager, TWVoteScript, Voting } from "typechain-types"; +import { mockDGAragonVoting } from "scripts/utils/upgrade"; -import { advanceChainTime, ether, log } from "lib"; -import { impersonate } from "lib/account"; -import { loadContract } from "lib/contract"; import { readNetworkState, Sk } from "lib/state-file"; -export async function main(): Promise { +export async function main(): Promise> { const state = readNetworkState(); - log("Starting mock Aragon voting..."); - const agentAddress = state[Sk.appAgent].proxy.address; - const votingAddress = state[Sk.appVoting].proxy.address; - const tokenManagerAddress = state[Sk.appTokenManager].proxy.address; - - const deployer = await impersonate(agentAddress, ether("100")); - - const voteScript = await loadContract("TWVoteScript", state[Sk.TWVoteScript].address); - const tokenManager = await loadContract("TokenManager", tokenManagerAddress); - const voting = await loadContract("Voting", votingAddress); - - const voteId = await voting.votesLength(); - console.log(await voteScript.getDebugParams()) - const newVoteBytecode = await voteScript.getNewVoteCallBytecode("TW Lido Upgrade description placeholder"); - await tokenManager.connect(deployer).forward(newVoteBytecode); - if (!(await voteScript.isValidVoteScript(voteId))) throw new Error("Vote script is not valid"); - await voting.connect(deployer).vote(voteId, true, false); - await advanceChainTime(await voting.voteTime()); - const executeTx = await voting.executeVote(voteId); - - const executeReceipt = await executeTx.wait(); - log.success("Voting executed: gas used", executeReceipt!.gasUsed); + const voteScriptAddress = state[Sk.TWVoteScript].address; + const votingDescription = "TW Lido Upgrade description placeholder"; + const proposalMetadata = "TW Lido Upgrade proposal metadata placeholder"; + return mockDGAragonVoting(voteScriptAddress, votingDescription, proposalMetadata, state); } diff --git a/scripts/utils/upgrade.ts b/scripts/utils/upgrade.ts index f093b6c151..a95909d21d 100644 --- a/scripts/utils/upgrade.ts +++ b/scripts/utils/upgrade.ts @@ -1,5 +1,14 @@ +import { TransactionReceipt } from "ethers"; import fs from "fs"; +import { IDualGovernance, IEmergencyProtectedTimelock, OmnibusBase, TokenManager, Voting } from "typechain-types"; + +import { advanceChainTime, ether, log } from "lib"; +import { impersonate } from "lib/account"; +import { loadContract } from "lib/contract"; +import { findEventsWithInterfaces } from "lib/event"; +import { DeploymentState, Sk } from "lib/state-file"; + const UPGRADE_PARAMETERS_FILE = process.env.UPGRADE_PARAMETERS_FILE; export function readUpgradeParameters() { @@ -10,3 +19,64 @@ export function readUpgradeParameters() { const rawData = fs.readFileSync(UPGRADE_PARAMETERS_FILE); return JSON.parse(rawData.toString()); } + +export async function mockDGAragonVoting( + omnibusScriptAddress: string, + description: string, + proposalMetadata: string, + state: DeploymentState, +): Promise<{ + voteId: bigint; + proposalId: bigint; + executeReceipt: TransactionReceipt; + scheduleReceipt: TransactionReceipt; + proposalExecutedReceipt: TransactionReceipt; +}> { + log("Starting mock Aragon voting..."); + const agentAddress = state[Sk.appAgent].proxy.address; + const votingAddress = state[Sk.appVoting].proxy.address; + const tokenManagerAddress = state[Sk.appTokenManager].proxy.address; + + const deployer = await impersonate(agentAddress, ether("100")); + const tokenManager = await loadContract("TokenManager", tokenManagerAddress); + const voting = await loadContract("Voting", votingAddress); + const timelock = await loadContract( + "IEmergencyProtectedTimelock", + state[Sk.dgEmergencyProtectedTimelock].proxy.address, + ); + const afterSubmitDelay = await timelock.getAfterSubmitDelay(); + const afterScheduleDelay = await timelock.getAfterScheduleDelay(); + + const voteId = await voting.votesLength(); + + const voteScriptTw = await loadContract("OmnibusBase", omnibusScriptAddress); + const voteBytecodeTw = await voteScriptTw.getNewVoteCallBytecode(description, proposalMetadata); + + await tokenManager.connect(deployer).forward(voteBytecodeTw); + if (!(await voteScriptTw.isValidVoteScript(voteId, proposalMetadata))) throw new Error("Vote script is not valid"); + await voting.connect(deployer).vote(voteId, true, false); + await advanceChainTime(await voting.voteTime()); + const executeTx = await voting.executeVote(voteId); + const executeReceipt = (await executeTx.wait())!; + log.success("TW voting executed: gas used", executeReceipt.gasUsed); + + const dualGovernance = await loadContract( + "IDualGovernance", + state[Sk.dgDualGovernance].proxy.address, + ); + const events = findEventsWithInterfaces(executeReceipt, "ProposalSubmitted", [dualGovernance.interface]); + const proposalId = events[0].args.id; + log.success("Proposal submitted: proposalId", proposalId); + + await advanceChainTime(afterSubmitDelay); + const scheduleTx = await dualGovernance.connect(deployer).scheduleProposal(proposalId); + const scheduleReceipt = (await scheduleTx.wait())!; + log.success("Proposal scheduled: gas used", scheduleReceipt.gasUsed); + + await advanceChainTime(afterScheduleDelay); + const proposalExecutedTx = await timelock.connect(deployer).execute(proposalId); + const proposalExecutedReceipt = (await proposalExecutedTx.wait())!; + log.success("Proposal executed: gas used", proposalExecutedReceipt.gasUsed); + + return { voteId, proposalId, executeReceipt, scheduleReceipt, proposalExecutedReceipt }; +} \ No newline at end of file diff --git a/test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol b/test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol deleted file mode 100644 index 0ea1fa80df..0000000000 --- a/test/0.8.25/upgrade/interfaces/IOracleReportSanityChecker_preV3.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - -/* See contracts/COMPILERS.md */ -// solhint-disable-next-line lido/fixed-compiler-version -pragma solidity >=0.4.24 <0.9.0; - - -/// @notice The set of restrictions used in the sanity checks of the oracle report -/// @dev struct is loaded from the storage and stored in memory during the tx running -struct LimitsList { - /// @notice The max possible number of validators that might be reported as `exited` - /// per single day, depends on the Consensus Layer churn limit - /// @dev Must fit into uint16 (<= 65_535) - uint256 exitedValidatorsPerDayLimit; - - /// @notice The max possible number of validators that might be reported as `appeared` - /// per single day, limited by the max daily deposits via DepositSecurityModule in practice - /// isn't limited by a consensus layer (because `appeared` includes `pending`, i.e., not `activated` yet) - /// @dev Must fit into uint16 (<= 65_535) - uint256 appearedValidatorsPerDayLimit; - - /// @notice The max annual increase of the total validators' balances on the Consensus Layer - /// since the previous oracle report - /// @dev Represented in the Basis Points (100% == 10_000) - uint256 annualBalanceIncreaseBPLimit; - - /// @notice The max deviation of the provided `simulatedShareRate` - /// and the actual one within the currently processing oracle report - /// @dev Represented in the Basis Points (100% == 10_000) - uint256 simulatedShareRateDeviationBPLimit; - - /// @notice The max number of exit requests allowed in report to ValidatorsExitBusOracle - uint256 maxValidatorExitRequestsPerReport; - - /// @notice The max number of data list items reported to accounting oracle in extra data per single transaction - /// @dev Must fit into uint16 (<= 65_535) - uint256 maxItemsPerExtraDataTransaction; - - /// @notice The max number of node operators reported per extra data list item - /// @dev Must fit into uint16 (<= 65_535) - uint256 maxNodeOperatorsPerExtraDataItem; - - /// @notice The min time required to be passed from the creation of the request to be - /// finalized till the time of the oracle report - uint256 requestTimestampMargin; - - /// @notice The positive token rebase allowed per single LidoOracle report - /// @dev uses 1e9 precision, e.g.: 1e6 - 0.1%; 1e9 - 100%, see `setMaxPositiveTokenRebase()` - uint256 maxPositiveTokenRebase; - - /// @notice Initial slashing amount per one validator to calculate initial slashing of the validators' balances on the Consensus Layer - /// @dev Represented in the PWei (1^15 Wei). Must fit into uint16 (<= 65_535) - uint256 initialSlashingAmountPWei; - - /// @notice Inactivity penalties amount per one validator to calculate penalties of the validators' balances on the Consensus Layer - /// @dev Represented in the PWei (1^15 Wei). Must fit into uint16 (<= 65_535) - uint256 inactivityPenaltiesAmountPWei; - - /// @notice The maximum percent on how Second Opinion Oracle reported value could be greater - /// than reported by the AccountingOracle. There is an assumption that second opinion oracle CL balance - /// can be greater as calculated for the withdrawal credentials. - /// @dev Represented in the Basis Points (100% == 10_000) - uint256 clBalanceOraclesErrorUpperBPLimit; -} - -/// @title Sanity checks for the Lido's oracle report -/// @notice The contracts contain methods to perform sanity checks of the Lido's oracle report -/// and lever methods for granular tuning of the params of the checks -interface IOracleReportSanityChecker_preV3 { - - /// @notice Returns the limits list for the Lido's oracle report sanity checks - function getOracleReportLimits() external view returns (LimitsList memory); -} - From 32fd51777e89966350887650dee53a1a56498397 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Mon, 21 Jul 2025 16:32:03 +0300 Subject: [PATCH 369/405] feat(orc-420): add ir for voting script --- foundry.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/foundry.toml b/foundry.toml index 15fdb28f4d..99ecd149ec 100644 --- a/foundry.toml +++ b/foundry.toml @@ -32,3 +32,11 @@ evm_version = "cancun" # fail_on_revert = true fmt = { int_types = 'long' } + +additional_compiler_profiles = [ + { name = "via-ir", via_ir = true, optimizer = true, optimizer_runs = 200 } +] + +compilation_restrictions = [ + { paths = "contracts/upgrade/*", via_ir = true, optimizer = true, optimizer_runs = 200 } +] \ No newline at end of file From ce5e3e5ccfa350b28e52028251a97621c5afa6dd Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 24 Jul 2025 19:34:36 +0300 Subject: [PATCH 370/405] feat(orc-420): fix signatures --- contracts/upgrade/TWVoteScript.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/upgrade/TWVoteScript.sol b/contracts/upgrade/TWVoteScript.sol index 21d8b3cd7e..0726d7267a 100644 --- a/contracts/upgrade/TWVoteScript.sol +++ b/contracts/upgrade/TWVoteScript.sol @@ -5,8 +5,8 @@ import {IAccessControl} from "@openzeppelin/contracts-v5.2/access/IAccessControl import {OmnibusBase} from "./utils/OmnibusBase.sol"; interface IOssifiableProxy { - function proxy__upgradeTo(address newImplementation) external; - function proxy__changeAdmin(address newAdmin) external; + function proxy__upgradeTo(address newImplementation_) external; + function proxy__changeAdmin(address newAdmin_) external; function proxy__getAdmin() external view returns (address); function proxy__getImplementation() external view returns (address); } @@ -35,7 +35,7 @@ interface IWithdrawalVault { } interface INodeOperatorsRegistry { - function finalizeUpgrade_v4(uint256 _exitDeadlineInSec) external; + function finalizeUpgrade_v4(uint256 _exitDeadlineThresholdInSeconds) external; } interface IOracleDaemonConfig { From 0f2876b56d843aa80409fa22edc67707c66a261e Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 24 Jul 2025 19:34:59 +0300 Subject: [PATCH 371/405] feat(orc-420): improve run tests script --- .../test-scratch-upgrade.sh | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh index 0f0c4f67c3..27a1700ca3 100644 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh @@ -1,9 +1,19 @@ -# RPC_URL: http://localhost:8555 -# DEPLOYER: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" # first acc of default mnemonic "test test ..." -# GAS_PRIORITY_FEE: 1 -# GAS_MAX_FEE: 100 -# NETWORK_STATE_FILE: deployed-mainnet-upgrade.json -# UPGRADE_PARAMETERS_FILE: upgrade-parameters-mainnet.json +# Start hardhat in background (suppress output) +yarn hardhat node --port 8545 --fork http://hr6vb81d1ndsx-rpc-3-mainnet-erigon.tooling-nodes.testnet.fi > /dev/null 2>&1 & +HARDHAT_PID=$! + +# Cleanup function to kill hardhat on exit +cleanup() { + echo "Stopping hardhat..." + kill $HARDHAT_PID 2>/dev/null + wait $HARDHAT_PID 2>/dev/null +} + +# Set trap to cleanup on script exit +trap cleanup EXIT + +# Wait for hardhat to start +sleep 5 export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise export SLOTS_PER_EPOCH=32 From 3e89cd3e7ce0a9bf265e78f1e9a2b3f5dd75437e Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Thu, 24 Jul 2025 20:08:14 +0300 Subject: [PATCH 372/405] feat(orc-420): fix validatorExitDelayVerifierArgs --- .../upgrade/steps/0100-deploy-tw-contracts.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index 89697b824a..fdbd90659a 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -110,12 +110,18 @@ export async function main() { const validatorExitDelayVerifierArgs = [ locator.address, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorPrev, - "0x0000000000000000000000000000000000000000000000000096000000000028", // GIndex gIFirstValidatorCurr, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesPrev, - "0x0000000000000000000000000000000000000000000000000000000000005b00", // GIndex gIHistoricalSummariesCurr, - 1, // uint64 firstSupportedSlot, - 1, // uint64 pivotSlot, + { + gIFirstValidatorPrev: "0x0000000000000000000000000000000000000000000000000096000000000028", + gIFirstValidatorCurr: "0x0000000000000000000000000000000000000000000000000096000000000028", + gIFirstHistoricalSummaryPrev: "0x000000000000000000000000000000000000000000000000000000b600000018", + gIFirstHistoricalSummaryCurr: "0x000000000000000000000000000000000000000000000000000000b600000018", + gIFirstBlockRootInSummaryPrev: "0x000000000000000000000000000000000000000000000000000000000040000d", + gIFirstBlockRootInSummaryCurr: "0x000000000000000000000000000000000000000000000000000000000040000d" + }, // GIndices struct + 22140000, // uint64 firstSupportedSlot, same as test data + 22140000, // uint64 pivotSlot, same as test data + 22140000, // uint64 capellaSlot, same as test data + 8192, // uint64 slotsPerHistoricalRoot, 32, // uint32 slotsPerEpoch, 12, // uint32 secondsPerSlot, genesisTime, // uint64 genesisTime, From b9f2066270679a1d74ebd73b2ed3c51d90fe8cf1 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Sun, 27 Jul 2025 14:19:16 +0300 Subject: [PATCH 373/405] feat(orc-420): fix staking limits dependent tests --- deployed-mainnet-upgrade.json | 80 ++++++++++++------- lib/protocol/helpers/withdrawal.ts | 22 +++-- test/integration/accounting.integration.ts | 6 +- .../protocol-happy-path.integration.ts | 17 ++-- 4 files changed, 78 insertions(+), 47 deletions(-) diff --git a/deployed-mainnet-upgrade.json b/deployed-mainnet-upgrade.json index f21eefd97c..5360361296 100644 --- a/deployed-mainnet-upgrade.json +++ b/deployed-mainnet-upgrade.json @@ -1,28 +1,30 @@ { "TWVoteScript": { - "contract": "test/0.8.25/upgrade/TWVoteScript.sol", - "address": "0xbdD488B78ac2b27052249e60E635B2533575a6Eb", + "contract": "contracts/upgrade/TWVoteScript.sol", + "address": "0x408888871A2Ffa9C7b73a07Aec0de0542b9ed43b", "constructorArgs": [ "0x2e59A20f205bB85a89C53f1936454680651E618e", + "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db", { "agent": "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", "lido_locator": "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - "lido_locator_impl": "0x16932B0c1eA503E4a40C7a75AC7200b4304C8De2", + "lido_locator_impl": "0x4fe0e1B3883141E59dddD86D6E18FC9c87EbBFf2", "validators_exit_bus_oracle": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", - "validators_exit_bus_oracle_impl": "0x2fcc261bB32262a150E4905F6d550D4FF05bC582", - "triggerable_withdrawals_gateway": "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a", + "validators_exit_bus_oracle_impl": "0xd3873FDF150b3fFFb447d3701DFD234DF452F367", + "triggerable_withdrawals_gateway": "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D", "withdrawal_vault": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", - "withdrawal_vault_impl": "0x63eE8865A8B25919B5103d02586AaaF078Ee9102", + "withdrawal_vault_impl": "0xfde41A17EBfA662867DA7324C0Bf5810623Cb3F8", "accounting_oracle": "0x852deD011285fe67063a08005c71a85690503Cee", - "accounting_oracle_impl": "0x4f0Ab9214649A6539586FbeB575b370Ba52Bd794", + "accounting_oracle_impl": "0x407154421a86338306c1e5abFFA6fF42d2cFeEdC", "staking_router": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", - "staking_router_impl": "0x90CA02Cb47113c75EB8E102c91B40181616cc9e9", - "validator_exit_verifier": "0xd15cF95D0DC31C7a01Ac5F73ccca6B572ADc8C05", + "staking_router_impl": "0x1Ae0817d98a8A222235A2383422e1A1c03d73e3a", + "validator_exit_verifier": "0x3B071F9a25B9Da0193E81F0a68b165d67Adb0714", "node_operators_registry": "0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5", - "node_operators_registry_impl": "0x4B651dcC3C2e4d2Fa6feF95D73eaEC48432b5d6a", + "node_operators_registry_impl": "0xB167Cb0b51983858EEc1E1716dF18a59A1fe35B4", "oracle_daemon_config": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", - "nor_app_repo": "0x0D97E876ad14DB2b183CFeEB8aa1A5C788eB1831", + "simple_dvt": "0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433", "node_operators_registry_app_id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", + "simple_dvt_app_id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4", "nor_version": [ 6, 0, @@ -52,7 +54,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x4f0Ab9214649A6539586FbeB575b370Ba52Bd794", + "address": "0x407154421a86338306c1e5abFFA6fF42d2cFeEdC", "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", @@ -151,7 +153,7 @@ }, "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x4B651dcC3C2e4d2Fa6feF95D73eaEC48432b5d6a", + "address": "0xB167Cb0b51983858EEc1E1716dF18a59A1fe35B4", "constructorArgs": [] }, "aragonApp": { @@ -299,6 +301,16 @@ "pauseIntentValidityPeriodBlocks": 6646 } }, + "dg:dualGovernance": { + "proxy": { + "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" + } + }, + "dg:emergencyProtectedTimelock": { + "proxy": { + "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" + } + }, "dummyEmptyContract": { "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", "contract": "contracts/0.8.9/utils/DummyEmptyContract.sol", @@ -375,7 +387,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x16932B0c1eA503E4a40C7a75AC7200b4304C8De2", + "address": "0x4fe0e1B3883141E59dddD86D6E18FC9c87EbBFf2", "constructorArgs": [ [ "0x852deD011285fe67063a08005c71a85690503Cee", @@ -392,8 +404,8 @@ "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", - "0xd15cF95D0DC31C7a01Ac5F73ccca6B572ADc8C05", - "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a" + "0x3B071F9a25B9Da0193E81F0a68b165d67Adb0714", + "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D" ] ] } @@ -473,7 +485,7 @@ "maxPositiveTokenRebase": 750000 } }, - "scratchDeployGasUsed": "24965663", + "scratchDeployGasUsed": "25571349", "stakingRouter": { "proxy": { "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", @@ -487,7 +499,7 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x90CA02Cb47113c75EB8E102c91B40181616cc9e9", + "address": "0x1Ae0817d98a8A222235A2383422e1A1c03d73e3a", "constructorArgs": [ "0x00000000219ab540356cBB839Cbe05303d7705Fa" ] @@ -496,7 +508,7 @@ "triggerableWithdrawalsGateway": { "implementation": { "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", - "address": "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a", + "address": "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D", "constructorArgs": [ "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", @@ -509,18 +521,24 @@ "validatorExitDelayVerifier": { "implementation": { "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", - "address": "0xd15cF95D0DC31C7a01Ac5F73ccca6B572ADc8C05", + "address": "0x3B071F9a25B9Da0193E81F0a68b165d67Adb0714", "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - "0x0000000000000000000000000000000000000000000000000096000000000028", - "0x0000000000000000000000000000000000000000000000000096000000000028", - "0x0000000000000000000000000000000000000000000000000000000000005b00", - "0x0000000000000000000000000000000000000000000000000000000000005b00", - 1, - 1, + { + "gIFirstValidatorPrev": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstValidatorCurr": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstHistoricalSummaryPrev": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstHistoricalSummaryCurr": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstBlockRootInSummaryPrev": "0x000000000000000000000000000000000000000000000000000000000040000d", + "gIFirstBlockRootInSummaryCurr": "0x000000000000000000000000000000000000000000000000000000000040000d" + }, + 22140000, + 22140000, + 22140000, + 8192, 32, 12, - 1639659600, + 1606824023, 98304 ] } @@ -538,10 +556,10 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0x2fcc261bB32262a150E4905F6d550D4FF05bC582", + "address": "0xd3873FDF150b3fFFb447d3701DFD234DF452F367", "constructorArgs": [ 12, - 1639659600, + 1606824023, "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb" ] } @@ -647,11 +665,11 @@ }, "implementation": { "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0x63eE8865A8B25919B5103d02586AaaF078Ee9102", + "address": "0xfde41A17EBfA662867DA7324C0Bf5810623Cb3F8", "constructorArgs": [ "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0x5E50A3d48982Ba8CCAfE398FB0f8881A31C4f67a" + "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D" ] } }, diff --git a/lib/protocol/helpers/withdrawal.ts b/lib/protocol/helpers/withdrawal.ts index eb10e630ba..e5edbebd05 100644 --- a/lib/protocol/helpers/withdrawal.ts +++ b/lib/protocol/helpers/withdrawal.ts @@ -1,4 +1,5 @@ import { ZeroAddress } from "ethers"; +import { ethers } from "hardhat"; import { certainAddress, ether, impersonate, log } from "lib"; @@ -27,12 +28,17 @@ export const unpauseWithdrawalQueue = async (ctx: ProtocolContext) => { export const finalizeWithdrawalQueue = async (ctx: ProtocolContext) => { const { lido, withdrawalQueue } = ctx.contracts; - const ethHolder = await impersonate(certainAddress("withdrawalQueue:eth:whale"), ether("100000")); + const unfinalizedAmount = await withdrawalQueue.unfinalizedStETH(); + const depositAmount = ether("10000"); + + const ethHolder = await impersonate( + certainAddress("withdrawalQueue:eth:whale"), + unfinalizedAmount + depositAmount + ether("10"), + ); const stEthHolder = await impersonate(certainAddress("withdrawalQueue:stEth:whale"), ether("100000")); - const stEthHolderAmount = ether("10000"); // Here sendTransaction is used to validate native way of submitting ETH for stETH - await stEthHolder.sendTransaction({ to: lido.address, value: stEthHolderAmount }); + await stEthHolder.sendTransaction({ to: lido.address, value: depositAmount }); let lastFinalizedRequestId = await withdrawalQueue.getLastFinalizedRequestId(); let lastRequestId = await withdrawalQueue.getLastRequestId(); @@ -48,10 +54,12 @@ export const finalizeWithdrawalQueue = async (ctx: ProtocolContext) => { "Last request ID": lastRequestId, }); - await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); - } + await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: depositAmount }); - await ctx.contracts.lido.connect(ethHolder).submit(ZeroAddress, { value: ether("10000") }); + // Mine N blocks to restore all staking limits + const limits = await ctx.contracts.lido.getStakeLimitFullInfo(); + await ethers.provider.send("hardhat_mine", [`0x${limits.maxStakeLimitGrowthBlocks.toString(16)}`]); + } log.success("Finalized withdrawal queue"); -}; +}; \ No newline at end of file diff --git a/test/integration/accounting.integration.ts b/test/integration/accounting.integration.ts index 9244371ee0..03d22a5f47 100644 --- a/test/integration/accounting.integration.ts +++ b/test/integration/accounting.integration.ts @@ -222,7 +222,7 @@ describe("Accounting", () => { ); }); - it.skip("Should account correctly with positive CL rebase close to the limits", async () => { + it("Should account correctly with positive CL rebase close to the limits", async () => { const { lido, accountingOracle, oracleReportSanityChecker, stakingRouter } = ctx.contracts; const { annualBalanceIncreaseBPLimit } = await oracleReportSanityChecker.getOracleReportLimits(); @@ -553,7 +553,7 @@ describe("Accounting", () => { expect(ctx.getEvents(reportTxReceipt, "ELRewardsReceived").length).be.equal(0); }); - it.skip("Should account correctly with withdrawals at limits", async () => { + it("Should account correctly with withdrawals at limits", async () => { const { lido, accountingOracle, withdrawalVault, stakingRouter } = ctx.contracts; const withdrawals = await rebaseLimitWei(); @@ -640,7 +640,7 @@ describe("Accounting", () => { expect(withdrawalVaultBalanceAfter).to.equal(0, "Expected withdrawals vault to be empty"); }); - it.skip("Should account correctly with withdrawals above limits", async () => { + it("Should account correctly with withdrawals above limits", async () => { const { lido, accountingOracle, withdrawalVault, stakingRouter } = ctx.contracts; const expectedWithdrawals = await rebaseLimitWei(); diff --git a/test/integration/protocol-happy-path.integration.ts b/test/integration/protocol-happy-path.integration.ts index 82a53ef136..2f8e0f4c79 100644 --- a/test/integration/protocol-happy-path.integration.ts +++ b/test/integration/protocol-happy-path.integration.ts @@ -200,23 +200,28 @@ describe("Protocol Happy Path", () => { const { depositSecurityModule } = ctx.contracts; - const withdrawalsUninitializedStETH = await withdrawalQueue.unfinalizedStETH(); + await lido.connect(stEthHolder).submit(ZeroAddress, { value: ether("3200") }); + + const withdrawalsUnfinalizedStETH = await withdrawalQueue.unfinalizedStETH(); const depositableEther = await lido.getDepositableEther(); + const bufferedEtherBeforeDeposit = await lido.getBufferedEther(); - const expectedDepositableEther = bufferedEtherBeforeDeposit - withdrawalsUninitializedStETH; + const expectedDepositableEther = bufferedEtherBeforeDeposit - withdrawalsUnfinalizedStETH; expect(depositableEther).to.equal(expectedDepositableEther, "Depositable ether"); log.debug("Depositable ether", { "Buffered ether": ethers.formatEther(bufferedEtherBeforeDeposit), - "Withdrawals uninitialized stETH": ethers.formatEther(withdrawalsUninitializedStETH), + "Withdrawals unfinalized stETH": ethers.formatEther(withdrawalsUnfinalizedStETH), "Depositable ether": ethers.formatEther(depositableEther), }); const dsmSigner = await impersonate(depositSecurityModule.address, ether("100")); - const stakingModules = (await stakingRouter.getStakingModules()).filter((m) => m.id === 1n); - console.log("Staking modules:", JSON.stringify(stakingModules, null, 2)); + const stakingModules = await stakingRouter.getStakingModules(); + + log.debug("Staking modules", { stakingModules }); + depositCount = 0n; let expectedBufferedEtherAfterDeposit = bufferedEtherBeforeDeposit; for (const module of stakingModules) { @@ -236,7 +241,7 @@ describe("Protocol Happy Path", () => { expectedBufferedEtherAfterDeposit -= unbufferedAmount; } - expect(depositCount).to.be.gt(0n, "Deposits"); + expect(depositCount).to.be.gt(0n, "No deposits applied"); const bufferedEtherAfterDeposit = await lido.getBufferedEther(); expect(bufferedEtherAfterDeposit).to.equal(expectedBufferedEtherAfterDeposit, "Buffered ether after deposit"); From c8714666091996666bdeb0df3653cd5570dbe57a Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Mon, 28 Jul 2025 19:38:53 +0300 Subject: [PATCH 374/405] feat(orc-420): remove old code --- lib/protocol/context.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/protocol/context.ts b/lib/protocol/context.ts index b31b285675..5f6d4b6232 100644 --- a/lib/protocol/context.ts +++ b/lib/protocol/context.ts @@ -12,15 +12,6 @@ const getSigner = async (signer: Signer, balance = ether("100"), signers: Protoc }; export const getProtocolContext = async (): Promise => { - // if (hre.network.name === "hardhat") { - // const networkConfig = hre.config.networks[hre.network.name]; - // if (!networkConfig.forking?.enabled) { - // await deployScratchProtocol(hre.network.name); - // } - // } else { - // await deployUpgrade(hre.network.name); - // } - const { contracts, signers } = await discover(); const interfaces = Object.values(contracts).map((contract) => contract.interface); From d30d2b2d11fd5c9390f8e3bf50a2d52c0f965f36 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 5 Aug 2025 14:41:06 +0200 Subject: [PATCH 375/405] fix: remove deployed-mainnet-upgrade.json --- .gitignore | 1 + deployed-mainnet-upgrade.json | 684 ---------------------------------- 2 files changed, 1 insertion(+), 684 deletions(-) delete mode 100644 deployed-mainnet-upgrade.json diff --git a/.gitignore b/.gitignore index 3776df612d..8f5ed142aa 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ accounts.json deployed-local.json deployed-hardhat.json deployed-local-devnet.json +deployed-mainnet-upgrade.json # MacOS .DS_Store diff --git a/deployed-mainnet-upgrade.json b/deployed-mainnet-upgrade.json deleted file mode 100644 index 5360361296..0000000000 --- a/deployed-mainnet-upgrade.json +++ /dev/null @@ -1,684 +0,0 @@ -{ - "TWVoteScript": { - "contract": "contracts/upgrade/TWVoteScript.sol", - "address": "0x408888871A2Ffa9C7b73a07Aec0de0542b9ed43b", - "constructorArgs": [ - "0x2e59A20f205bB85a89C53f1936454680651E618e", - "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db", - { - "agent": "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "lido_locator": "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - "lido_locator_impl": "0x4fe0e1B3883141E59dddD86D6E18FC9c87EbBFf2", - "validators_exit_bus_oracle": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", - "validators_exit_bus_oracle_impl": "0xd3873FDF150b3fFFb447d3701DFD234DF452F367", - "triggerable_withdrawals_gateway": "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D", - "withdrawal_vault": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", - "withdrawal_vault_impl": "0xfde41A17EBfA662867DA7324C0Bf5810623Cb3F8", - "accounting_oracle": "0x852deD011285fe67063a08005c71a85690503Cee", - "accounting_oracle_impl": "0x407154421a86338306c1e5abFFA6fF42d2cFeEdC", - "staking_router": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", - "staking_router_impl": "0x1Ae0817d98a8A222235A2383422e1A1c03d73e3a", - "validator_exit_verifier": "0x3B071F9a25B9Da0193E81F0a68b165d67Adb0714", - "node_operators_registry": "0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5", - "node_operators_registry_impl": "0xB167Cb0b51983858EEc1E1716dF18a59A1fe35B4", - "oracle_daemon_config": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", - "simple_dvt": "0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433", - "node_operators_registry_app_id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", - "simple_dvt_app_id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4", - "nor_version": [ - 6, - 0, - 0 - ], - "vebo_consensus_version": 4, - "ao_consensus_version": 4, - "nor_exit_deadline_in_sec": 1800, - "exit_events_lookback_window_in_slots": 7200, - "nor_content_uri": "0x697066733a516d61375058486d456a346a7332676a4d3976744850747176754b3832695335455950694a6d7a4b4c7a55353847" - } - ] - }, - "accountingOracle": { - "deployParameters": { - "consensusVersion": 1 - }, - "proxy": { - "address": "0x852deD011285fe67063a08005c71a85690503Cee", - "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", - "deployTx": "0x3def88f27741216b131de2861cf89af2ca2ac4242b384ee33dca8cc70c51c8dd", - "constructorArgs": [ - "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x" - ] - }, - "implementation": { - "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x407154421a86338306c1e5abFFA6fF42d2cFeEdC", - "constructorArgs": [ - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0x442af784A788A5bd6F42A01Ebe9F287a871243fb", - 12, - 1606824023 - ] - } - }, - "apmRegistryFactoryAddress": "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A", - "app:aragon-agent": { - "implementation": { - "contract": "@aragon/apps-agent/contracts/Agent.sol", - "address": "0x3A93C17FC82CC33420d1809dDA9Fb715cc89dd37", - "constructorArgs": [] - }, - "aragonApp": { - "name": "aragon-agent", - "fullName": "aragon-agent.lidopm.eth", - "id": "0x701a4fd1f5174d12a0f1d9ad2c88d0ad11ab6aad0ac72b7d9ce621815f8016a9" - }, - "proxy": { - "address": "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, - "app:aragon-finance": { - "implementation": { - "contract": "@aragon/apps-finance/contracts/Finance.sol", - "address": "0x836835289A2E81B66AE5d95b7c8dBC0480dCf9da", - "constructorArgs": [] - }, - "aragonApp": { - "name": "aragon-finance", - "fullName": "aragon-finance.lidopm.eth", - "id": "0x5c9918c99c4081ca9459c178381be71d9da40e49e151687da55099c49a4237f1" - }, - "proxy": { - "address": "0xB9E5CBB9CA5b0d659238807E84D0176930753d86", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, - "app:aragon-token-manager": { - "implementation": { - "contract": "@aragon/apps-lido/apps/token-manager/contracts/TokenManager.sol", - "address": "0xde3A93028F2283cc28756B3674BD657eaFB992f4", - "constructorArgs": [] - }, - "aragonApp": { - "name": "aragon-token-manager", - "fullName": "aragon-token-manager.lidopm.eth", - "id": "0xcd567bdf93dd0f6acc3bc7f2155f83244d56a65abbfbefb763e015420102c67b" - }, - "proxy": { - "address": "0xf73a1260d222f447210581DDf212D915c09a3249", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, - "app:aragon-voting": { - "implementation": { - "contract": "@aragon/apps-lido/apps/voting/contracts/Voting.sol", - "address": "0x72fb5253AD16307B9E773d2A78CaC58E309d5Ba4", - "constructorArgs": [] - }, - "aragonApp": { - "name": "aragon-voting", - "fullName": "aragon-voting.lidopm.eth", - "id": "0x0abcd104777321a82b010357f20887d61247493d89d2e987ff57bcecbde00e1e" - }, - "proxy": { - "address": "0x2e59A20f205bB85a89C53f1936454680651E618e", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol" - } - }, - "app:lido": { - "proxy": { - "address": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - }, - "implementation": { - "address": "0x17144556fd3424EDC8Fc8A4C940B2D04936d17eb", - "contract": "contracts/0.4.24/Lido.sol", - "deployTx": "0xb4b5e02643c9802fd0f7c73c4854c4f1b83497aca13f8297ba67207b71c4dcd9", - "constructorArgs": [] - }, - "aragonApp": { - "fullName": "lido.lidopm.eth", - "name": "lido", - "id": "0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320", - "ipfsCid": "QmQkJMtvu4tyJvWrPXJfjLfyTWn959iayyNjp7YqNzX7pS", - "contentURI": "0x697066733a516d516b4a4d7476753474794a76577250584a666a4c667954576e393539696179794e6a703759714e7a58377053" - } - }, - "app:node-operators-registry": { - "proxy": { - "address": "0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5" - }, - "implementation": { - "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0xB167Cb0b51983858EEc1E1716dF18a59A1fe35B4", - "constructorArgs": [] - }, - "aragonApp": { - "fullName": "node-operators-registry.lidopm.eth", - "name": "node-operators-registry", - "id": "0x7071f283424072341f856ac9e947e7ec0eb68719f757a7e785979b6b8717579d", - "ipfsCid": "Qma7PXHmEj4js2gjM9vtHPtqvuK82iS5EYPiJmzKLzU58G", - "contentURI": "0x697066733a516d61375058486d456a346a7332676a4d3976744850747176754b3832695335455950694a6d7a4b4c7a55353847" - } - }, - "app:oracle": { - "proxy": { - "address": "0x442af784A788A5bd6F42A01Ebe9F287a871243fb" - }, - "implementation": { - "address": "0xa29b819654cE6224A222bb5f586920105E2D7E0E", - "contract": "contracts/0.4.24/oracle/LegacyOracle.sol", - "deployTx": "0xe666e3ce409bb4c18e1016af0b9ed3495b20361a69f2856bccb9e67599795b6f", - "constructorArgs": [] - }, - "aragonApp": { - "fullName": "oracle.lidopm.eth", - "name": "oracle", - "id": "0x8b47ba2a8454ec799cd91646e7ec47168e91fd139b23f017455f3e5898aaba93", - "ipfsCid": "QmUMPfiEKq5Mxm8y2GYQPLujGaJiWz1tvep5W7EdAGgCR8", - "contentURI": "0x697066733a516d656138394d5533504852503763513157616b3672327355654d554146324c39727132624c6d5963644b764c57" - } - }, - "app:simple-dvt": { - "stakingRouterModuleParams": { - "moduleName": "SimpleDVT", - "moduleType": "curated-onchain-v1", - "targetShare": 50, - "moduleFee": 800, - "treasuryFee": 200, - "penaltyDelay": 432000, - "easyTrackTrustedCaller": "0x08637515E85A4633E23dfc7861e2A9f53af640f7", - "easyTrackAddress": "0xF0211b7660680B49De1A7E9f25C65660F0a13Fea", - "easyTrackFactories": { - "AddNodeOperators": "0xcAa3AF7460E83E665EEFeC73a7a542E5005C9639", - "ActivateNodeOperators": "0xCBb418F6f9BFd3525CE6aADe8F74ECFEfe2DB5C8", - "DeactivateNodeOperators": "0x8B82C1546D47330335a48406cc3a50Da732672E7", - "SetVettedValidatorsLimits": "0xD75778b855886Fc5e1eA7D6bFADA9EB68b35C19D", - "SetNodeOperatorNames": "0x7d509BFF310d9460b1F613e4e40d342201a83Ae4", - "SetNodeOperatorRewardAddresses": "0x589e298964b9181D9938B84bB034C3BB9024E2C0", - "UpdateTargetValidatorLimits": "0x41CF3DbDc939c5115823Fba1432c4EC5E7bD226C", - "ChangeNodeOperatorManagers": "0xE31A0599A6772BCf9b2bFc9e25cf941e793c9a7D" - } - }, - "aragonApp": { - "name": "simple-dvt", - "fullName": "simple-dvt.lidopm.eth", - "id": "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4", - "ipfsCid": "QmaSSujHCGcnFuetAPGwVW5BegaMBvn5SCsgi3LSfvraSo", - "contentURI": "0x697066733a516d615353756a484347636e4675657441504777565735426567614d42766e355343736769334c5366767261536f" - }, - "proxy": { - "address": "0xaE7B191A31f627b4eB1d4DaC64eaB9976995b433", - "contract": "@aragon/os/contracts/apps/AppProxyUpgradeable.sol", - "constructorArgs": [ - "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", - "0xe1635b63b5f7b5e545f2a637558a4029dea7905361a2f0fc28c66e9136cf86a4", - "0x" - ] - }, - "implementation": { - "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x1770044a38402e3CfCa2Fcfa0C84a093c9B42135", - "constructorArgs": [] - } - }, - "aragon-kernel": { - "implementation": { - "contract": "@aragon/os/contracts/kernel/Kernel.sol", - "address": "0x2b33CF282f867A7FF693A66e11B0FcC5552e4425", - "constructorArgs": [ - true - ] - }, - "proxy": { - "address": "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", - "contract": "@aragon/os/contracts/kernel/KernelProxy.sol" - } - }, - "aragonIDAddress": "0x546aa2eae2514494eeadb7bbb35243348983c59d", - "burner": { - "address": "0xD15a672319Cf0352560eE76d9e89eAB0889046D3", - "contract": "contracts/0.8.9/Burner.sol", - "deployTx": "0xbebf5c85404a0d8e36b859046c984fdf6dd764b5d317feb7eb3525016005b1d9", - "constructorArgs": [ - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0", - "32145684728326685744" - ], - "deployParameters": { - "totalCoverSharesBurnt": "0", - "totalNonCoverSharesBurnt": "32145684728326685744" - } - }, - "chainSpec": { - "depositContractAddress": "0x00000000219ab540356cBB839Cbe05303d7705Fa", - "slotsPerEpoch": 32, - "secondsPerSlot": 12, - "genesisTime": 1606824023 - }, - "createAppReposTx": "0xf48cb21c6be021dd18bd8e02ce89ac7b924245b859f0a8b7c47e88a39016ed41", - "daoAragonId": "lido-dao", - "daoFactoryAddress": "0x7378ad1ba8f3c8e64bbb2a04473edd35846360f1", - "daoInitialSettings": { - "token": { - "name": "Lido DAO Token", - "symbol": "LDO" - }, - "voting": { - "minSupportRequired": "500000000000000000", - "minAcceptanceQuorum": "50000000000000000", - "voteDuration": 86400 - }, - "fee": { - "totalPercent": 10, - "treasuryPercent": 0, - "insurancePercent": 50, - "nodeOperatorsPercent": 50 - } - }, - "daoTokenAddress": "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", - "deployCommit": "e45c4d6fb8120fd29426b8d969c19d8a798ca974", - "deployer": "0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA", - "depositSecurityModule": { - "address": "0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD", - "contract": "contracts/0.8.9/DepositSecurityModule.sol", - "deployTx": "0x21307a2321f167f99de11ccec86d7bdd8233481bbffa493e15c519ca8d662c4f", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0x00000000219ab540356cBB839Cbe05303d7705Fa", - "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", - 6646, - 200 - ], - "deployParameters": { - "maxDepositsPerBlock": 150, - "minDepositBlockDistance": 25, - "pauseIntentValidityPeriodBlocks": 6646 - } - }, - "dg:dualGovernance": { - "proxy": { - "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" - } - }, - "dg:emergencyProtectedTimelock": { - "proxy": { - "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" - } - }, - "dummyEmptyContract": { - "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", - "contract": "contracts/0.8.9/utils/DummyEmptyContract.sol", - "deployTx": "0x9d76786f639bd18365f10c087444761db5dafd0edc85c5c1a3e90219f2d1331d", - "constructorArgs": [] - }, - "eip712StETH": { - "address": "0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7", - "contract": "contracts/0.8.9/EIP712StETH.sol", - "deployTx": "0xecb5010620fb13b0e2bbc98b8a0c82de0d7385491452cd36cf303cd74216ed91", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - ] - }, - "ensAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", - "executionLayerRewardsVault": { - "address": "0x388C818CA8B9251b393131C08a736A67ccB19297", - "contract": "contracts/0.8.9/LidoExecutionLayerRewardsVault.sol", - "deployTx": "0xd72cf25e4a5fe3677b6f9b2ae13771e02ad66f8d2419f333bb8bde3147bd4294" - }, - "hashConsensusForAccountingOracle": { - "address": "0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288", - "contract": "contracts/0.8.9/oracle/HashConsensus.sol", - "deployTx": "0xd74dcca9bacede9f332d70562f49808254061853937ffbbfc7397ab5d017041a", - "constructorArgs": [ - 32, - 12, - 1606824023, - 225, - 100, - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x852deD011285fe67063a08005c71a85690503Cee" - ], - "deployParameters": { - "fastLaneLengthSlots": 100, - "epochsPerFrame": 225 - } - }, - "hashConsensusForValidatorsExitBusOracle": { - "address": "0x7FaDB6358950c5fAA66Cb5EB8eE5147De3df355a", - "contract": "contracts/0.8.9/oracle/HashConsensus.sol", - "deployTx": "0xed1ab73dd5458b5ec0b174508318d2f39a31029112af21f87d09106933bd3a9e", - "constructorArgs": [ - 32, - 12, - 1606824023, - 75, - 100, - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e" - ], - "deployParameters": { - "fastLaneLengthSlots": 100, - "epochsPerFrame": 75 - } - }, - "ipfsAPI": "https://ipfs.infura.io:5001/api/v0", - "lidoApm": { - "deployTx": "0xfa66476569ecef5790f2d0634997b952862bbca56aa088f151b8049421eeb87b", - "address": "0x0cb113890b04B49455DfE06554e2D784598A29C9" - }, - "lidoApmEnsName": "lidopm.eth", - "lidoApmEnsRegDurationSec": 94608000, - "lidoLocator": { - "proxy": { - "address": "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", - "deployTx": "0x3a2910624533935cc8c21837b1705bcb159a760796930097016186be705cc455", - "constructorArgs": [ - "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x" - ] - }, - "implementation": { - "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x4fe0e1B3883141E59dddD86D6E18FC9c87EbBFf2", - "constructorArgs": [ - [ - "0x852deD011285fe67063a08005c71a85690503Cee", - "0xfFA96D84dEF2EA035c7AB153D8B991128e3d72fD", - "0x388C818CA8B9251b393131C08a736A67ccB19297", - "0x442af784A788A5bd6F42A01Ebe9F287a871243fb", - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0x6232397ebac4f5772e53285B26c47914E9461E75", - "0xe6793B9e4FbA7DE0ee833F9D02bba7DB5EB27823", - "0xD15a672319Cf0352560eE76d9e89eAB0889046D3", - "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", - "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", - "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", - "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", - "0x3B071F9a25B9Da0193E81F0a68b165d67Adb0714", - "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D" - ] - ] - } - }, - "lidoTemplate": { - "contract": "contracts/0.4.24/template/LidoTemplate.sol", - "address": "0x752350797CB92Ad3BF1295Faf904B27585e66BF5", - "deployTx": "0xdcd4ebe028aa3663a1fe8bbc92ae8489045e29d2a6ef5284083d9be5c3fa5f19", - "constructorArgs": [ - "0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA", - "0x7378ad1ba8f3c8e64bbb2a04473edd35846360f1", - "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", - "0x909d05f384d0663ed4be59863815ab43b4f347ec", - "0x546aa2eae2514494eeadb7bbb35243348983c59d", - "0xa0BC4B67F5FacDE4E50EAFF48691Cfc43F4E280A" - ] - }, - "minFirstAllocationStrategy": { - "contract": "contracts/common/lib/MinFirstAllocationStrategy.sol", - "address": "0x7e70De6D1877B3711b2bEDa7BA00013C7142d993", - "constructorArgs": [] - }, - "miniMeTokenFactoryAddress": "0x909d05f384d0663ed4be59863815ab43b4f347ec", - "networkId": 1, - "newDaoTx": "0x3feabd79e8549ad68d1827c074fa7123815c80206498946293d5373a160fd866", - "oracleDaemonConfig": { - "address": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", - "contract": "contracts/0.8.9/OracleDaemonConfig.sol", - "deployTx": "0xa4f380b8806f5a504ef67fce62989e09be5a48bf114af63483c01c22f0c9a36f", - "constructorArgs": [ - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - [] - ], - "deployParameters": { - "NORMALIZED_CL_REWARD_PER_EPOCH": 64, - "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000, - "REBASE_CHECK_NEAREST_EPOCH_DISTANCE": 1, - "REBASE_CHECK_DISTANT_EPOCH_DISTANCE": 23, - "VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS": 7200, - "VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS": 28800, - "PREDICTION_DURATION_IN_SLOTS": 50400, - "FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT": 1350, - "NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP": 100 - } - }, - "oracleReportSanityChecker": { - "address": "0x6232397ebac4f5772e53285B26c47914E9461E75", - "contract": "contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol", - "deployTx": "0x700c83996ad7deefda286044280ad86108dfef9c880909bd8e75a3746f7d631c", - "constructorArgs": [ - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - [ - 9000, - 43200, - 1000, - 50, - 600, - 8, - 24, - 7680, - 750000, - 1000, - 101, - 50 - ] - ], - "deployParameters": { - "churnValidatorsPerDayLimit": 20000, - "oneOffCLBalanceDecreaseBPLimit": 500, - "annualBalanceIncreaseBPLimit": 1000, - "simulatedShareRateDeviationBPLimit": 50, - "maxValidatorExitRequestsPerReport": 600, - "maxAccountingExtraDataListItemsCount": 2, - "maxNodeOperatorsPerExtraDataItemCount": 100, - "requestTimestampMargin": 7680, - "maxPositiveTokenRebase": 750000 - } - }, - "scratchDeployGasUsed": "25571349", - "stakingRouter": { - "proxy": { - "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", - "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", - "deployTx": "0xb8620f04a8db6bb52cfd0978c6677a5f16011e03d4622e5d660ea6ba34c2b122", - "constructorArgs": [ - "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x" - ] - }, - "implementation": { - "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x1Ae0817d98a8A222235A2383422e1A1c03d73e3a", - "constructorArgs": [ - "0x00000000219ab540356cBB839Cbe05303d7705Fa" - ] - } - }, - "triggerableWithdrawalsGateway": { - "implementation": { - "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", - "address": "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D", - "constructorArgs": [ - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - 13000, - 1, - 48 - ] - } - }, - "validatorExitDelayVerifier": { - "implementation": { - "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", - "address": "0x3B071F9a25B9Da0193E81F0a68b165d67Adb0714", - "constructorArgs": [ - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - { - "gIFirstValidatorPrev": "0x0000000000000000000000000000000000000000000000000096000000000028", - "gIFirstValidatorCurr": "0x0000000000000000000000000000000000000000000000000096000000000028", - "gIFirstHistoricalSummaryPrev": "0x000000000000000000000000000000000000000000000000000000b600000018", - "gIFirstHistoricalSummaryCurr": "0x000000000000000000000000000000000000000000000000000000b600000018", - "gIFirstBlockRootInSummaryPrev": "0x000000000000000000000000000000000000000000000000000000000040000d", - "gIFirstBlockRootInSummaryCurr": "0x000000000000000000000000000000000000000000000000000000000040000d" - }, - 22140000, - 22140000, - 22140000, - 8192, - 32, - 12, - 1606824023, - 98304 - ] - } - }, - "validatorsExitBusOracle": { - "proxy": { - "address": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", - "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", - "deployTx": "0xef3eea1523d2161c2f36ba61e327e3520231614c055b8a88c7f5928d18e423ea", - "constructorArgs": [ - "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x" - ] - }, - "implementation": { - "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0xd3873FDF150b3fFFb447d3701DFD234DF452F367", - "constructorArgs": [ - 12, - 1606824023, - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb" - ] - } - }, - "vestingParams": { - "unvestedTokensAmount": "363197500000000000000000000", - "holders": { - "0x9Bb75183646e2A0DC855498bacD72b769AE6ceD3": "20000000000000000000000000", - "0x0f89D54B02ca570dE82F770D33c7B7Cf7b3C3394": "25000000000000000000000000", - "0xe49f68B9A01d437B0b7ea416376a7AB21532624e": "2282000000000000000000000", - "0xb842aFD82d940fF5D8F6EF3399572592EBF182B0": "17718000000000000000000000", - "0x9849c2C1B73B41AEE843A002C332a2d16aaaB611": "10000000000000000000000000", - "0x96481cb0fcd7673254ebccc42dce9b92da10ea04": "5000000000000000000000000", - "0xB3DFe140A77eC43006499CB8c2E5e31975caD909": "7500000000000000000000000", - "0x61C808D82A3Ac53231750daDc13c777b59310bD9": "20000000000000000000000000", - "0x447f95026107aaed7472A0470931e689f51e0e42": "20000000000000000000000000", - "0x6ae83EAB68b7112BaD5AfD72d6B24546AbFF137D": "2222222220000000000000000", - "0xC24da173A250e9Ca5c54870639EbE5f88be5102d": "17777777780000000000000000", - "0x1f3813fE7ace2a33585F1438215C7F42832FB7B3": "20000000000000000000000000", - "0x82a8439BA037f88bC73c4CCF55292e158A67f125": "7000000000000000000000000", - "0x91715128a71c9C734CDC20E5EdEEeA02E72e428E": "15000000000000000000000000", - "0xB5587A54fF7022AC218438720BDCD840a32f0481": "5000000000000000000000000", - "0xf5fb27b912d987b5b6e02a1b1be0c1f0740e2c6f": "2000000000000000000000000", - "0x8b1674a617F103897Fb82eC6b8EB749BA0b9765B": "15000000000000000000000000", - "0x48Acf41D10a063f9A6B718B9AAd2e2fF5B319Ca2": "5000000000000000000000000", - "0x7eE09c11D6Dc9684D6D5a4C6d333e5b9e336bb6C": "10000000000000000000000000", - "0x11099aC9Cc097d0C9759635b8e16c6a91ECC43dA": "2000000000000000000000000", - "0x3d4AD2333629eE478E4f522d60A56Ae1Db5D3Cdb": "5000000000000000000000000", - "0xd5eCB56c6ca8f8f52D2DB4dC1257d6161cf3Da29": "100000000000000000000000", - "0x7F5e13a815EC9b4466d283CD521eE9829e7F6f0e": "200000000000000000000000", - "0x2057cbf2332ab2697a52B8DbC85756535d577e32": "500000000000000000000000", - "0x537dfB5f599A3d15C50E2d9270e46b808A52559D": "1000000000000000000000000", - "0x33c4c38e96337172d3de39df82060de26b638c4b": "550000000000000000000000", - "0x6094E1Dd925caCe56Fa501dAEc02b01a49E55770": "300000000000000000000000", - "0x977911f476B28f9F5332fA500387deE81e480a44": "40000000000000000000000", - "0x66d3FdA643320c6DddFBba39e635288A5dF75FB9": "200000000000000000000000", - "0xDFC0ae54af992217100845597982274A26d8CB28": "12500000000000000000000", - "0x32254b28F793CC18B3575C86c61fE3D7421cbbef": "500000000000000000000000", - "0x0Bf5566fB5F1f9934a3944AEF128a1b1a8cF3f17": "50000000000000000000000", - "0x1d3Fa8bf35870271115B997b8eCFe18529422a16": "50000000000000000000000", - "0x366B9729C5A89EC4618A0AB95F832E411eaE8237": "200000000000000000000000", - "0x20921142A35c89bE5D002973d2D6B72d9a625FB0": "200000000000000000000000", - "0x663b91628674846e8D1CBB779EFc8202d86284E2": "7500000000000000000000000", - "0xa6829908f728C6bC5627E2aFe93a0B71E978892D": "300000000000000000000000", - "0x9575B7859DF77F2A0EF034339b80e24dE44AB3F6": "200000000000000000000000", - "0xEe217c23131C6F055F7943Ef1f80Bec99dF35244": "400000000000000000000000", - "0xadde043f556d1083f060A7298E79eaBa08A3a077": "400000000000000000000000", - "0xaFBEfC8401c885A0bb6Ea6Af43f592A015433C65": "200000000000000000000000", - "0x8a62A63b877877bd5B1209B9b67F3d2685284268": "200000000000000000000000", - "0x62Ac238Ac055017DEcAb645E7E56176749f316d0": "200000000000000000000000", - "0x55Bc991b2edF3DDb4c520B222bE4F378418ff0fA": "5000000000000000000000000", - "0x8D689476EB446a1FB0065bFFAc32398Ed7F89165": "10000000000000000000000000", - "0x083fc10cE7e97CaFBaE0fE332a9c4384c5f54E45": "5000000000000000000000000", - "0x0028E24e4Fe5184792Bd0Cf498C11AE5b76185f5": "5000000000000000000000000", - "0xFe45baf0F18c207152A807c1b05926583CFE2e4b": "5000000000000000000000000", - "0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE": "5000000000000000000000000", - "0xD7f0dDcBb0117A53e9ff2cad814B8B810a314f59": "5000000000000000000000000", - "0xb8d83908AAB38a159F3dA47a59d84dB8e1838712": "50000000000000000000000000", - "0xA2dfC431297aee387C05bEEf507E5335E684FbCD": "50000000000000000000000000", - "0x1597D19659F3DE52ABd475F7D2314DCca29359BD": "50000000000000000000000000", - "0x695C388153bEa0fbE3e1C049c149bAD3bc917740": "50000000000000000000000000", - "0x945755dE7EAc99008c8C57bdA96772d50872168b": "50000000000000000000000000", - "0xFea88380bafF95e85305419eB97247981b1a8eEE": "30000000000000000000000000", - "0xAD4f7415407B83a081A0Bee22D05A8FDC18B42da": "50000000000000000000000000", - "0x68335B3ac272C8238b722963368F87dE736b64D6": "5000000000000000000000000", - "0xfA2Ab7C161Ef7F83194498f36ca7aFba90FD08d4": "5000000000000000000000000", - "0x58A764028350aB15899fDCcAFFfd3940e602CEEA": "10000000000000000000000000" - }, - "start": 1639785600, - "cliff": 1639785600, - "end": 1671321600, - "revokable": false - }, - "withdrawalQueueERC721": { - "proxy": { - "address": "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", - "contract": "contracts/0.8.9/proxy/OssifiableProxy.sol", - "deployTx": "0x98c2170be034f750f5006cb69ea0aeeaf0858b11f6324ee53d582fa4dd49bc1a", - "constructorArgs": [ - "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - "0x" - ] - }, - "implementation": { - "address": "0xE42C659Dc09109566720EA8b2De186c2Be7D94D9", - "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol", - "deployTx": "0x6ab0151735c01acdef518421358d41a08752169bc383c57d57f5bfa135ac6eb1", - "constructorArgs": [ - "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "Lido: stETH Withdrawal NFT", - "unstETH" - ], - "deployParameters": { - "name": "Lido: stETH Withdrawal NFT", - "symbol": "unstETH" - } - } - }, - "withdrawalVault": { - "proxy": { - "address": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f" - }, - "implementation": { - "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0xfde41A17EBfA662867DA7324C0Bf5810623Cb3F8", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0xbdB493827007eE26c16F10F6EABad6E97D9ead7D" - ] - } - }, - "wstETH": { - "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "contract": "contracts/0.6.12/WstETH.sol", - "deployTx": "0xaf2c1a501d2b290ef1e84ddcfc7beb3406f8ece2c46dee14e212e8233654ff05", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - ] - } -} From 870e0b8545df7e2d2bd045c659f3ca7e0f832691 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 5 Aug 2025 14:41:26 +0200 Subject: [PATCH 376/405] feat: remove unused test-scratch-upgrade.sh script --- .../test-scratch-upgrade.sh | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 scripts/triggerable-withdrawals/test-scratch-upgrade.sh diff --git a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh b/scripts/triggerable-withdrawals/test-scratch-upgrade.sh deleted file mode 100644 index 27a1700ca3..0000000000 --- a/scripts/triggerable-withdrawals/test-scratch-upgrade.sh +++ /dev/null @@ -1,36 +0,0 @@ -# Start hardhat in background (suppress output) -yarn hardhat node --port 8545 --fork http://hr6vb81d1ndsx-rpc-3-mainnet-erigon.tooling-nodes.testnet.fi > /dev/null 2>&1 & -HARDHAT_PID=$! - -# Cleanup function to kill hardhat on exit -cleanup() { - echo "Stopping hardhat..." - kill $HARDHAT_PID 2>/dev/null - wait $HARDHAT_PID 2>/dev/null -} - -# Set trap to cleanup on script exit -trap cleanup EXIT - -# Wait for hardhat to start -sleep 5 - -export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise -export SLOTS_PER_EPOCH=32 -export GENESIS_TIME=1606824023 # just some time -# export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" -# export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" - -export DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 # first acc of default mnemonic "test test ..." -export GAS_PRIORITY_FEE=1 -export GAS_MAX_FEE=100 - -export NETWORK_STATE_FILE=deployed-mainnet-upgrade.json - -cp deployed-mainnet.json $NETWORK_STATE_FILE - -yarn upgrade:deploy -yarn upgrade:mock-voting -# cp $NETWORK_STATE_FILE deployed-mainnet.json -# yarn hardhat --network custom run --no-compile scripts/utils/mine.ts -yarn test:integration From b8ec4f452aa4bcf99af936f177f19093fe825a3f Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 5 Aug 2025 15:13:14 +0200 Subject: [PATCH 377/405] refactor: remove unused overrides from Hardhat config and console log from deploy function --- hardhat.config.ts | 13 ------------- lib/deploy.ts | 1 - 2 files changed, 14 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 0f3e127cb1..9e5d105c59 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -162,19 +162,6 @@ const config: HardhatUserConfig = { }, }, ], - overrides: { - "contracts/upgrade/TWVoteScript.sol": { - version: "0.8.25", - settings: { - viaIR: true, - optimizer: { - enabled: true, - runs: 200, - }, - evmVersion: "cancun", - }, - }, - }, }, tracer: { tasks: ["watch"], diff --git a/lib/deploy.ts b/lib/deploy.ts index cab7d1c96f..1d2d5b04af 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -120,7 +120,6 @@ export async function deployWithoutProxy( if (withStateFile) { const contractPath = await getContractPath(artifactName); - console.log(`Contract path: ${contractPath}`, nameInState); updateObjectInState(nameInState, { contract: contractPath, [addressFieldName]: contract.address, From 62237e5a94570754aad9f1b613cf7cf9debba6d7 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 10:32:15 +0200 Subject: [PATCH 378/405] feat: add IOssifiableProxy interface and update getAddress function to use state parameter --- contracts/common/interfaces/IOssifiableProxy.sol | 12 ++++++++++++ contracts/upgrade/TWVoteScript.sol | 10 ++-------- lib/state-file.ts | 6 ++---- 3 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 contracts/common/interfaces/IOssifiableProxy.sol diff --git a/contracts/common/interfaces/IOssifiableProxy.sol b/contracts/common/interfaces/IOssifiableProxy.sol new file mode 100644 index 0000000000..7eb3898388 --- /dev/null +++ b/contracts/common/interfaces/IOssifiableProxy.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 + +// See contracts/COMPILERS.md +// solhint-disable-next-line lido/fixed-compiler-version +pragma solidity >=0.4.24; + +interface IOssifiableProxy { + function proxy__upgradeTo(address newImplementation) external; + function proxy__changeAdmin(address newAdmin) external; + function proxy__getAdmin() external view returns (address); + function proxy__getImplementation() external view returns (address); +} diff --git a/contracts/upgrade/TWVoteScript.sol b/contracts/upgrade/TWVoteScript.sol index 0726d7267a..9479584dfb 100644 --- a/contracts/upgrade/TWVoteScript.sol +++ b/contracts/upgrade/TWVoteScript.sol @@ -3,13 +3,7 @@ pragma solidity 0.8.25; import {IAccessControl} from "@openzeppelin/contracts-v5.2/access/IAccessControl.sol"; import {OmnibusBase} from "./utils/OmnibusBase.sol"; - -interface IOssifiableProxy { - function proxy__upgradeTo(address newImplementation_) external; - function proxy__changeAdmin(address newAdmin_) external; - function proxy__getAdmin() external view returns (address); - function proxy__getImplementation() external view returns (address); -} +import {IOssifiableProxy} from "contracts/common/interfaces/IOssifiableProxy.sol"; interface IRepo { function newVersion(uint16[3] calldata _newSemanticVersion, address _contractAddress, bytes calldata _contentURI) external; @@ -375,4 +369,4 @@ contract TWVoteScript is OmnibusBase { params.node_operators_registry_app_id ); } -} \ No newline at end of file +} diff --git a/lib/state-file.ts b/lib/state-file.ts index 54282a9c27..b19715fe07 100644 --- a/lib/state-file.ts +++ b/lib/state-file.ts @@ -98,7 +98,7 @@ export enum Sk { dgEmergencyProtectedTimelock = "dg:emergencyProtectedTimelock", } -export function getAddress(contractKey: Sk): string { +export function getAddress(contractKey: Sk, state: DeploymentState): string { switch (contractKey) { case Sk.accountingOracle: case Sk.appAgent: @@ -117,6 +117,7 @@ export function getAddress(contractKey: Sk): string { case Sk.validatorsExitBusOracle: case Sk.withdrawalQueueERC721: case Sk.withdrawalVault: + return state[contractKey].proxy.address; case Sk.burner: case Sk.appSimpleDvt: case Sk.aragonNodeOperatorsRegistryAppRepo: @@ -214,10 +215,7 @@ export async function resetStateFile(networkName: string = hardhatNetwork.name): if ((error as NodeJS.ErrnoException).code !== "ENOENT") { throw new Error(`No network state file ${fileName}: ${(error as Error).message}`); } - // If file does not exist, create it with default values } finally { - // const templateFileName = _getFileName("scripts/defaults", "testnet-defaults", ""); - const templateData = readFileSync("scripts/scratch/deployed-testnet-defaults.json", "utf8"); writeFileSync(fileName, templateData, { encoding: "utf8", flag: "w" }); } From 84063385a0239dc94163e0f8ef554dca78548ca0 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 13:54:13 +0200 Subject: [PATCH 379/405] fix: update import path for IStakingModule in ICSModule interface --- contracts/common/interfaces/ICSModule.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/common/interfaces/ICSModule.sol b/contracts/common/interfaces/ICSModule.sol index 6c3a2f5c00..911b1c973d 100644 --- a/contracts/common/interfaces/ICSModule.sol +++ b/contracts/common/interfaces/ICSModule.sol @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 // For full version see: https://github.com/lidofinance/community-staking-module/blob/develop/src/interfaces/ICSModule.sol -import { IStakingModule } from "contracts/0.8.9/interfaces/IStakingModule.sol"; +import { IStakingModule } from "contracts/common/interfaces/IStakingModule.sol"; pragma solidity 0.8.9; @@ -94,4 +94,4 @@ interface ICSModule is IStakingModule ) external; function depositETH(uint256 nodeOperatorId) external payable; -} \ No newline at end of file +} From bcf57edc34ccecb8a1f63adb171da07882ab556e Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 14:18:07 +0200 Subject: [PATCH 380/405] fix: remove stuckKeys from extra data preparation in submitMainReport function --- .../core/accounting-oracle-extra-data.integration.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/integration/core/accounting-oracle-extra-data.integration.ts b/test/integration/core/accounting-oracle-extra-data.integration.ts index 8d386d495f..69766b955b 100644 --- a/test/integration/core/accounting-oracle-extra-data.integration.ts +++ b/test/integration/core/accounting-oracle-extra-data.integration.ts @@ -107,10 +107,7 @@ describe("Integration: AccountingOracle extra data", () => { async function submitMainReport() { const { nor } = ctx.contracts; const extraData = prepareExtraData( - { - stuckKeys: [stuckKeys], - exitedKeys: [exitedKeys], - }, + { exitedKeys: [exitedKeys] }, { maxItemsPerChunk: 1 }, ); @@ -131,7 +128,6 @@ describe("Integration: AccountingOracle extra data", () => { const { totalExitedValidators } = await nor.getStakingModuleSummary(); const { extraDataItemsCount, extraDataChunks, extraDataChunkHashes } = prepareExtraData({ - stuckKeys: [stuckKeys], exitedKeys: [exitedKeys], }); expect(extraDataChunks.length).to.equal(1); From 3d5e8497c5dadad971f70ae59900f3aba48ad837 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 15:01:46 +0200 Subject: [PATCH 381/405] fix: remove stuckKeys handling and update exitedKeys logic in accounting oracle tests --- ...ccounting-oracle-extra-data.integration.ts | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/test/integration/core/accounting-oracle-extra-data.integration.ts b/test/integration/core/accounting-oracle-extra-data.integration.ts index 69766b955b..872d4e3713 100644 --- a/test/integration/core/accounting-oracle-extra-data.integration.ts +++ b/test/integration/core/accounting-oracle-extra-data.integration.ts @@ -28,7 +28,6 @@ describe("Integration: AccountingOracle extra data", () => { let snapshot: string; let originalState: string; - let stuckKeys: KeyType; let exitedKeys: KeyType; before(async () => { @@ -63,27 +62,18 @@ describe("Integration: AccountingOracle extra data", () => { } const numNodeOperators = Math.min(10, Number(await ctx.contracts.nor.getNodeOperatorsCount())); - const numStuckKeys = 2; - stuckKeys = { - moduleId: Number(MODULE_ID), - nodeOpIds: [], - keysCounts: [], - }; exitedKeys = { moduleId: Number(MODULE_ID), nodeOpIds: [], keysCounts: [], }; - for (let i = firstNodeOperatorInRange; i < firstNodeOperatorInRange + numNodeOperators; i++) { + + // Add at least 2 node operators with exited validators to test chunking + for (let i = firstNodeOperatorInRange; i < firstNodeOperatorInRange + Math.min(2, numNodeOperators); i++) { const oldNumExited = await getExitedCount(BigInt(i)); - const numExited = oldNumExited + (i === firstNodeOperatorInRange ? NUM_NEWLY_EXITED_VALIDATORS : 0n); - if (numExited !== oldNumExited) { - exitedKeys.nodeOpIds.push(Number(i)); - exitedKeys.keysCounts.push(Number(numExited)); - } else { - stuckKeys.nodeOpIds.push(Number(i)); - stuckKeys.keysCounts.push(numStuckKeys); - } + const numExited = oldNumExited + (i === firstNodeOperatorInRange ? NUM_NEWLY_EXITED_VALIDATORS : 1n); + exitedKeys.nodeOpIds.push(Number(i)); + exitedKeys.keysCounts.push(Number(numExited)); } } }); @@ -106,16 +96,33 @@ describe("Integration: AccountingOracle extra data", () => { async function submitMainReport() { const { nor } = ctx.contracts; + + // Split exitedKeys into two separate entries for different node operators to test chunking + const firstExitedKeys = { + moduleId: Number(MODULE_ID), + nodeOpIds: exitedKeys.nodeOpIds.length > 0 ? [exitedKeys.nodeOpIds[0]] : [], + keysCounts: exitedKeys.keysCounts.length > 0 ? [exitedKeys.keysCounts[0]] : [], + }; + + const secondExitedKeys = { + moduleId: Number(MODULE_ID), + nodeOpIds: exitedKeys.nodeOpIds.length > 1 ? [exitedKeys.nodeOpIds[1]] : [], + keysCounts: exitedKeys.keysCounts.length > 1 ? [exitedKeys.keysCounts[1]] : [], + }; + const extraData = prepareExtraData( - { exitedKeys: [exitedKeys] }, - { maxItemsPerChunk: 1 }, + { exitedKeys: [firstExitedKeys, secondExitedKeys] }, + { maxItemsPerChunk: 1 } // This will create 2 chunks from 2 items ); const { totalExitedValidators } = await nor.getStakingModuleSummary(); + + // Add total exited validators for both entries + const totalNewExited = NUM_NEWLY_EXITED_VALIDATORS + 1n; // First operator has 1, second has 1 return await reportWithoutExtraData( ctx, - [totalExitedValidators + NUM_NEWLY_EXITED_VALIDATORS], + [totalExitedValidators + totalNewExited], [NOR_MODULE_ID], extraData, ); @@ -126,9 +133,10 @@ describe("Integration: AccountingOracle extra data", () => { // Get initial summary const { totalExitedValidators } = await nor.getStakingModuleSummary(); - + + // Use both node operators with exited keys for a single chunk test const { extraDataItemsCount, extraDataChunks, extraDataChunkHashes } = prepareExtraData({ - exitedKeys: [exitedKeys], + exitedKeys: [exitedKeys], // Use all exitedKeys in one chunk }); expect(extraDataChunks.length).to.equal(1); expect(extraDataChunkHashes.length).to.equal(1); @@ -140,7 +148,7 @@ describe("Integration: AccountingOracle extra data", () => { extraDataHash: extraDataChunkHashes[0], extraDataItemsCount: BigInt(extraDataItemsCount), extraDataList: hexToBytes(extraDataChunks[0]), - numExitedValidatorsByStakingModule: [totalExitedValidators + NUM_NEWLY_EXITED_VALIDATORS], + numExitedValidatorsByStakingModule: [totalExitedValidators + NUM_NEWLY_EXITED_VALIDATORS + 1n], // Both operators stakingModuleIdsWithNewlyExitedValidators: [NOR_MODULE_ID], }; @@ -168,7 +176,7 @@ describe("Integration: AccountingOracle extra data", () => { expect(extraDataSubmittedEvents[0].args.itemsCount).to.equal(extraDataItemsCount); expect((await nor.getStakingModuleSummary()).totalExitedValidators).to.equal( - numExitedBefore + NUM_NEWLY_EXITED_VALIDATORS, + numExitedBefore + NUM_NEWLY_EXITED_VALIDATORS + 1n, // Both operators ); }); From 429ef9e1500069c7447f845fa73c0f003e0b8230 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 15:26:02 +0200 Subject: [PATCH 382/405] fix: remove stuck items reporting and related logic in accounting oracle tests --- ...racle-extra-data-full-items.integration.ts | 114 ++---------------- 1 file changed, 8 insertions(+), 106 deletions(-) diff --git a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts index ec2bffad89..03d410408a 100644 --- a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts +++ b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts @@ -5,14 +5,12 @@ import { ethers } from "hardhat"; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; -import { Burner__factory, IStakingModule, NodeOperatorsRegistry } from "typechain-types"; +import { NodeOperatorsRegistry } from "typechain-types"; import { advanceChainTime, ether, EXTRA_DATA_TYPE_EXITED_VALIDATORS, - EXTRA_DATA_TYPE_STUCK_VALIDATORS, - findEventsWithInterfaces, ItemType, LoadedContract, log, @@ -24,12 +22,7 @@ import { getProtocolContext, ProtocolContext, withCSM } from "lib/protocol"; import { reportWithoutExtraData } from "lib/protocol/helpers/accounting"; import { norSdvtEnsureOperators } from "lib/protocol/helpers/nor-sdvt"; import { removeStakingLimit, setModuleStakeShareLimit } from "lib/protocol/helpers/staking"; -import { - calcNodeOperatorRewards, - CSM_MODULE_ID, - NOR_MODULE_ID, - SDVT_MODULE_ID, -} from "lib/protocol/helpers/staking-module"; +import { CSM_MODULE_ID, NOR_MODULE_ID, SDVT_MODULE_ID } from "lib/protocol/helpers/staking-module"; import { Snapshot } from "test/suite"; @@ -141,11 +134,8 @@ describe("Integration: AccountingOracle extra data full items", () => { } function testReportingModuleWithMaxExtraDataItems({ - norStuckItems, norExitedItems, - sdvtStuckItems, sdvtExitedItems, - csmStuckItems, csmExitedItems, }: { norStuckItems: number; @@ -194,37 +184,19 @@ describe("Integration: AccountingOracle extra data full items", () => { // Slice arrays based on item counts const idsExited = new Map(); - const idsStuck = new Map(); idsExited.set(NOR_MODULE_ID, norIds.slice(0, norExitedItems * maxNodeOperatorsPerExtraDataItem)); - idsStuck.set(NOR_MODULE_ID, norIds.slice(0, norStuckItems * maxNodeOperatorsPerExtraDataItem)); - idsExited.set(SDVT_MODULE_ID, sdvtIds.slice(0, sdvtExitedItems * maxNodeOperatorsPerExtraDataItem)); - idsStuck.set(SDVT_MODULE_ID, sdvtIds.slice(0, sdvtStuckItems * maxNodeOperatorsPerExtraDataItem)); if (ctx.flags.withCSM) { idsExited.set(CSM_MODULE_ID, csmIds.slice(0, csmExitedItems * maxNodeOperatorsPerExtraDataItem)); - idsStuck.set(CSM_MODULE_ID, csmIds.slice(0, csmStuckItems * maxNodeOperatorsPerExtraDataItem)); } const numKeysReportedByNo = new ListKeyMapHelper(); // [moduleId, nodeOpId, type] -> numKeys const reportExtraItems: ItemType[] = []; - for (const { moduleId, module } of modules) { - const ids = idsStuck.get(moduleId)!; - for (const id of ids) { - const summary = await module.getNodeOperatorSummary(id); - const numKeys = summary.stuckValidatorsCount + 1n; - numKeysReportedByNo.set([moduleId, id, EXTRA_DATA_TYPE_STUCK_VALIDATORS], numKeys); - reportExtraItems.push({ - moduleId: Number(moduleId), - nodeOpIds: [Number(id)], - keysCounts: [Number(numKeys)], - type: EXTRA_DATA_TYPE_STUCK_VALIDATORS, - }); - } - } + // Stuck keys reporting removed upon Triggerable Withdrawals upgrade for (const { moduleId, module } of modules) { const ids = idsExited.get(moduleId)!; @@ -271,20 +243,7 @@ describe("Integration: AccountingOracle extra data full items", () => { ); } - // Store initial share balances for node operators with stuck validators - const sharesBefore = new ListKeyMapHelper(); - for (const { moduleId, module } of modules) { - if (moduleId === CSM_MODULE_ID) continue; - - const ids = idsStuck.get(moduleId)!; - for (const id of ids) { - const nodeOperator = await (module as unknown as LoadedContract).getNodeOperator( - id, - false, - ); - sharesBefore.set([moduleId, id], await ctx.contracts.lido.sharesOf(nodeOperator.rewardAddress)); - } - } + // Share balances tracking removed upon Triggerable Withdrawals upgrade const { reportTx, submitter, extraDataChunks } = await reportWithoutExtraData( ctx, @@ -322,70 +281,12 @@ describe("Integration: AccountingOracle extra data full items", () => { expect(summary.totalExitedValidators).to.equal(numExpectedExited); } - // Check module stuck validators, penalties and rewards - const moduleIdsStuck = idsStuck.get(moduleId)!; - for (const opId of moduleIdsStuck) { - // Verify stuck validators count matches expected - const operatorSummary = await module.getNodeOperatorSummary(opId); - const numExpectedStuck = numKeysReportedByNo.get([moduleId, opId, EXTRA_DATA_TYPE_STUCK_VALIDATORS]); - expect(operatorSummary.stuckValidatorsCount).to.equal(numExpectedStuck); - } + // Stuck validators check removed upon Triggerable Withdrawals upgrade if (moduleId === CSM_MODULE_ID) { continue; } - const moduleNor = module as unknown as LoadedContract; - - if (moduleIdsStuck.length > 0) { - // Find the TransferShares event for module rewards - const receipt = await reportTx.wait(); - const transferSharesEvents = await findEventsWithInterfaces(receipt!, "TransferShares", [ - ctx.contracts.lido.interface, - ]); - const moduleRewardsEvent = transferSharesEvents.find((e) => e.args.to === module.address); - const moduleRewards = moduleRewardsEvent ? moduleRewardsEvent.args.sharesValue : 0n; - - let modulePenaltyShares = 0n; - - // Check each stuck node operator - for (const opId of moduleIdsStuck) { - // Verify operator is penalized - expect(await moduleNor.isOperatorPenalized(opId)).to.be.true; - - // Get operator reward address and current shares balance - const operator = await moduleNor.getNodeOperator(opId, false); - const sharesAfter = await ctx.contracts.lido.sharesOf(operator.rewardAddress); - - // Calculate expected rewards - const rewardsAfter = await calcNodeOperatorRewards( - moduleNor as unknown as LoadedContract, - opId, - moduleRewards, - ); - - // Verify operator received only half the rewards (due to penalty) - const sharesDiff = sharesAfter - sharesBefore.get([moduleId, opId])!; - const expectedReward = rewardsAfter / 2n; - - // Allow for small rounding differences (up to 2 wei) - expect(sharesDiff).to.be.closeTo(expectedReward, 2n); - - // Track total penalty shares - modulePenaltyShares += expectedReward; - } - - // Check if penalty shares were burned - if (modulePenaltyShares > 0n) { - const distributeReceipt = await distributeTxReceipts[String(moduleId)]; - const burnEvents = await findEventsWithInterfaces(distributeReceipt!, "StETHBurnRequested", [ - Burner__factory.createInterface(), - ]); - const totalBurnedShares = burnEvents.reduce((sum, event) => sum + event.args.amountOfShares, 0n); - - // Verify that the burned shares match the penalty shares (with small tolerance for rounding) - expect(totalBurnedShares).to.be.closeTo(modulePenaltyShares, 100n); - } - } + // Penalty check removed upon Triggerable Withdrawals upgrade } }; } @@ -396,7 +297,8 @@ describe("Integration: AccountingOracle extra data full items", () => { for (const norExitedItems of [0, 1]) { for (const sdvtStuckItems of [0]) { for (const sdvtExitedItems of [0, 1]) { - for (const csmStuckItems of withCSM() ? [0, 1] : [0]) { + for (const csmStuckItems of [0]) { + // stuck items reporting is removed upon Triggerable Withdrawals upgrade for (const csmExitedItems of withCSM() ? [0, 1] : [0]) { if ( norStuckItems + norExitedItems + sdvtStuckItems + sdvtExitedItems + csmStuckItems + csmExitedItems === From 85c027df599670ce17be39520c19dde414ded8e0 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 16:00:14 +0200 Subject: [PATCH 383/405] fix: remove unused reportTx variable in accounting oracle integration test --- .../core/accounting-oracle-extra-data-full-items.integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts index 03d410408a..8ce4a77167 100644 --- a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts +++ b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts @@ -245,7 +245,7 @@ describe("Integration: AccountingOracle extra data full items", () => { // Share balances tracking removed upon Triggerable Withdrawals upgrade - const { reportTx, submitter, extraDataChunks } = await reportWithoutExtraData( + const { submitter, extraDataChunks } = await reportWithoutExtraData( ctx, numExitedValidatorsByStakingModule, modulesWithExited, From 8635059934e3faa2eb417889e2e9a5ad59b9d20b Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 16:56:59 +0200 Subject: [PATCH 384/405] fix: add gas estimation and logging for transaction and deployment processes --- lib/deploy.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/deploy.ts b/lib/deploy.ts index 1d2d5b04af..60403a4ffa 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -35,10 +35,15 @@ export async function makeTx( ): Promise { log.withArguments(`Call: ${yl(contract.name)}[${cy(contract.address)}].${yl(funcName)}`, args); + const estimatedGas = await contract.getFunction(funcName).estimateGas(...args, txParams); + log(`Gas estimate: ${cy(estimatedGas.toString())}`); + const tx = await contract.getFunction(funcName)(...args, txParams); + log(`Transaction sent: ${cy(tx.hash)}`); const receipt = await tx.wait(); const gasUsed = receipt.gasUsed; + log(`Gas used: ${cy(gasUsed.toString())}`); incrementGasUsed(gasUsed, withStateFile); return receipt; @@ -70,11 +75,17 @@ async function deployContractType2( ): Promise { const txParams = await getDeployTxParams(deployer); const factory = (await ethers.getContractFactory(artifactName, signerOrOptions)) as ContractFactory; + + const deployTx = await factory.getDeployTransaction(...constructorArgs, txParams); + const estimatedGas = await ethers.provider.estimateGas(deployTx); + log(`Deploy gas estimate: ${cy(estimatedGas.toString())}`); + const contract = await factory.deploy(...constructorArgs, txParams); const tx = contract.deploymentTransaction(); if (!tx) { throw new Error(`Failed to send the deployment transaction for ${artifactName}`); } + log(`Deployment transaction sent: ${cy(tx.hash)}`); const receipt = await tx.wait(); if (!receipt) { @@ -82,6 +93,7 @@ async function deployContractType2( } const gasUsed = receipt.gasUsed; + log(`Deployment gas used: ${cy(gasUsed.toString())}`); incrementGasUsed(gasUsed, withStateFile); (contract as DeployedContract).deploymentGasUsed = gasUsed; (contract as DeployedContract).deploymentTx = tx.hash; From e34246cf4f899744ff4d4b7887c084bbf6c9ca43 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 7 Aug 2025 16:57:41 +0200 Subject: [PATCH 385/405] Revert "fix: add gas estimation and logging for transaction and deployment processes" This reverts commit 8635059934e3faa2eb417889e2e9a5ad59b9d20b. --- lib/deploy.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/deploy.ts b/lib/deploy.ts index 60403a4ffa..1d2d5b04af 100644 --- a/lib/deploy.ts +++ b/lib/deploy.ts @@ -35,15 +35,10 @@ export async function makeTx( ): Promise { log.withArguments(`Call: ${yl(contract.name)}[${cy(contract.address)}].${yl(funcName)}`, args); - const estimatedGas = await contract.getFunction(funcName).estimateGas(...args, txParams); - log(`Gas estimate: ${cy(estimatedGas.toString())}`); - const tx = await contract.getFunction(funcName)(...args, txParams); - log(`Transaction sent: ${cy(tx.hash)}`); const receipt = await tx.wait(); const gasUsed = receipt.gasUsed; - log(`Gas used: ${cy(gasUsed.toString())}`); incrementGasUsed(gasUsed, withStateFile); return receipt; @@ -75,17 +70,11 @@ async function deployContractType2( ): Promise { const txParams = await getDeployTxParams(deployer); const factory = (await ethers.getContractFactory(artifactName, signerOrOptions)) as ContractFactory; - - const deployTx = await factory.getDeployTransaction(...constructorArgs, txParams); - const estimatedGas = await ethers.provider.estimateGas(deployTx); - log(`Deploy gas estimate: ${cy(estimatedGas.toString())}`); - const contract = await factory.deploy(...constructorArgs, txParams); const tx = contract.deploymentTransaction(); if (!tx) { throw new Error(`Failed to send the deployment transaction for ${artifactName}`); } - log(`Deployment transaction sent: ${cy(tx.hash)}`); const receipt = await tx.wait(); if (!receipt) { @@ -93,7 +82,6 @@ async function deployContractType2( } const gasUsed = receipt.gasUsed; - log(`Deployment gas used: ${cy(gasUsed.toString())}`); incrementGasUsed(gasUsed, withStateFile); (contract as DeployedContract).deploymentGasUsed = gasUsed; (contract as DeployedContract).deploymentTx = tx.hash; From b9d422297bca1b5d7fdb617ce3a76073088a9121 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 8 Aug 2025 12:20:49 +0200 Subject: [PATCH 386/405] fix: remove stuck items reporting and related logic in accounting oracle integration tests --- ...racle-extra-data-full-items.integration.ts | 61 ++++++------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts index 8ce4a77167..dd72d06490 100644 --- a/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts +++ b/test/integration/core/accounting-oracle-extra-data-full-items.integration.ts @@ -22,7 +22,11 @@ import { getProtocolContext, ProtocolContext, withCSM } from "lib/protocol"; import { reportWithoutExtraData } from "lib/protocol/helpers/accounting"; import { norSdvtEnsureOperators } from "lib/protocol/helpers/nor-sdvt"; import { removeStakingLimit, setModuleStakeShareLimit } from "lib/protocol/helpers/staking"; -import { CSM_MODULE_ID, NOR_MODULE_ID, SDVT_MODULE_ID } from "lib/protocol/helpers/staking-module"; +import { + CSM_MODULE_ID, + NOR_MODULE_ID, + SDVT_MODULE_ID, +} from "lib/protocol/helpers/staking-module"; import { Snapshot } from "test/suite"; @@ -138,11 +142,8 @@ describe("Integration: AccountingOracle extra data full items", () => { sdvtExitedItems, csmExitedItems, }: { - norStuckItems: number; norExitedItems: number; - sdvtStuckItems: number; sdvtExitedItems: number; - csmStuckItems: number; csmExitedItems: number; }) { return async () => { @@ -196,8 +197,6 @@ describe("Integration: AccountingOracle extra data full items", () => { const reportExtraItems: ItemType[] = []; - // Stuck keys reporting removed upon Triggerable Withdrawals upgrade - for (const { moduleId, module } of modules) { const ids = idsExited.get(moduleId)!; for (const id of ids) { @@ -243,8 +242,6 @@ describe("Integration: AccountingOracle extra data full items", () => { ); } - // Share balances tracking removed upon Triggerable Withdrawals upgrade - const { submitter, extraDataChunks } = await reportWithoutExtraData( ctx, numExitedValidatorsByStakingModule, @@ -280,46 +277,24 @@ describe("Integration: AccountingOracle extra data full items", () => { const numExpectedExited = numKeysReportedByNo.get([moduleId, id, EXTRA_DATA_TYPE_EXITED_VALIDATORS]); expect(summary.totalExitedValidators).to.equal(numExpectedExited); } - - // Stuck validators check removed upon Triggerable Withdrawals upgrade - - if (moduleId === CSM_MODULE_ID) { - continue; - } - // Penalty check removed upon Triggerable Withdrawals upgrade } }; } - // TODO: stuck items reporting test is not working, but maybe there is no need to fix it - // because stuck items reporting is removed upon Triggerable Withdrawals upgrade - for (const norStuckItems of [0]) { - for (const norExitedItems of [0, 1]) { - for (const sdvtStuckItems of [0]) { - for (const sdvtExitedItems of [0, 1]) { - for (const csmStuckItems of [0]) { - // stuck items reporting is removed upon Triggerable Withdrawals upgrade - for (const csmExitedItems of withCSM() ? [0, 1] : [0]) { - if ( - norStuckItems + norExitedItems + sdvtStuckItems + sdvtExitedItems + csmStuckItems + csmExitedItems === - 0 - ) { - continue; - } - it( - `should process extra data with full items for all modules with norStuckItems=${norStuckItems}, norExitedItems=${norExitedItems}, sdvtStuckItems=${sdvtStuckItems}, sdvtExitedItems=${sdvtExitedItems}, csmStuckItems=${csmStuckItems}, csmExitedItems=${csmExitedItems}`, - testReportingModuleWithMaxExtraDataItems({ - norStuckItems, - norExitedItems, - sdvtStuckItems, - sdvtExitedItems, - csmStuckItems, - csmExitedItems, - }), - ); - } - } + for (const norExitedItems of [0, 1]) { + for (const sdvtExitedItems of [0, 1]) { + for (const csmExitedItems of withCSM() ? [0, 1] : [0]) { + if (norExitedItems + sdvtExitedItems + csmExitedItems === 0) { + continue; } + it( + `should process extra data with full items for all modules with norExitedItems=${norExitedItems}, sdvtExitedItems=${sdvtExitedItems}, csmExitedItems=${csmExitedItems}`, + testReportingModuleWithMaxExtraDataItems({ + norExitedItems, + sdvtExitedItems, + csmExitedItems, + }), + ); } } } From f531dde249b88173726469ecee91754d3d0cc538 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 8 Aug 2025 14:07:31 +0200 Subject: [PATCH 387/405] fix: update network configuration for hoodi and redeploy locator + tw verifier --- deployed-hoodi.json | 8 ++++---- hardhat.config.ts | 36 +++++++++++++++++++++++++++++++----- scripts/tw-deploy.sh | 4 ++-- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index d033e9efa5..10faa1f3e2 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -498,7 +498,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x003f20CD17e7683A7F88A7AfF004f0C44F0cfB31", + "address": "0xA656983a6686615850BE018b7d42a7C3E46DcD71", "constructorArgs": [ [ "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", @@ -515,7 +515,7 @@ "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", "0x2a833402e3F46fFC1ecAb3598c599147a78731a9", - "0xFd4386A8795956f4B6D01cbb6dB116749731D7bD", + "0x7990A2F4E16E3c0D651306D26084718DB5aC9947", "0x6679090D92b08a2a686eF8614feECD8cDFE209db" ] ] @@ -608,7 +608,7 @@ ] ] }, - "scratchDeployGasUsed": "173935386", + "scratchDeployGasUsed": "175831654", "simpleDvt": { "deployParameters": { "stakingModuleTypeId": "curated-onchain-v1", @@ -649,7 +649,7 @@ "validatorExitDelayVerifier": { "implementation": { "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", - "address": "0xFd4386A8795956f4B6D01cbb6dB116749731D7bD", + "address": "0x7990A2F4E16E3c0D651306D26084718DB5aC9947", "constructorArgs": [ "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", { diff --git a/hardhat.config.ts b/hardhat.config.ts index 9e5d105c59..4b69806b3d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -91,12 +91,38 @@ const config: HardhatUserConfig = { browserURL: process.env.LOCAL_DEVNET_EXPLORER_URL ?? "", }, }, + { + network: "holesky", + chainId: 17000, + urls: { + apiURL: "https://api-holesky.etherscan.io/api", + browserURL: "https://holesky.etherscan.io/", + }, + }, + { + network: "sepolia", + chainId: 11155111, + urls: { + apiURL: "https://api-sepolia.etherscan.io/api", + browserURL: "https://sepolia.etherscan.io/", + }, + }, + { + network: "hoodi", + chainId: 560048, + urls: { + apiURL: "https://api-hoodi.etherscan.io/api", + browserURL: "https://hoodi.etherscan.io/", + }, + }, ], - apiKey: process.env.LOCAL_DEVNET_EXPLORER_API_URL - ? { - "local-devnet": "local-devnet", - } - : process.env.ETHERSCAN_API_KEY || "", + apiKey: { + mainnet: process.env.ETHERSCAN_API_KEY || "", + sepolia: process.env.ETHERSCAN_API_KEY || "", + holesky: process.env.ETHERSCAN_API_KEY || "", + hoodi: process.env.ETHERSCAN_API_KEY || "", + "local-devnet": process.env.LOCAL_DEVNET_EXPLORER_API_URL ? "local-devnet" : "", + }, }, solidity: { compilers: [ diff --git a/scripts/tw-deploy.sh b/scripts/tw-deploy.sh index ca03b76699..0f465dc6d1 100755 --- a/scripts/tw-deploy.sh +++ b/scripts/tw-deploy.sh @@ -2,7 +2,7 @@ set -e +u set -o pipefail -export NETWORK=${NETWORK:="holesky"} # if defined use the value set to default otherwise +export NETWORK=${NETWORK:="hoodi"} # if defined use the value set to default otherwise export RPC_URL=${RPC_URL:="http://127.0.0.1:8545"} # if defined use the value set to default otherwise # export WITHDRAWAL_QUEUE_BASE_URI="<< SET IF REQUIED >>" # export DSM_PREDEFINED_ADDRESS="<< SET IF REQUIED >>" @@ -11,7 +11,7 @@ export DEPLOYER=${DEPLOYER:="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} # fir export GAS_PRIORITY_FEE=1 export GAS_MAX_FEE=100 -export NETWORK_STATE_FILE="deployed-holesky.json" +export NETWORK_STATE_FILE="deployed-hoodi.json" # export NETWORK_STATE_DEFAULTS_FILE="scripts/scratch/deployed-testnet-defaults.json" From e36f72f0d8b0209e86a4536c30acf603d5305c4f Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:00:54 +0200 Subject: [PATCH 388/405] chore: adjust calculation of historical block root gI --- contracts/0.8.25/ValidatorExitDelayVerifier.sol | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/contracts/0.8.25/ValidatorExitDelayVerifier.sol b/contracts/0.8.25/ValidatorExitDelayVerifier.sol index 82f7a4bf26..9b43a42c12 100644 --- a/contracts/0.8.25/ValidatorExitDelayVerifier.sol +++ b/contracts/0.8.25/ValidatorExitDelayVerifier.sol @@ -138,6 +138,7 @@ contract ValidatorExitDelayVerifier { uint256 eligibleExitRequestTimestamp ); error InvalidCapellaSlot(); + error HistoricalSummaryDoesNotExist(); /** * @dev The previous and current forks can be essentially the same. @@ -392,9 +393,14 @@ contract ValidatorExitDelayVerifier { uint64 recentSlot, uint64 targetSlot ) internal view returns (GIndex gI) { - uint256 targetSlotShifted = targetSlot - CAPELLA_SLOT; - uint256 summaryIndex = targetSlotShifted / SLOTS_PER_HISTORICAL_ROOT; - uint256 rootIndex = targetSlot % SLOTS_PER_HISTORICAL_ROOT; + uint64 targetSlotShifted = targetSlot - CAPELLA_SLOT; + uint64 summaryIndex = targetSlotShifted / SLOTS_PER_HISTORICAL_ROOT; + uint64 rootIndex = targetSlot % SLOTS_PER_HISTORICAL_ROOT; + + uint64 summaryCreatedAtSlot = targetSlot - rootIndex + SLOTS_PER_HISTORICAL_ROOT; + if (summaryCreatedAtSlot > recentSlot) { + revert HistoricalSummaryDoesNotExist(); + } gI = recentSlot < PIVOT_SLOT ? GI_FIRST_HISTORICAL_SUMMARY_PREV @@ -402,7 +408,7 @@ contract ValidatorExitDelayVerifier { gI = gI.shr(summaryIndex); // historicalSummaries[summaryIndex] gI = gI.concat( - targetSlot < PIVOT_SLOT + summaryCreatedAtSlot < PIVOT_SLOT ? GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV : GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR ); // historicalSummaries[summaryIndex].blockRoots[0] From 55e32d55ffaa77ded82971b5f05756c96643603b Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 22 Aug 2025 20:20:20 +0200 Subject: [PATCH 389/405] test: port tests for getHistoricalBlockRootGI --- .../ValidatorExitDelayVerifier__Harness.sol | 38 +++++ .../0.8.25/validatorExitDelayVerifier.test.ts | 131 +++++++++++++++++- 2 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 test/0.8.25/contracts/ValidatorExitDelayVerifier__Harness.sol diff --git a/test/0.8.25/contracts/ValidatorExitDelayVerifier__Harness.sol b/test/0.8.25/contracts/ValidatorExitDelayVerifier__Harness.sol new file mode 100644 index 0000000000..474292135c --- /dev/null +++ b/test/0.8.25/contracts/ValidatorExitDelayVerifier__Harness.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED +// for testing purposes only + +pragma solidity 0.8.25; + +import {ValidatorExitDelayVerifier, GIndices, GIndex} from "contracts/0.8.25/ValidatorExitDelayVerifier.sol"; + +contract ValidatorExitDelayVerifier__Harness is ValidatorExitDelayVerifier { + constructor( + address lidoLocator, + GIndices memory gIndices, + uint64 firstSupportedSlot, + uint64 pivotSlot, + uint64 capellaSlot, + uint64 slotsPerHistoricalRoot, + uint32 slotsPerEpoch, + uint32 secondsPerSlot, + uint64 genesisTime, + uint32 shardCommitteePeriodInSeconds + ) + ValidatorExitDelayVerifier( + lidoLocator, + gIndices, + firstSupportedSlot, + pivotSlot, + capellaSlot, + slotsPerHistoricalRoot, + slotsPerEpoch, + secondsPerSlot, + genesisTime, + shardCommitteePeriodInSeconds + ) + {} + + function getHistoricalBlockRootGI(uint64 recentSlot, uint64 targetSlot) external returns (GIndex gI) { + return _getHistoricalBlockRootGI(recentSlot, targetSlot); + } +} diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index 29e690cd53..ef65acce5c 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -2,7 +2,12 @@ import { expect } from "chai"; import { ContractTransactionResponse } from "ethers"; import { ethers } from "hardhat"; -import { StakingRouter_Mock, ValidatorExitDelayVerifier, ValidatorsExitBusOracle_Mock } from "typechain-types"; +import { + StakingRouter_Mock, + ValidatorExitDelayVerifier, + ValidatorExitDelayVerifier__Harness, + ValidatorsExitBusOracle_Mock, +} from "typechain-types"; import { ILidoLocator } from "typechain-types/test/0.8.9/contracts/oracle/OracleReportSanityCheckerMocks.sol"; import { updateBeaconBlockRoot } from "lib"; @@ -234,7 +239,7 @@ describe("ValidatorExitDelayVerifier.sol", () => { const veboExitRequestTimestamp = GENESIS_TIME + (ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot - intervalInSlotsBetweenProvableBlockAndExitRequest) * - SECONDS_PER_SLOT; + SECONDS_PER_SLOT; const proofSlotTimestamp = GENESIS_TIME + ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot * SECONDS_PER_SLOT; const exitRequests: ExitRequest[] = [ @@ -739,3 +744,125 @@ describe("ValidatorExitDelayVerifier.sol", () => { }); }); }); + +describe("getHistoricalBlockRootGI", () => { + const FIRST_SUPPORTED_SLOT = 8192n; + const PIVOT_SLOT = 8192n * 13n; + const CAPELLA_SLOT = 8192n; + const SLOTS_PER_HISTORICAL_ROOT = 8192n; + const SLOTS_PER_EPOCH = 32n; + const SECONDS_PER_SLOT = 12n; + const GENESIS_TIME = 1606824000n; + const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192n; + const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; + + const GI_FIRST_HISTORICAL_SUMMARY_PREV = "0x0000000000000000000000000000000000000000000000000000007600000018"; + const GI_FIRST_HISTORICAL_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000b600000018"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; + const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000060000d"; + + // Validator GI values are irrelevant for this test, but the constructor requires them. + const GI_FIRST_VALIDATOR_PREV = "0x0000000000000000000000000000000000000000000000000096000000000028"; + const GI_FIRST_VALIDATOR_CURR = "0x0000000000000000000000000000000000000000000000000096000000000028"; + + let harness: ValidatorExitDelayVerifier__Harness; + + before(async () => { + harness = await ethers.deployContract("ValidatorExitDelayVerifier__Harness", [ + LIDO_LOCATOR, + { + gIFirstValidatorPrev: GI_FIRST_VALIDATOR_PREV, + gIFirstValidatorCurr: GI_FIRST_VALIDATOR_CURR, + gIFirstHistoricalSummaryPrev: GI_FIRST_HISTORICAL_SUMMARY_PREV, + gIFirstHistoricalSummaryCurr: GI_FIRST_HISTORICAL_SUMMARY_CURR, + gIFirstBlockRootInSummaryPrev: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV, + gIFirstBlockRootInSummaryCurr: GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR, + }, + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + CAPELLA_SLOT, + SLOTS_PER_HISTORICAL_ROOT, + SLOTS_PER_EPOCH, + SECONDS_PER_SLOT, + GENESIS_TIME, + SHARD_COMMITTEE_PERIOD_IN_SECONDS, + ]); + }); + + it("computes historical block root GI before pivot", async () => { + const recentSlot = PIVOT_SLOT - 1n; + + // historicalSummaries[0].blockRoots[0] + let gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 8192n); + expect(gI).to.equal(0x1d80000000000dn); + + // historicalSummaries[0].blockRoots[1] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 8193n); + expect(gI).to.equal(0x1d80000000010dn); + + // historicalSummaries[4].blockRoots[8082] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 49042n); + expect(gI).to.equal(0x1d8000011f920dn); + }); + + it("computes historical block root GI after pivot", async () => { + const recentSlot = PIVOT_SLOT + SLOTS_PER_HISTORICAL_ROOT; + + // historicalSummaries[0].blockRoots[0] + let gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 8192n); + expect(gI).to.equal(0x2d80000000000dn); + + // historicalSummaries[0].blockRoots[1] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 8193n); + expect(gI).to.equal(0x2d80000000010dn); + + // historicalSummaries[4].blockRoots[8082] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 49042n); + expect(gI).to.equal(0x2d8000011f920dn); + + // NOTE: targetSlot < PIVOT, but historicalSummary was built for slot >= PIVOT. + // historicalSummaries[11].blockRoots[2195] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, 100499n); + expect(gI).to.equal(0x2d800002e8930dn); + + // historicalSummaries[11].blockRoots[8191] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, PIVOT_SLOT - 1n); + expect(gI).to.equal(0x2d800002ffff0dn); + + // historicalSummaries[12].blockRoots[0] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, PIVOT_SLOT); + expect(gI).to.equal(0x2d80000320000dn); + + // historicalSummaries[X].blockRoots[1] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, PIVOT_SLOT + 1n); + expect(gI).to.equal(0x2d80000320010dn); + + // historicalSummaries[X].blockRoots[42] + gI = await harness.getHistoricalBlockRootGI.staticCall(recentSlot, PIVOT_SLOT + 42n); + expect(gI).to.equal(0x2d800003202a0dn); + }); + + it("reverts when the summary cannot exist", async () => { + const targetSlot = 8192n; + + await expect(harness.getHistoricalBlockRootGI(8192n, targetSlot)).to.be.revertedWithCustomError( + harness, + "HistoricalSummaryDoesNotExist", + ); + + await expect(harness.getHistoricalBlockRootGI(8193n, targetSlot)).to.be.revertedWithCustomError( + harness, + "HistoricalSummaryDoesNotExist", + ); + + await expect(harness.getHistoricalBlockRootGI(8192n + 8191n, targetSlot)).to.be.revertedWithCustomError( + harness, + "HistoricalSummaryDoesNotExist", + ); + + await expect(harness.getHistoricalBlockRootGI(8191n, targetSlot)).to.be.revertedWithCustomError( + harness, + "HistoricalSummaryDoesNotExist", + ); + }); +}); From 061e27d8f6c75026ba06941128759ceffb0123dd Mon Sep 17 00:00:00 2001 From: hweawer Date: Wed, 27 Aug 2025 14:31:07 +0200 Subject: [PATCH 390/405] feat(tw-deploy): gateseal state and network state var --- deployed-mainnet.json | 141 ++++++++++++++++++++++++------------------ scripts/tw-deploy.sh | 2 +- 2 files changed, 81 insertions(+), 62 deletions(-) diff --git a/deployed-mainnet.json b/deployed-mainnet.json index 08f6b59a20..f138801e99 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -15,7 +15,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x0e65898527E77210fB0133D00dd4C0E86Dc29bC7", + "address": "0x409466e099CC4F9b3929967a64036EBD257fCE30", "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", @@ -114,7 +114,7 @@ }, "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x1770044a38402e3CfCa2Fcfa0C84a093c9B42135", + "address": "0x063A1D1Cfc6354FB21dC6DE6fBe4B748cbD01941", "constructorArgs": [] }, "aragonApp": { @@ -190,9 +190,7 @@ "implementation": { "contract": "@aragon/os/contracts/kernel/Kernel.sol", "address": "0x2b33CF282f867A7FF693A66e11B0FcC5552e4425", - "constructorArgs": [ - true - ] + "constructorArgs": [true] }, "proxy": { "address": "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", @@ -262,6 +260,16 @@ "pauseIntentValidityPeriodBlocks": 6646 } }, + "dg:dualGovernance": { + "proxy": { + "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" + } + }, + "dg:emergencyProtectedTimelock": { + "proxy": { + "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" + } + }, "dummyEmptyContract": { "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", "contract": "contracts/0.8.9/utils/DummyEmptyContract.sol", @@ -272,9 +280,7 @@ "address": "0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7", "contract": "contracts/0.8.9/EIP712StETH.sol", "deployTx": "0xecb5010620fb13b0e2bbc98b8a0c82de0d7385491452cd36cf303cd74216ed91", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - ] + "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"] }, "ensAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", "executionLayerRewardsVault": { @@ -282,6 +288,20 @@ "contract": "contracts/0.8.9/LidoExecutionLayerRewardsVault.sol", "deployTx": "0xd72cf25e4a5fe3677b6f9b2ae13771e02ad66f8d2419f333bb8bde3147bd4294" }, + "gateSeal": { + "address": "0x8A854C4E750CDf24f138f34A9061b2f556066912", + "factoryAddress": "0x6c82877cac5a7a739f16ca0a89c0a328b8764a24", + "sealDuration": 1209600, + "expiryTimestamp": 1787816411, + "sealingCommittee": "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C" + }, + "gateSealTW": { + "factoryAddress": "0x6c82877cac5a7a739f16ca0a89c0a328b8764a24", + "sealDuration": 1209600, + "expiryTimestamp": 1787816411, + "sealingCommittee": "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C", + "address": "0xA6BC802fAa064414AA62117B4a53D27fFfF741F1" + }, "hashConsensusForAccountingOracle": { "address": "0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288", "contract": "contracts/0.8.9/oracle/HashConsensus.sol", @@ -338,7 +358,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x3ABc4764f0237923d52056CFba7E9AEBf87113D3", + "address": "0xf13B5b418fffd2a1D6aAe3bE750D411Fa3AfF10d", "constructorArgs": [ [ "0x852deD011285fe67063a08005c71a85690503Cee", @@ -354,7 +374,9 @@ "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", - "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09" + "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", + "0xA6b6bAe20a4A11CdeE202DbdE53e89E776f2dAC9", + "0x6d7EFb67236AaAeC2005ec704Bf5d755dd0703c4" ] ] } @@ -384,10 +406,7 @@ "address": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", "contract": "contracts/0.8.9/OracleDaemonConfig.sol", "deployTx": "0xa4f380b8806f5a504ef67fce62989e09be5a48bf114af63483c01c22f0c9a36f", - "constructorArgs": [ - "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", - [] - ], + "constructorArgs": ["0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", []], "deployParameters": { "NORMALIZED_CL_REWARD_PER_EPOCH": 64, "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000, @@ -407,20 +426,7 @@ "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - [ - 9000, - 43200, - 1000, - 50, - 600, - 8, - 24, - 7680, - 750000, - 1000, - 101, - 50 - ] + [9000, 43200, 1000, 50, 600, 8, 24, 7680, 750000, 1000, 101, 50] ], "deployParameters": { "churnValidatorsPerDayLimit": 20000, @@ -434,6 +440,7 @@ "maxPositiveTokenRebase": 750000 } }, + "scratchDeployGasUsed": "126395873", "stakingRouter": { "proxy": { "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", @@ -447,9 +454,45 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x89eDa99C0551d4320b56F82DDE8dF2f8D2eF81aA", + "address": "0xbd605Ad2010E12c16B0cd0F2B8FE3c6d90BB51E7", + "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] + } + }, + "triggerableWithdrawalsGateway": { + "implementation": { + "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", + "address": "0x6d7EFb67236AaAeC2005ec704Bf5d755dd0703c4", "constructorArgs": [ - "0x00000000219ab540356cBB839Cbe05303d7705Fa" + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + 11200, + 1, + 48 + ] + } + }, + "validatorExitDelayVerifier": { + "implementation": { + "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", + "address": "0xA6b6bAe20a4A11CdeE202DbdE53e89E776f2dAC9", + "constructorArgs": [ + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + { + "gIFirstValidatorPrev": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstValidatorCurr": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstHistoricalSummaryPrev": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstHistoricalSummaryCurr": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstBlockRootInSummaryPrev": "0x000000000000000000000000000000000000000000000000000000000040000d", + "gIFirstBlockRootInSummaryCurr": "0x000000000000000000000000000000000000000000000000000000000040000d" + }, + 1, + 1, + 0, + 8192, + 32, + 12, + 1606824023, + 98304 ] } }, @@ -465,17 +508,9 @@ ] }, "implementation": { - "address": "0xA89Ea51FddE660f67d1850e03C9c9862d33Bc42c", "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "deployTx": "0x5ab545276f78a72a432c3e971c96384973abfab6394e08cb077a006c25aef7a7", - "constructorArgs": [ - 12, - 1606824023, - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb" - ], - "deployParameters": { - "consensusVersion": 1 - } + "address": "0x2107c6AC639F21F27D38C1b048C825Ee42536690", + "constructorArgs": [12, 1606824023, "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"] } }, "vestingParams": { @@ -562,11 +597,7 @@ "address": "0xE42C659Dc09109566720EA8b2De186c2Be7D94D9", "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol", "deployTx": "0x6ab0151735c01acdef518421358d41a08752169bc383c57d57f5bfa135ac6eb1", - "constructorArgs": [ - "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - "Lido: stETH Withdrawal NFT", - "unstETH" - ], + "constructorArgs": ["0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "Lido: stETH Withdrawal NFT", "unstETH"], "deployParameters": { "name": "Lido: stETH Withdrawal NFT", "symbol": "unstETH" @@ -578,12 +609,12 @@ "address": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f" }, "implementation": { - "address": "0xCC52f17756C04bBa7E377716d7062fC36D7f69Fd", "contract": "contracts/0.8.9/WithdrawalVault.sol", - "deployTx": "0xd9eb2eca684770e4d2b192709b6071875f75072a0ce794a582824ee907a704f3", + "address": "0xc44C2b82dbEef6DdB195E0432Fa5e755C345D1e3", "constructorArgs": [ "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c" + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0x6d7EFb67236AaAeC2005ec704Bf5d755dd0703c4" ] } }, @@ -591,18 +622,6 @@ "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "contract": "contracts/0.6.12/WstETH.sol", "deployTx": "0xaf2c1a501d2b290ef1e84ddcfc7beb3406f8ece2c46dee14e212e8233654ff05", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - ] - }, - "dg:dualGovernance": { - "proxy": { - "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" - } - }, - "dg:emergencyProtectedTimelock": { - "proxy": { - "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" - } + "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"] } } diff --git a/scripts/tw-deploy.sh b/scripts/tw-deploy.sh index 0f465dc6d1..b22b864a22 100755 --- a/scripts/tw-deploy.sh +++ b/scripts/tw-deploy.sh @@ -11,7 +11,7 @@ export DEPLOYER=${DEPLOYER:="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} # fir export GAS_PRIORITY_FEE=1 export GAS_MAX_FEE=100 -export NETWORK_STATE_FILE="deployed-hoodi.json" +export NETWORK_STATE_FILE=${NETWORK_STATE_FILE:="deployed-hoodi.json"} # export NETWORK_STATE_DEFAULTS_FILE="scripts/scratch/deployed-testnet-defaults.json" From b5a5ecf115e7534810d95c15f2ff4e12c5a091d5 Mon Sep 17 00:00:00 2001 From: hweawer Date: Wed, 27 Aug 2025 14:34:39 +0200 Subject: [PATCH 391/405] Revert test deploy --- deployed-mainnet.json | 101 +++++++++++------------------------------- 1 file changed, 27 insertions(+), 74 deletions(-) diff --git a/deployed-mainnet.json b/deployed-mainnet.json index f138801e99..ecb53d61a2 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -15,7 +15,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x409466e099CC4F9b3929967a64036EBD257fCE30", + "address": "0x0e65898527E77210fB0133D00dd4C0E86Dc29bC7", "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", @@ -114,7 +114,7 @@ }, "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x063A1D1Cfc6354FB21dC6DE6fBe4B748cbD01941", + "address": "0x1770044a38402e3CfCa2Fcfa0C84a093c9B42135", "constructorArgs": [] }, "aragonApp": { @@ -260,16 +260,6 @@ "pauseIntentValidityPeriodBlocks": 6646 } }, - "dg:dualGovernance": { - "proxy": { - "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" - } - }, - "dg:emergencyProtectedTimelock": { - "proxy": { - "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" - } - }, "dummyEmptyContract": { "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", "contract": "contracts/0.8.9/utils/DummyEmptyContract.sol", @@ -289,19 +279,12 @@ "deployTx": "0xd72cf25e4a5fe3677b6f9b2ae13771e02ad66f8d2419f333bb8bde3147bd4294" }, "gateSeal": { - "address": "0x8A854C4E750CDf24f138f34A9061b2f556066912", + "address": "0xf9C9fDB4A5D2AA1D836D5370AB9b28BC1847e178", "factoryAddress": "0x6c82877cac5a7a739f16ca0a89c0a328b8764a24", - "sealDuration": 1209600, - "expiryTimestamp": 1787816411, + "sealDuration": 950400, + "expiryTimestamp": 1772323200, "sealingCommittee": "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C" }, - "gateSealTW": { - "factoryAddress": "0x6c82877cac5a7a739f16ca0a89c0a328b8764a24", - "sealDuration": 1209600, - "expiryTimestamp": 1787816411, - "sealingCommittee": "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C", - "address": "0xA6BC802fAa064414AA62117B4a53D27fFfF741F1" - }, "hashConsensusForAccountingOracle": { "address": "0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288", "contract": "contracts/0.8.9/oracle/HashConsensus.sol", @@ -358,7 +341,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0xf13B5b418fffd2a1D6aAe3bE750D411Fa3AfF10d", + "address": "0x3ABc4764f0237923d52056CFba7E9AEBf87113D3", "constructorArgs": [ [ "0x852deD011285fe67063a08005c71a85690503Cee", @@ -374,9 +357,7 @@ "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", - "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", - "0xA6b6bAe20a4A11CdeE202DbdE53e89E776f2dAC9", - "0x6d7EFb67236AaAeC2005ec704Bf5d755dd0703c4" + "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09" ] ] } @@ -440,7 +421,6 @@ "maxPositiveTokenRebase": 750000 } }, - "scratchDeployGasUsed": "126395873", "stakingRouter": { "proxy": { "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", @@ -454,48 +434,10 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0xbd605Ad2010E12c16B0cd0F2B8FE3c6d90BB51E7", + "address": "0x89eDa99C0551d4320b56F82DDE8dF2f8D2eF81aA", "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] } }, - "triggerableWithdrawalsGateway": { - "implementation": { - "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", - "address": "0x6d7EFb67236AaAeC2005ec704Bf5d755dd0703c4", - "constructorArgs": [ - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - 11200, - 1, - 48 - ] - } - }, - "validatorExitDelayVerifier": { - "implementation": { - "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", - "address": "0xA6b6bAe20a4A11CdeE202DbdE53e89E776f2dAC9", - "constructorArgs": [ - "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", - { - "gIFirstValidatorPrev": "0x0000000000000000000000000000000000000000000000000096000000000028", - "gIFirstValidatorCurr": "0x0000000000000000000000000000000000000000000000000096000000000028", - "gIFirstHistoricalSummaryPrev": "0x000000000000000000000000000000000000000000000000000000b600000018", - "gIFirstHistoricalSummaryCurr": "0x000000000000000000000000000000000000000000000000000000b600000018", - "gIFirstBlockRootInSummaryPrev": "0x000000000000000000000000000000000000000000000000000000000040000d", - "gIFirstBlockRootInSummaryCurr": "0x000000000000000000000000000000000000000000000000000000000040000d" - }, - 1, - 1, - 0, - 8192, - 32, - 12, - 1606824023, - 98304 - ] - } - }, "validatorsExitBusOracle": { "proxy": { "address": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", @@ -508,9 +450,13 @@ ] }, "implementation": { + "address": "0xA89Ea51FddE660f67d1850e03C9c9862d33Bc42c", "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "address": "0x2107c6AC639F21F27D38C1b048C825Ee42536690", - "constructorArgs": [12, 1606824023, "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"] + "deployTx": "0x5ab545276f78a72a432c3e971c96384973abfab6394e08cb077a006c25aef7a7", + "constructorArgs": [12, 1606824023, "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"], + "deployParameters": { + "consensusVersion": 1 + } } }, "vestingParams": { @@ -609,13 +555,10 @@ "address": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f" }, "implementation": { + "address": "0xCC52f17756C04bBa7E377716d7062fC36D7f69Fd", "contract": "contracts/0.8.9/WithdrawalVault.sol", - "address": "0xc44C2b82dbEef6DdB195E0432Fa5e755C345D1e3", - "constructorArgs": [ - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - "0x6d7EFb67236AaAeC2005ec704Bf5d755dd0703c4" - ] + "deployTx": "0xd9eb2eca684770e4d2b192709b6071875f75072a0ce794a582824ee907a704f3", + "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c"] } }, "wstETH": { @@ -623,5 +566,15 @@ "contract": "contracts/0.6.12/WstETH.sol", "deployTx": "0xaf2c1a501d2b290ef1e84ddcfc7beb3406f8ece2c46dee14e212e8233654ff05", "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"] + }, + "dg:dualGovernance": { + "proxy": { + "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" + } + }, + "dg:emergencyProtectedTimelock": { + "proxy": { + "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" + } } } From db598c148046c02f1240f9262f47fe368f91b2ba Mon Sep 17 00:00:00 2001 From: hweawer Date: Thu, 28 Aug 2025 14:04:22 +0200 Subject: [PATCH 392/405] fix(tw-deploy): Fix CAPELLA_SLOT for mainnet --- scripts/triggerable-withdrawals/tw-deploy.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 3a504da0bb..da5e1ad264 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -112,6 +112,9 @@ async function main(): Promise { const BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000000040000d"; const BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX = BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX; + const FIRST_SUPPORTED_SLOT = 1; + const PIVOT_SLOT = 1; + const CAPELLA_SLOT = 194048 * 32; // capellaSlot @see https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/capella/fork.md?plain=1#L22 const SLOTS_PER_HISTORICAL_ROOT = 8192; // TriggerableWithdrawalsGateway params @@ -205,9 +208,9 @@ async function main(): Promise { [ locator.address, gIndexes, - 1, // firstSupportedSlot - 1, // pivotSlot - 0, // capellaSlot @see https://github.com/eth-clients/hoodi/blob/main/metadata/config.yaml#L33 + FIRST_SUPPORTED_SLOT, + PIVOT_SLOT, + CAPELLA_SLOT, SLOTS_PER_HISTORICAL_ROOT, // slotsPerHistoricalRoot SLOTS_PER_EPOCH, SECONDS_PER_SLOT, From 9133ba548ca548bfa13cd648377c7f9975f71bca Mon Sep 17 00:00:00 2001 From: hweawer Date: Mon, 1 Sep 2025 10:46:37 +0200 Subject: [PATCH 393/405] fix(tw-deploy): Fix slots for verifier --- scripts/triggerable-withdrawals/tw-deploy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index da5e1ad264..40c99a7aad 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -112,8 +112,8 @@ async function main(): Promise { const BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX = "0x000000000000000000000000000000000000000000000000000000000040000d"; const BLOCK_ROOT_IN_SUMMARY_CURR_GINDEX = BLOCK_ROOT_IN_SUMMARY_PREV_GINDEX; - const FIRST_SUPPORTED_SLOT = 1; - const PIVOT_SLOT = 1; + const FIRST_SUPPORTED_SLOT = 364032 * SLOTS_PER_EPOCH; // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7600.md#activation + const PIVOT_SLOT = FIRST_SUPPORTED_SLOT; const CAPELLA_SLOT = 194048 * 32; // capellaSlot @see https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/capella/fork.md?plain=1#L22 const SLOTS_PER_HISTORICAL_ROOT = 8192; From 2feb83f3f66869e2aa0a44c36f4228808ae9f302 Mon Sep 17 00:00:00 2001 From: hweawer Date: Wed, 3 Sep 2025 13:23:23 +0200 Subject: [PATCH 394/405] Update hoodi state after TW redeploy --- deployed-hoodi.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deployed-hoodi.json b/deployed-hoodi.json index 10faa1f3e2..427eeb3f7f 100644 --- a/deployed-hoodi.json +++ b/deployed-hoodi.json @@ -498,7 +498,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0xA656983a6686615850BE018b7d42a7C3E46DcD71", + "address": "0x47975A61067a4CE41BeB730cf6c57378E55b849A", "constructorArgs": [ [ "0xcb883B1bD0a41512b42D2dB267F2A2cd919FB216", @@ -515,7 +515,7 @@ "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", "0x4473dCDDbf77679A643BdB654dbd86D67F8d32f2", "0x2a833402e3F46fFC1ecAb3598c599147a78731a9", - "0x7990A2F4E16E3c0D651306D26084718DB5aC9947", + "0xa5F5A9360275390fF9728262a29384399f38d2f0", "0x6679090D92b08a2a686eF8614feECD8cDFE209db" ] ] @@ -608,7 +608,7 @@ ] ] }, - "scratchDeployGasUsed": "175831654", + "scratchDeployGasUsed": "177752789", "simpleDvt": { "deployParameters": { "stakingModuleTypeId": "curated-onchain-v1", @@ -649,7 +649,7 @@ "validatorExitDelayVerifier": { "implementation": { "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", - "address": "0x7990A2F4E16E3c0D651306D26084718DB5aC9947", + "address": "0xa5F5A9360275390fF9728262a29384399f38d2f0", "constructorArgs": [ "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", { From 100a2a41611a42ca6e9189222241750bda7112e2 Mon Sep 17 00:00:00 2001 From: Maksim Kuraian Date: Wed, 10 Sep 2025 10:00:08 +0300 Subject: [PATCH 395/405] chore: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c06ba184c9..484b540b50 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Please refer to the [Lido Contributor Code of Conduct](/CODE_OF_CONDUCT.md). ## License -2024 Lido +2025 Lido This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From 3c5737aa270eb7e1f77ef395aeaf72bc59325246 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 12 Sep 2025 11:51:47 +0200 Subject: [PATCH 396/405] fix: update test script to run all unit tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e926066b9..500ebf07cc 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "lint:ts:fix": "yarn lint:ts --fix", "lint": "yarn lint:sol && yarn lint:ts", "format": "prettier . --write", - "test": "hardhat test test/0.8.9/lib/GIndex.test.ts --parallel", + "test": "hardhat test test/**/*.test.ts --parallel", "upgrade:deploy": "STEPS_FILE=upgrade/steps-deploy.json UPGRADE_PARAMETERS_FILE=upgrade-parameters-mainnet.json yarn hardhat --network custom run scripts/utils/migrate.ts", "upgrade:mock-voting": "STEPS_FILE=upgrade/steps-mock-voting.json UPGRADE_PARAMETERS_FILE=upgrade-parameters-mainnet.json yarn hardhat --network custom run scripts/utils/migrate.ts", "test:forge": "forge test", From 33b24753fe88897728f330f4c0b685ed61bf34a6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 12 Sep 2025 11:56:13 +0200 Subject: [PATCH 397/405] feat: add mainnet configuration to Hardhat config --- hardhat.config.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 4b69806b3d..909290dcff 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -48,7 +48,7 @@ const config: HardhatUserConfig = { }, "custom": { url: RPC_URL, - timeout: 120_000 + timeout: 120_000, }, "local": { url: process.env.LOCAL_RPC_URL || RPC_URL, @@ -80,6 +80,11 @@ const config: HardhatUserConfig = { url: process.env.SEPOLIA_RPC_URL || RPC_URL, chainId: 11155111, }, + "mainnet": { + url: RPC_URL, + chainId: 1, + accounts: loadAccounts("mainnet"), + }, }, etherscan: { customChains: [ @@ -117,10 +122,10 @@ const config: HardhatUserConfig = { }, ], apiKey: { - mainnet: process.env.ETHERSCAN_API_KEY || "", - sepolia: process.env.ETHERSCAN_API_KEY || "", - holesky: process.env.ETHERSCAN_API_KEY || "", - hoodi: process.env.ETHERSCAN_API_KEY || "", + "mainnet": process.env.ETHERSCAN_API_KEY || "", + "sepolia": process.env.ETHERSCAN_API_KEY || "", + "holesky": process.env.ETHERSCAN_API_KEY || "", + "hoodi": process.env.ETHERSCAN_API_KEY || "", "local-devnet": process.env.LOCAL_DEVNET_EXPLORER_API_URL ? "local-devnet" : "", }, }, From 3247a423b56e98f1aec4bf6f268d931f8da12985 Mon Sep 17 00:00:00 2001 From: F4ever Date: Fri, 12 Sep 2025 15:21:53 +0200 Subject: [PATCH 398/405] feat: tw deploy on mainnet --- deployed-mainnet.json | 147 ++++++++++++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 34 deletions(-) diff --git a/deployed-mainnet.json b/deployed-mainnet.json index ecb53d61a2..627868d0a2 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -15,7 +15,7 @@ }, "implementation": { "contract": "contracts/0.8.9/oracle/AccountingOracle.sol", - "address": "0x0e65898527E77210fB0133D00dd4C0E86Dc29bC7", + "address": "0xE9906E543274cebcd335d2C560094089e9547e8d", "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", @@ -114,7 +114,7 @@ }, "implementation": { "contract": "contracts/0.4.24/nos/NodeOperatorsRegistry.sol", - "address": "0x1770044a38402e3CfCa2Fcfa0C84a093c9B42135", + "address": "0x6828b023e737f96B168aCd0b5c6351971a4F81aE", "constructorArgs": [] }, "aragonApp": { @@ -190,7 +190,9 @@ "implementation": { "contract": "@aragon/os/contracts/kernel/Kernel.sol", "address": "0x2b33CF282f867A7FF693A66e11B0FcC5552e4425", - "constructorArgs": [true] + "constructorArgs": [ + true + ] }, "proxy": { "address": "0xb8FFC3Cd6e7Cf5a098A1c92F48009765B24088Dc", @@ -260,6 +262,16 @@ "pauseIntentValidityPeriodBlocks": 6646 } }, + "dg:dualGovernance": { + "proxy": { + "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" + } + }, + "dg:emergencyProtectedTimelock": { + "proxy": { + "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" + } + }, "dummyEmptyContract": { "address": "0x6F6541C2203196fEeDd14CD2C09550dA1CbEDa31", "contract": "contracts/0.8.9/utils/DummyEmptyContract.sol", @@ -270,7 +282,9 @@ "address": "0x8F73e4C2A6D852bb4ab2A45E6a9CF5715b3228B7", "contract": "contracts/0.8.9/EIP712StETH.sol", "deployTx": "0xecb5010620fb13b0e2bbc98b8a0c82de0d7385491452cd36cf303cd74216ed91", - "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"] + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + ] }, "ensAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", "executionLayerRewardsVault": { @@ -279,12 +293,19 @@ "deployTx": "0xd72cf25e4a5fe3677b6f9b2ae13771e02ad66f8d2419f333bb8bde3147bd4294" }, "gateSeal": { - "address": "0xf9C9fDB4A5D2AA1D836D5370AB9b28BC1847e178", + "address": "0x8A854C4E750CDf24f138f34A9061b2f556066912", "factoryAddress": "0x6c82877cac5a7a739f16ca0a89c0a328b8764a24", - "sealDuration": 950400, - "expiryTimestamp": 1772323200, + "sealDuration": 1209600, + "expiryTimestamp": 1788908579, "sealingCommittee": "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C" }, + "gateSealTW": { + "factoryAddress": "0x6c82877cac5a7a739f16ca0a89c0a328b8764a24", + "sealDuration": 1209600, + "expiryTimestamp": 1788908579, + "sealingCommittee": "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C", + "address": "0xA6BC802fAa064414AA62117B4a53D27fFfF741F1" + }, "hashConsensusForAccountingOracle": { "address": "0xD624B08C83bAECF0807Dd2c6880C3154a5F0B288", "contract": "contracts/0.8.9/oracle/HashConsensus.sol", @@ -341,7 +362,7 @@ }, "implementation": { "contract": "contracts/0.8.9/LidoLocator.sol", - "address": "0x3ABc4764f0237923d52056CFba7E9AEBf87113D3", + "address": "0x2C298963FB763f74765829722a1ebe0784f4F5Cf", "constructorArgs": [ [ "0x852deD011285fe67063a08005c71a85690503Cee", @@ -357,7 +378,9 @@ "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e", "0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1", "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f", - "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09" + "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", + "0xbDb567672c867DB533119C2dcD4FB9d8b44EC82f", + "0xDC00116a0D3E064427dA2600449cfD2566B3037B" ] ] } @@ -387,7 +410,10 @@ "address": "0xbf05A929c3D7885a6aeAd833a992dA6E5ac23b09", "contract": "contracts/0.8.9/OracleDaemonConfig.sol", "deployTx": "0xa4f380b8806f5a504ef67fce62989e09be5a48bf114af63483c01c22f0c9a36f", - "constructorArgs": ["0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", []], + "constructorArgs": [ + "0x8Ea83AD72396f1E0cD2f8E72b1461db8Eb6aF7B5", + [] + ], "deployParameters": { "NORMALIZED_CL_REWARD_PER_EPOCH": 64, "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000, @@ -407,7 +433,20 @@ "constructorArgs": [ "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", - [9000, 43200, 1000, 50, 600, 8, 24, 7680, 750000, 1000, 101, 50] + [ + 9000, + 43200, + 1000, + 50, + 600, + 8, + 24, + 7680, + 750000, + 1000, + 101, + 50 + ] ], "deployParameters": { "churnValidatorsPerDayLimit": 20000, @@ -421,6 +460,7 @@ "maxPositiveTokenRebase": 750000 } }, + "scratchDeployGasUsed": "52987994", "stakingRouter": { "proxy": { "address": "0xFdDf38947aFB03C621C71b06C9C70bce73f12999", @@ -434,8 +474,48 @@ }, "implementation": { "contract": "contracts/0.8.9/StakingRouter.sol", - "address": "0x89eDa99C0551d4320b56F82DDE8dF2f8D2eF81aA", - "constructorArgs": ["0x00000000219ab540356cBB839Cbe05303d7705Fa"] + "address": "0x226f9265CBC37231882b7409658C18bB7738173A", + "constructorArgs": [ + "0x00000000219ab540356cBB839Cbe05303d7705Fa" + ] + } + }, + "triggerableWithdrawalsGateway": { + "implementation": { + "contract": "contracts/0.8.9/TriggerableWithdrawalsGateway.sol", + "address": "0xDC00116a0D3E064427dA2600449cfD2566B3037B", + "constructorArgs": [ + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + 11200, + 1, + 48 + ] + } + }, + "validatorExitDelayVerifier": { + "implementation": { + "contract": "contracts/0.8.25/ValidatorExitDelayVerifier.sol", + "address": "0xbDb567672c867DB533119C2dcD4FB9d8b44EC82f", + "constructorArgs": [ + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + { + "gIFirstValidatorPrev": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstValidatorCurr": "0x0000000000000000000000000000000000000000000000000096000000000028", + "gIFirstHistoricalSummaryPrev": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstHistoricalSummaryCurr": "0x000000000000000000000000000000000000000000000000000000b600000018", + "gIFirstBlockRootInSummaryPrev": "0x000000000000000000000000000000000000000000000000000000000040000d", + "gIFirstBlockRootInSummaryCurr": "0x000000000000000000000000000000000000000000000000000000000040000d" + }, + 11649024, + 11649024, + 6209536, + 8192, + 32, + 12, + 1606824023, + 98304 + ] } }, "validatorsExitBusOracle": { @@ -450,13 +530,13 @@ ] }, "implementation": { - "address": "0xA89Ea51FddE660f67d1850e03C9c9862d33Bc42c", "contract": "contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol", - "deployTx": "0x5ab545276f78a72a432c3e971c96384973abfab6394e08cb077a006c25aef7a7", - "constructorArgs": [12, 1606824023, "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb"], - "deployParameters": { - "consensusVersion": 1 - } + "address": "0x905A211eD6830Cfc95643f0bE2ff64E7f3bf9b94", + "constructorArgs": [ + 12, + 1606824023, + "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb" + ] } }, "vestingParams": { @@ -543,7 +623,11 @@ "address": "0xE42C659Dc09109566720EA8b2De186c2Be7D94D9", "contract": "contracts/0.8.9/WithdrawalQueueERC721.sol", "deployTx": "0x6ab0151735c01acdef518421358d41a08752169bc383c57d57f5bfa135ac6eb1", - "constructorArgs": ["0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "Lido: stETH Withdrawal NFT", "unstETH"], + "constructorArgs": [ + "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + "Lido: stETH Withdrawal NFT", + "unstETH" + ], "deployParameters": { "name": "Lido: stETH Withdrawal NFT", "symbol": "unstETH" @@ -555,26 +639,21 @@ "address": "0xB9D7934878B5FB9610B3fE8A5e441e8fad7E293f" }, "implementation": { - "address": "0xCC52f17756C04bBa7E377716d7062fC36D7f69Fd", "contract": "contracts/0.8.9/WithdrawalVault.sol", - "deployTx": "0xd9eb2eca684770e4d2b192709b6071875f75072a0ce794a582824ee907a704f3", - "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c"] + "address": "0x7D2BAa6094E1C4B60Da4cbAF4A77C3f4694fD53D", + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + "0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c", + "0xDC00116a0D3E064427dA2600449cfD2566B3037B" + ] } }, "wstETH": { "address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", "contract": "contracts/0.6.12/WstETH.sol", "deployTx": "0xaf2c1a501d2b290ef1e84ddcfc7beb3406f8ece2c46dee14e212e8233654ff05", - "constructorArgs": ["0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"] - }, - "dg:dualGovernance": { - "proxy": { - "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" - } - }, - "dg:emergencyProtectedTimelock": { - "proxy": { - "address": "0xCE0425301C85c5Ea2A0873A2dEe44d78E02D2316" - } + "constructorArgs": [ + "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" + ] } } From bb80f34bd387443704aee7ac836af03ee63ba596 Mon Sep 17 00:00:00 2001 From: F4ever Date: Tue, 23 Sep 2025 15:17:47 +0200 Subject: [PATCH 399/405] fix: deploy contract --- .../upgrade/steps/0100-deploy-tw-contracts.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts index fdbd90659a..d58d3bb74c 100644 --- a/scripts/upgrade/steps/0100-deploy-tw-contracts.ts +++ b/scripts/upgrade/steps/0100-deploy-tw-contracts.ts @@ -5,7 +5,16 @@ import { readUpgradeParameters } from "scripts/utils/upgrade"; import { LidoLocator } from "typechain-types"; -import { cy, deployImplementation, deployWithoutProxy, loadContract, log, persistNetworkState, readNetworkState, Sk } from "lib"; +import { + cy, + deployImplementation, + deployWithoutProxy, + loadContract, + log, + persistNetworkState, + readNetworkState, + Sk, +} from "lib"; dotenv.config({ path: join(__dirname, "../../.env") }); @@ -112,15 +121,15 @@ export async function main() { locator.address, { gIFirstValidatorPrev: "0x0000000000000000000000000000000000000000000000000096000000000028", - gIFirstValidatorCurr: "0x0000000000000000000000000000000000000000000000000096000000000028", + gIFirstValidatorCurr: "0x0000000000000000000000000000000000000000000000000096000000000028", gIFirstHistoricalSummaryPrev: "0x000000000000000000000000000000000000000000000000000000b600000018", gIFirstHistoricalSummaryCurr: "0x000000000000000000000000000000000000000000000000000000b600000018", gIFirstBlockRootInSummaryPrev: "0x000000000000000000000000000000000000000000000000000000000040000d", - gIFirstBlockRootInSummaryCurr: "0x000000000000000000000000000000000000000000000000000000000040000d" + gIFirstBlockRootInSummaryCurr: "0x000000000000000000000000000000000000000000000000000000000040000d", }, // GIndices struct - 22140000, // uint64 firstSupportedSlot, same as test data - 22140000, // uint64 pivotSlot, same as test data - 22140000, // uint64 capellaSlot, same as test data + 11649024, // uint64 firstSupportedSlot, same as test data + 11649024, // uint64 pivotSlot, same as test data + 6209536, // uint64 capellaSlot, same as test data 8192, // uint64 slotsPerHistoricalRoot, 32, // uint32 slotsPerEpoch, 12, // uint32 secondsPerSlot, From 85afa92b4b58c5fd2b3ff0517cccb0e829c7c81d Mon Sep 17 00:00:00 2001 From: F4ever Date: Tue, 23 Sep 2025 15:31:42 +0200 Subject: [PATCH 400/405] fix: update tw verifier deploy --- scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 467b5b8055..00d40fb506 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -12,8 +12,6 @@ import { import { log } from "lib/log"; import { readNetworkState, Sk, updateObjectInState } from "lib/state-file"; -import { ACTIVE_VALIDATOR_PROOF } from "test/0.8.25/validatorState"; - function getEnvVariable(name: string, defaultValue?: string): string { const value = process.env[name] ?? defaultValue; if (value === undefined) { @@ -221,9 +219,9 @@ export async function main() { const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000040000d"; - const FIRST_SUPPORTED_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; - const PIVOT_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; - const CAPELLA_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; + const FIRST_SUPPORTED_SLOT = 11649024; + const PIVOT_SLOT = 11649024; + const CAPELLA_SLOT = 194048 * 32; const SLOTS_PER_HISTORICAL_ROOT = 8192; // Deploy ValidatorExitDelayVerifier From 4566418405abd9d085eef25dfc75e1506f395fc1 Mon Sep 17 00:00:00 2001 From: F4ever Date: Tue, 23 Sep 2025 15:48:26 +0200 Subject: [PATCH 401/405] fix: update dualgov contract --- deployed-mainnet.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployed-mainnet.json b/deployed-mainnet.json index ecb53d61a2..8ee250263a 100644 --- a/deployed-mainnet.json +++ b/deployed-mainnet.json @@ -569,7 +569,7 @@ }, "dg:dualGovernance": { "proxy": { - "address": "0xcdF49b058D606AD34c5789FD8c3BF8B3E54bA2db" + "address": "0xC1db28B3301331277e307FDCfF8DE28242A4486E" } }, "dg:emergencyProtectedTimelock": { From 4101ddc8a62d5e3dcdadab843ebbad3dfdd6d2a9 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:53:07 +0200 Subject: [PATCH 402/405] chore: update fixture for validator proof --- test/0.8.25/validatorState.ts | 160 +++++++++++++++++----------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/test/0.8.25/validatorState.ts b/test/0.8.25/validatorState.ts index 5c0dc9ab21..6b05f0f6e0 100644 --- a/test/0.8.25/validatorState.ts +++ b/test/0.8.25/validatorState.ts @@ -31,21 +31,21 @@ export type ValidatorStateProof = { }; export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { - beaconBlockHeaderRoot: "0xeb961eae87c614e11a7959a529a59db3c9d825d284dc30e0d12e43ba6daf4cca", + beaconBlockHeaderRoot: "0x7f79131ffeab9783e5dea3ec37c7646c2f2990d408a876e01a5fda3f482c0add", beaconBlockHeader: { slot: 22140000, - proposerIndex: "1337", - parentRoot: "0x8576d3eb5ef5b3a460b85e5493d0d0510b7d1a2943a4e51add5227c9d3bffa0f", - stateRoot: "0xa802c5f4f818564a2774a19937fdfafc0241f475d7f28312c3609c6e5995d980", - bodyRoot: "0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1", + proposerIndex: '1337', + parentRoot: '0xc6fffa2dd68f27834a548362829dabc6825cc5790611f2362deb697150e3ff63', + stateRoot: '0x404df16e11f3bfee9aabf186610011dd4d5da05de53020b4655eb65ef3d612b2', + bodyRoot: '0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1' }, - futureBeaconBlockHeaderRoot: "0x1210ae93b4e995d0fd654d986e26a55cc124ddd0232821378a404340a3e837be", + futureBeaconBlockHeaderRoot: "0x84735afbfaf9076b832bb40a07697217a1b75b1bb75e7a4a527a30e5b068ebc1", futureBeaconBlockHeader: { slot: 46908000, - proposerIndex: "31415", - parentRoot: "0xb11bfc560fb8d69246efe86a367a4e1a083a93c1205c93aa5e74e362a913498d", - stateRoot: "0x4ae3998e0571212dc99159869a603f835927908bc427b8053aced1e65a309873", - bodyRoot: "0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1", + proposerIndex: '31415', + parentRoot: '0x20e470325a06a7c6544b2703cabff6c76ad3eaa188ccbef4f3bd14fb8de36b80', + stateRoot: '0x9109a7fbfa45bfc3de0668619941b3a7e6af6e217c3075f7f25647423045db85', + bodyRoot: '0xca4f98890bc98a59f015d06375a5e00546b8f2ac1e88d31b1774ea28d4b3e7d1' }, firstValidatorGI: "0x0000000000000000000000000000000000000000000000000096000000000028", @@ -62,26 +62,26 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { }, validatorProof: [ '0x0edd708eb0bcfc6f5c6c86579867e00b50938ae4656b566c1525385cf0e17d99', - '0x4598d399eb5a13129ec7df15bf7f67ce62894f8dde56e20e01576d4b1c85d4c1', + '0x5e4f1934451903cf345691abaf67c9c830dbdde4fe3ce8fe008d89465aea9cac', '0x250ab3879dec53f13d60b1fcba79ce887f2626863c4e1e678bbd0d0f1d1e9beb', '0xb71de3abf0dcb360fa49c4f512c5bfc5d513ecbd1dbc76c57ac29de173a65c23', '0x4eca0ad10870d33a08f0d4608d7ac506fa484876c884ed10887d46b5e1e694ca', - '0xbed6b05d39560f0ebfd42fcaa33ddbdd9daf2efc3e0002fb327beae8088a4dad', - '0xed185c1976880109a80043b032ff1bbedf74c501dc6e9a2785dabb438513a7dc', - '0xc37a16340d7620558ce6048ce5913bfdfb4122b13185e99b6643a64d8f7e033c', - '0xcbfafa05858b8aa3c8ff0312e723037a38985efea6528963b583aa7927907633', - '0x73e9bb21041240d959b1fc6951a11b78cc5bf2955801663bf49cc397dfeef4dd', - '0xe563a45ae8fd94663ad9b3cb0ac3e25c69827aa4b7ee39e5390b3bd1143e04bb', - '0xbf617e7d2bd6314bb2fe5f525b95f42e629ee88a4d5075ed4841fe1165e2e633', - '0x552485bed4a23db34514b36b78eca98f5933d4c874845042db9420c20e25bb19', - '0xe72547a4304ee482db2d7fc7d8663b139889b1fc17d15cd29fd41fc8da7e4405', - '0xc549ff35940423c68e7241a05ca1aa3af69fc4765232c04e575205fa57b8c3b1', - '0x071f50189feb9a59c48c8d3a5b22bc42576ca404065aad4c3c34e662143fb35e', - '0xfd1bd6dd9b73b8d2dfeebd1a0b45eb8903da8db371b72e3042f668a13ba0c43e', - '0x4d8f1bb6f91e4161c180f9ecaad2f15c66cb6ec7f2cdedc3bcb93c274aad5fa2', - '0x3b6350121535a5c9588561919e9772a84b549602680679766f6de26de339926e', - '0xbb3b90338675e2e6bbb5057f462829234b29a9ab6ae17f1bb1e64c76bce64970', - '0x3072ffd933e00085269e510dea720c2ac88a7853fdb3231ee5fb27e1e9a934cc', + '0x97214d69c7e0db4c0df084ee93881be65ec590e3361a88b2f50076dec4b07328', + '0xa32d619cb63a7c86d3b81d967e5017fa1e48f31ada4d55023156c240dcde02f7', + '0x6066a04e946b399ff7b1589d48d83429c6db4d8faf1c7ecf0cd6ff6530db52ef', + '0x6ce7f2a7f0902e80cda28959d8a19966d9b7d7f5bcfa3f07da0d283d646f7117', + '0xf452b91c8da53f9249099338af6a6eab36790d0a4aa09a47caa7fd1825352b22', + '0x32649afa58f891910d3ab76412332a3ce4727168ba555928033ecd559cd80217', + '0x34641f6e7726bcba9d41979623d38e2e35860d242d396a9ff6bcb591bf73d67d', + '0x8c0180bdbe7b3747eee9bbefae1fd0aa343e3c4a4c817ad4f84ecd41d44cd046', + '0x278f41383ab2754d194431f32ebce04a5d9927926587b0eb1c52debad860d4eb', + '0x187c3aad32e71bbe323136bed2cbc18f72e5303a967ac9589576feb79d289eb4', + '0xc560592b3b7d15d365bdbe64cba68140cd15a14d1ca4236ed5168a92c58e1abc', + '0xa942ed55cd5366a963d3a23c2107169700001c37c6596a78bf98f2c1344f716c', + '0x2d9d317a34544b879a98c88bd86a71f22fb49463b2c3069b575d82c946f0bda8', + '0x06252d62505bc6bf4e0b24c33490044bd955b7476f1fa419aae3ac09926b1d5d', + '0x47bbc5b31c4de80d662684baf15aff05f6ddd7f24dfe73c738799e7d8e1a6065', + '0x3532c6f0eed9913afe0929a630297fa0ac56c01c93cd0571e393a63d70f90035', '0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c', '0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167', '0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7', @@ -101,60 +101,60 @@ export const ACTIVE_VALIDATOR_PROOF: ValidatorStateProof = { '0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74', '0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76', '0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f', - '0x908a1e0000000000000000000000000000000000000000000000000000000000', + '0x80d81f0000000000000000000000000000000000000000000000000000000000', '0xc6341f0000000000000000000000000000000000000000000000000000000000', - '0x41e1bba24366f5cf6502295fea29d06396dd5d0031241811264f5212de2feb00', - '0xc965aa7691807a7649ae6690e1a1d4871fc9ac6c3d96625fde856e152ea236d1', - '0x85d66de5e59bf58a58e8e7334299a7fedcd87d23f97cf1579eae74e9fc0f0eaa', - '0x5c76c5ff78ad80ef4bf12d6adb8df3b15f9c23798bda1aa1e110041254e76cce', - '0x1c4a401fbd320fd7b848c9fc6118444da8797c2e41c525eb475ff76dbf44500b' + '0x5deaf3748443ce43812d2c83fc5705f835f0f91d9d7a61c685341ce2e9db92de', + '0x782a75c7c475f5b8b94628d7026a0af8869e73dd22975c6bcf48fc6e92bff69c', + '0x879bc7898fbd7e023c70713d56d3e5ece2f600fc3d35c0aee9bc68b068aa3397', + '0x080caaa371889b30782b10bee3dbf1280dd4389bfa91a58e781f6196f60b7489', + '0x3845c84d7f2cee4481d287c29f6475415aee37648e5b37c6d55e1090f4f8de97' ], - historicalSummariesGI: "0x000000000000000000000000000000000000000000000000002d800000146000", + historicalSummariesGI: '0x000000000000000000000000000000000000000000000000002d8001e6146000', historicalRootProof: [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", - "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", - "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", - "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", - "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", - "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", - "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", - "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", - "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", - "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", - "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", - "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", - "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", - "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", - "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", - "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", - "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", - "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", - "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", - "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", - "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", - "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", - "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", - "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", - "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", - "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", - "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", - "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", - "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", - "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", - "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", - "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", - "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", - "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", - "0x0100000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x2658397f87f190d84814e4595b3ec8eb0110ab5be675d59434d5a3dfd5ef760d", - "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", - "0xe537052d30df4f0436cd5a3c5debd331c770d9df46da47e0e3db74906186fa09", - "0x4616e1d9312a92eb228e8cd5483fa1fca64d99781d62129bc53718d194b98c45", - "0xa1381fdc64967103fe79c0705727851ce61e7f91bee7e3e7759f9283c91ff7ff", - ], + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b', + '0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71', + '0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c', + '0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c', + '0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30', + '0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1', + '0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c', + '0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193', + '0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1', + '0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b', + '0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220', + '0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b', + '0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71', + '0x5a628dd1ce3fa111b88385de5436f9699629864bd4753eb86ecd99e47cd81d9b', + '0xbaa17ddd36320571f219c76a022f68412ed93328e4f396bffb34c5ee8776f255', + '0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30', + '0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1', + '0x627408028f9281312369d3070f5c2b5d63bf3ac7eac3f84709e7a30fa534597f', + '0x561bfb9a097d51c322d7a0d7ea9f5275c69ecf0e01c5944021b39cf46bfa3889', + '0xd737cda4318e40e6ad25817f36043774cdf83f60683fc808c50f1a99ea3ddc71', + '0x19f72d9191e1612c13e22506d969a89a8e9615941d092ba14d5ce9eb2e6029c2', + '0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220', + '0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f', + '0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e', + '0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784', + '0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb', + '0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb', + '0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab', + '0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4', + '0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f', + '0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa', + '0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c', + '0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167', + '0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7', + '0x9907000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x2658397f87f190d84814e4595b3ec8eb0110ab5be675d59434d5a3dfd5ef760d', + '0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71', + '0xe537052d30df4f0436cd5a3c5debd331c770d9df46da47e0e3db74906186fa09', + '0x4616e1d9312a92eb228e8cd5483fa1fca64d99781d62129bc53718d194b98c45', + '0xa1381fdc64967103fe79c0705727851ce61e7f91bee7e3e7759f9283c91ff7ff' + ] }; From e87c073bf00f3772da31b9c5d8ba7c3c800092bf Mon Sep 17 00:00:00 2001 From: F4ever Date: Tue, 23 Sep 2025 16:26:12 +0200 Subject: [PATCH 403/405] fix: units params --- test/0.8.25/validatorExitDelayVerifier.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index ef65acce5c..dc20ab29de 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -38,14 +38,14 @@ describe("ValidatorExitDelayVerifier.sol", () => { await Snapshot.restore(originalState); }); - const FIRST_SUPPORTED_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; - const PIVOT_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; + const FIRST_SUPPORTED_SLOT = 11649024; + const PIVOT_SLOT = 11649024; + const CAPELLA_SLOT = 194048 * 32; const SLOTS_PER_EPOCH = 32; const SECONDS_PER_SLOT = 12; const GENESIS_TIME = 1606824000; const SHARD_COMMITTEE_PERIOD_IN_SECONDS = 8192; const LIDO_LOCATOR = "0x0000000000000000000000000000000000000001"; - const CAPELLA_SLOT = ACTIVE_VALIDATOR_PROOF.beaconBlockHeader.slot; const SLOTS_PER_HISTORICAL_ROOT = 8192; describe("ValidatorExitDelayVerifier Constructor", () => { From b738605d2923f985f8f07e4d68707f09c73d201c Mon Sep 17 00:00:00 2001 From: F4ever Date: Wed, 24 Sep 2025 11:03:27 +0200 Subject: [PATCH 404/405] feat: add description for values --- scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts | 6 ++++++ scripts/triggerable-withdrawals/tw-deploy.ts | 2 +- test/0.8.25/validatorExitDelayVerifier.test.ts | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts index 00d40fb506..7dbe9cfcc7 100644 --- a/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts +++ b/scripts/scratch/steps/0090-deploy-non-aragon-contracts.ts @@ -219,9 +219,15 @@ export async function main() { const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_PREV = "0x000000000000000000000000000000000000000000000000000000000040000d"; const GI_FIRST_BLOCK_ROOT_IN_SUMMARY_CURR = "0x000000000000000000000000000000000000000000000000000000000040000d"; + // Mainnet values + // Pectra hardfork slot + // https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/electra/fork.md#configuration const FIRST_SUPPORTED_SLOT = 11649024; const PIVOT_SLOT = 11649024; + // Capella hardfork slot + // https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/capella/fork.md#configuration const CAPELLA_SLOT = 194048 * 32; + // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters const SLOTS_PER_HISTORICAL_ROOT = 8192; // Deploy ValidatorExitDelayVerifier diff --git a/scripts/triggerable-withdrawals/tw-deploy.ts b/scripts/triggerable-withdrawals/tw-deploy.ts index 40c99a7aad..10d2981cdb 100644 --- a/scripts/triggerable-withdrawals/tw-deploy.ts +++ b/scripts/triggerable-withdrawals/tw-deploy.ts @@ -115,7 +115,7 @@ async function main(): Promise { const FIRST_SUPPORTED_SLOT = 364032 * SLOTS_PER_EPOCH; // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7600.md#activation const PIVOT_SLOT = FIRST_SUPPORTED_SLOT; const CAPELLA_SLOT = 194048 * 32; // capellaSlot @see https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/capella/fork.md?plain=1#L22 - const SLOTS_PER_HISTORICAL_ROOT = 8192; + const SLOTS_PER_HISTORICAL_ROOT = 8192; // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters // TriggerableWithdrawalsGateway params const TRIGGERABLE_WITHDRAWALS_MAX_LIMIT = 11_200; diff --git a/test/0.8.25/validatorExitDelayVerifier.test.ts b/test/0.8.25/validatorExitDelayVerifier.test.ts index dc20ab29de..f648d7ddc5 100644 --- a/test/0.8.25/validatorExitDelayVerifier.test.ts +++ b/test/0.8.25/validatorExitDelayVerifier.test.ts @@ -38,8 +38,13 @@ describe("ValidatorExitDelayVerifier.sol", () => { await Snapshot.restore(originalState); }); + // Mainnet values + // Pectra hardfork slot + // https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/electra/fork.md#configuration const FIRST_SUPPORTED_SLOT = 11649024; const PIVOT_SLOT = 11649024; + // Capella hardfork slot + // https://github.com/ethereum/consensus-specs/blob/365320e778965631cbef11fd93328e82a746b1f6/specs/capella/fork.md#configuration const CAPELLA_SLOT = 194048 * 32; const SLOTS_PER_EPOCH = 32; const SECONDS_PER_SLOT = 12; From e1146b84a3a1bb4b0b5aebed69c49bb9120c3b42 Mon Sep 17 00:00:00 2001 From: F4ever Date: Thu, 25 Sep 2025 18:26:54 +0200 Subject: [PATCH 405/405] fix: scratch deploy after TW release --- scripts/scratch/deployed-testnet-defaults.json | 10 ++++------ scripts/scratch/steps/0130-grant-roles.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/scratch/deployed-testnet-defaults.json b/scripts/scratch/deployed-testnet-defaults.json index 3593396d89..576be56108 100644 --- a/scripts/scratch/deployed-testnet-defaults.json +++ b/scripts/scratch/deployed-testnet-defaults.json @@ -78,7 +78,7 @@ }, "accountingOracle": { "deployParameters": { - "consensusVersion": 3 + "consensusVersion": 4 } }, "hashConsensusForValidatorsExitBusOracle": { @@ -89,7 +89,7 @@ }, "validatorsExitBusOracle": { "deployParameters": { - "consensusVersion": 3 + "consensusVersion": 4 } }, "depositSecurityModule": { @@ -122,11 +122,9 @@ "NORMALIZED_CL_REWARD_MISTAKE_RATE_BP": 1000, "REBASE_CHECK_NEAREST_EPOCH_DISTANCE": 1, "REBASE_CHECK_DISTANT_EPOCH_DISTANCE": 23, - "VALIDATOR_DELAYED_TIMEOUT_IN_SLOTS": 7200, - "VALIDATOR_DELINQUENT_TIMEOUT_IN_SLOTS": 28800, - "NODE_OPERATOR_NETWORK_PENETRATION_THRESHOLD_BP": 100, "PREDICTION_DURATION_IN_SLOTS": 50400, - "FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT": 1350 + "FINALIZATION_MAX_NEGATIVE_REBASE_EPOCH_SHIFT": 1350, + "EXIT_EVENTS_LOOKBACK_WINDOW_IN_SLOTS": 100800 } }, "nodeOperatorsRegistry": { diff --git a/scripts/scratch/steps/0130-grant-roles.ts b/scripts/scratch/steps/0130-grant-roles.ts index 7927049f5f..34750338de 100644 --- a/scripts/scratch/steps/0130-grant-roles.ts +++ b/scripts/scratch/steps/0130-grant-roles.ts @@ -29,6 +29,7 @@ export async function main() { const validatorsExitBusOracleAddress = state[Sk.validatorsExitBusOracle].proxy.address; const depositSecurityModuleAddress = state[Sk.depositSecurityModule].address; const triggerableWithdrawalsGatewayAddress = state[Sk.triggerableWithdrawalsGateway].address; + const validatorExitDelayVerifierAddress = state[Sk.validatorExitDelayVerifier].address; // StakingRouter const stakingRouter = await loadContract("StakingRouter", stakingRouterAddress); @@ -57,6 +58,13 @@ export async function main() { { from: deployer }, ); + await makeTx( + stakingRouter, + "grantRole", + [await stakingRouter.REPORT_VALIDATOR_EXITING_STATUS_ROLE(), validatorExitDelayVerifierAddress], + { from: deployer }, + ); + // ValidatorsExitBusOracle if (gateSealAddress) { const validatorsExitBusOracle = await loadContract(