diff --git a/.gitignore b/.gitignore index c43b2aff..0d6c9b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,14 +3,17 @@ __pycache__ .hypothesis/ build/ reports/ +typings/ .DS_Store .env /venv +.direnv .vscode .pytest_cache node_modules /contracts_flattened logs cache +tags .idea/ .yarn/ diff --git a/contracts/AllowedMerkleGatesRegistry.sol b/contracts/AllowedMerkleGatesRegistry.sol new file mode 100644 index 00000000..08565d84 --- /dev/null +++ b/contracts/AllowedMerkleGatesRegistry.sol @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/access/AccessControl.sol"; + +/// @title Registry of allowed merkle gates addresses of Staking Module +/// @notice Stores list of allowed addresses +contract AllowedMerkleGatesRegistry is AccessControl { + // ------------- + // EVENTS + // ------------- + event GateAdded(address indexed _gate, string _title); + event GateRemoved(address indexed _gate); + + // ------------- + // ERRORS + // ------------- + string private constant ERROR_GATE_ALREADY_ADDED_TO_ALLOWED_LIST = + "GATE_ALREADY_ADDED_TO_ALLOWED_LIST"; + string private constant ERROR_GATE_NOT_FOUND_IN_ALLOWED_LIST = + "GATE_NOT_FOUND_IN_ALLOWED_LIST"; + + // ------------- + // VARIABLES + // ------------- + + /// @dev List of allowed gates + address[] private allowedGates; + + // Position of the address in the `allowedGates` array, + // plus 1 because index 0 means a value is not in the set. + mapping(address => uint256) private allowedGateIndices; + + // ------------- + // CONSTRUCTOR + // ------------- + + /// @param _admin Address which will be granted with role DEFAULT_ADMIN_ROLE + constructor(address _admin) { + _setupRole(DEFAULT_ADMIN_ROLE, _admin); + } + + // ------------- + // EXTERNAL METHODS + // ------------- + + /// @notice Adds address to list of allowed addresses + function addGate(address _gate, string memory _title) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + require( + allowedGateIndices[_gate] == 0, + ERROR_GATE_ALREADY_ADDED_TO_ALLOWED_LIST + ); + + allowedGates.push(_gate); + allowedGateIndices[_gate] = allowedGates.length; + emit GateAdded(_gate, _title); + } + + /// @notice Removes address from list of allowed addresses + /// @dev To delete an allowed address from the allowedGates array in O(1), + /// we swap the element to delete with the last one in the array, + /// and then remove the last element (sometimes called as 'swap and pop'). + function removeGate(address _gate) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + uint256 index = _getAllowedGateIndex(_gate); + uint256 lastIndex = allowedGates.length - 1; + + if (index != lastIndex) { + address lastAllowedGate = allowedGates[lastIndex]; + allowedGates[index] = lastAllowedGate; + allowedGateIndices[lastAllowedGate] = index + 1; + } + + allowedGates.pop(); + delete allowedGateIndices[_gate]; + emit GateRemoved(_gate); + } + + /// @notice Returns if passed address is listed as allowed gate in the registry + function isGateAllowed(address _gate) external view returns (bool) { + return allowedGateIndices[_gate] > 0; + } + + /// @notice Returns current list of allowed gates + function getAllowedGates() external view returns (address[] memory) { + return allowedGates; + } + + // ------------------ + // PRIVATE METHODS + // ------------------ + + function _getAllowedGateIndex(address _gate) private view returns (uint256 _index) { + _index = allowedGateIndices[_gate]; + require(_index > 0, ERROR_GATE_NOT_FOUND_IN_ALLOWED_LIST); + _index -= 1; + } +} diff --git a/contracts/EVMScriptFactories/CSMSettleELStealingPenalty.sol b/contracts/EVMScriptFactories/CSMSettleELStealingPenalty.sol deleted file mode 100644 index afcd717b..00000000 --- a/contracts/EVMScriptFactories/CSMSettleELStealingPenalty.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.6; - -import "../TrustedCaller.sol"; -import "../libraries/EVMScriptCreator.sol"; -import "../interfaces/IEVMScriptFactory.sol"; -import "../interfaces/ICSModule.sol"; - -/// @author vgorkavenko -/// @notice Creates EVMScript to settle EL stealing penalty for a specific node operators on CSM -contract CSMSettleElStealingPenalty is TrustedCaller, IEVMScriptFactory { - - // ------------- - // ERRORS - // ------------- - - string private constant ERROR_EMPTY_NODE_OPERATORS_IDS = - "EMPTY_NODE_OPERATORS_IDS"; - string private constant ERROR_OUT_OF_RANGE_NODE_OPERATOR_ID = - "OUT_OF_RANGE_NODE_OPERATOR_ID"; - - // ------------- - // VARIABLES - // ------------- - - /// @notice Address of CSModule - ICSModule public immutable csm; - - // ------------- - // CONSTRUCTOR - // ------------- - - constructor(address _trustedCaller, address _csm) - TrustedCaller(_trustedCaller) - { - csm = ICSModule(_csm); - } - - // ------------- - // EXTERNAL METHODS - // ------------- - - /// @notice Creates EVMScript to settle EL stealing penalty for the specific node operators on CSM - /// @param _creator Address who creates EVMScript - /// @param _evmScriptCallData Encoded: uint256[] memory nodeOperatorIds - function createEVMScript(address _creator, bytes memory _evmScriptCallData) - external - view - override - onlyTrustedCaller(_creator) - returns (bytes memory) - { - uint256[] memory nodeOperatorIds = _decodeEVMScriptCallData(_evmScriptCallData); - - _validateInputData(nodeOperatorIds); - - return - EVMScriptCreator.createEVMScript( - address(csm), - ICSModule.settleELRewardsStealingPenalty.selector, - _evmScriptCallData - ); - } - - /// @notice Decodes call data used by createEVMScript method - /// @param _evmScriptCallData Encoded: uint256[] memory nodeOperatorIds - /// @return Node operator IDs to settle EL stealing penalty - function decodeEVMScriptCallData(bytes memory _evmScriptCallData) - external - pure - returns (uint256[] memory) - { - return _decodeEVMScriptCallData(_evmScriptCallData); - } - - // ------------------ - // PRIVATE METHODS - // ------------------ - - function _decodeEVMScriptCallData(bytes memory _evmScriptCallData) - private - pure - returns (uint256[] memory) - { - return abi.decode(_evmScriptCallData, (uint256[])); - } - - function _validateInputData( - uint256[] memory nodeOperatorsIds - ) private view { - require(nodeOperatorsIds.length > 0, ERROR_EMPTY_NODE_OPERATORS_IDS); - uint256 nodeOperatorsCount = csm.getNodeOperatorsCount(); - for (uint256 i = 0; i < nodeOperatorsIds.length; ++i) { - require(nodeOperatorsIds[i] < nodeOperatorsCount, ERROR_OUT_OF_RANGE_NODE_OPERATOR_ID); - } - } -} diff --git a/contracts/EVMScriptFactories/ReportWithdrawalsForSlashedValidators.sol b/contracts/EVMScriptFactories/ReportWithdrawalsForSlashedValidators.sol new file mode 100644 index 00000000..08f9895d --- /dev/null +++ b/contracts/EVMScriptFactories/ReportWithdrawalsForSlashedValidators.sol @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import {TrustedCaller} from "../TrustedCaller.sol"; +import {EVMScriptCreator} from "../libraries/EVMScriptCreator.sol"; +import {IEVMScriptFactory} from "../interfaces/IEVMScriptFactory.sol"; +import {ICSModule, WithdrawnValidatorInfo} from "../interfaces/ICSModule.sol"; + +/// @notice Creates an EVMScript to report slashed validators as withdrawn to a CSM-like module. +contract ReportWithdrawalsForSlashedValidators is TrustedCaller, IEVMScriptFactory { + // ------------- + // ERRORS + // ------------- + + string private constant ERROR_EMPTY_VALIDATOR_INFO_LIST = "EMPTY_VALIDATOR_INFO_LIST"; + string private constant ERROR_OPERATOR_DOES_NOT_EXIST = "OPERATOR_DOES_NOT_EXIST"; + string private constant ERROR_VALIDATOR_NOT_SLASHED = "VALIDATOR_NOT_SLASHED"; + string private constant ERROR_ZERO_EXIT_BALANCE = "ZERO_EXIT_BALANCE"; + + // ------------- + // VARIABLES + // ------------- + + // @notice Alias for the factory. + string public name; + + /// @notice Address of the module. + ICSModule public immutable module; + + // ------------- + // CONSTRUCTOR + // ------------- + + constructor( + address _trustedCaller, + string memory _name, + address _module + ) TrustedCaller(_trustedCaller) { + name = _name; + module = ICSModule(_module); + } + + // ------------- + // EXTERNAL METHODS + // ------------- + + /// @notice Creates an EVMScript to report slashed validators as withdrawn to a CSM-like module. + /// @param _creator Address who creates EVMScript. + /// @param _evmScriptCallData Encoded (WithdrawnValidatorInfo[]). + function createEVMScript(address _creator, bytes memory _evmScriptCallData) + external + view + override + onlyTrustedCaller(_creator) + returns (bytes memory) + { + WithdrawnValidatorInfo[] memory decodedCallData = _decodeEVMScriptCallData( + _evmScriptCallData + ); + _validateInputData(decodedCallData); + + return + EVMScriptCreator.createEVMScript( + address(module), + module.reportSlashedWithdrawnValidators.selector, + _evmScriptCallData + ); + } + + /// @notice Decodes call data used by createEVMScript method + /// @param _evmScriptCallData Encoded (WithdrawnValidatorInfo[]) + /// @return WithdrawnValidatorInfo[] + function decodeEVMScriptCallData(bytes memory _evmScriptCallData) + external + pure + returns (WithdrawnValidatorInfo[] memory) + { + return _decodeEVMScriptCallData(_evmScriptCallData); + } + + // ------------------ + // PRIVATE METHODS + // ------------------ + + function _decodeEVMScriptCallData(bytes memory _evmScriptCallData) + private + pure + returns (WithdrawnValidatorInfo[] memory) + { + return abi.decode(_evmScriptCallData, (WithdrawnValidatorInfo[])); + } + + // NOTE: The method doesn't validate the `slashingPenalty` field. It can be arbitrarily large (if the committee + // decides so), and it can be zero in case of some kind of off-chain agreement. + function _validateInputData(WithdrawnValidatorInfo[] memory _decodedCallData) private view { + require(_decodedCallData.length > 0, ERROR_EMPTY_VALIDATOR_INFO_LIST); + + uint256 nosCount = module.getNodeOperatorsCount(); + for (uint256 i; i < _decodedCallData.length; ++i) { + require(_decodedCallData[i].nodeOperatorId < nosCount, ERROR_OPERATOR_DOES_NOT_EXIST); + require(_decodedCallData[i].exitBalance > 0, ERROR_ZERO_EXIT_BALANCE); + require(_decodedCallData[i].isSlashed, ERROR_VALIDATOR_NOT_SLASHED); + } + } +} diff --git a/contracts/EVMScriptFactories/CSMSetVettedGateTree.sol b/contracts/EVMScriptFactories/SetMerkleGateTree.sol similarity index 54% rename from contracts/EVMScriptFactories/CSMSetVettedGateTree.sol rename to contracts/EVMScriptFactories/SetMerkleGateTree.sol index 3d46de24..d40b867e 100644 --- a/contracts/EVMScriptFactories/CSMSetVettedGateTree.sol +++ b/contracts/EVMScriptFactories/SetMerkleGateTree.sol @@ -6,16 +6,18 @@ pragma solidity 0.8.6; import "../TrustedCaller.sol"; import "../libraries/EVMScriptCreator.sol"; import "../interfaces/IEVMScriptFactory.sol"; -import "../interfaces/IVettedGate.sol"; +import "../interfaces/IMerkleGate.sol"; +import "../interfaces/IAllowedMerkleGatesRegistry.sol"; /// @author vgorkavenko -/// @notice Creates EVMScript to set tree for CSM's VettedGate -contract CSMSetVettedGateTree is TrustedCaller, IEVMScriptFactory { +/// @notice Creates EVMScript to set tree for Module's Gate that implements IMerkleGate +contract SetMerkleGateTree is TrustedCaller, IEVMScriptFactory { // ------------- // ERRORS // ------------- - + string private constant ERROR_GATE_NOT_ALLOWED = + "GATE_NOT_ALLOWED"; string private constant ERROR_EMPTY_TREE_ROOT = "EMPTY_TREE_ROOT"; string private constant ERROR_EMPTY_TREE_CID = @@ -29,30 +31,33 @@ contract CSMSetVettedGateTree is TrustedCaller, IEVMScriptFactory { // VARIABLES // ------------- - /// @notice Alias for factory (e.g. "IdentifiedCommunityStakerSetTreeParams") + /// @notice Alias for factory (e.g. "CSMv3") string public name; - /// @notice Address of VettedGate - IVettedGate public immutable vettedGate; + /// @notice Address of AllowedMerkleGatesRegistry contract + IAllowedMerkleGatesRegistry public immutable allowedMerkleGatesRegistry; // ------------- // CONSTRUCTOR // ------------- - constructor(address _trustedCaller, string memory _name, address _vettedGate) + constructor(address _trustedCaller, string memory _name, address _allowedMerkleGatesRegistry) TrustedCaller(_trustedCaller) { name = _name; - vettedGate = IVettedGate(_vettedGate); + allowedMerkleGatesRegistry = IAllowedMerkleGatesRegistry(_allowedMerkleGatesRegistry); } // ------------- // EXTERNAL METHODS // ------------- - /// @notice Creates EVMScript to set treeRoot and treeCid for CSM's VettedGate + /// @notice Creates EVMScript to set treeRoot and treeCid for Module's Gate /// @param _creator Address who creates EVMScript - /// @param _evmScriptCallData Encoded: bytes32 treeRoot and string treeCid + /// @param _evmScriptCallData Encoded tuple: (address gate, bytes32 treeRoot, string treeCid) where + /// gate - address of gate implementing IMerkleGate + /// treeRoot - root of the Merkle tree + /// treeCid - CID of the Merkle tree function createEVMScript(address _creator, bytes calldata _evmScriptCallData) external view @@ -60,26 +65,26 @@ contract CSMSetVettedGateTree is TrustedCaller, IEVMScriptFactory { onlyTrustedCaller(_creator) returns (bytes memory) { - (bytes32 treeRoot, string memory treeCid) = _decodeEVMScriptCallData(_evmScriptCallData); + (address gate, bytes32 treeRoot, string memory treeCid) = _decodeEVMScriptCallData(_evmScriptCallData); - _validateInputData(treeRoot, treeCid); + _validateInputData(gate, treeRoot, treeCid); - return - EVMScriptCreator.createEVMScript( - address(vettedGate), - IVettedGate.setTreeParams.selector, - _evmScriptCallData - ); + return EVMScriptCreator.createEVMScript( + gate, + IMerkleGate.setTreeParams.selector, + abi.encode(treeRoot, treeCid) + ); } /// @notice Decodes call data used by createEVMScript method /// @param _evmScriptCallData Encoded: bytes32 treeRoot and string treeCid + /// @return gate The address of the gate /// @return treeRoot The root of the tree /// @return treeCid The CID of the tree function decodeEVMScriptCallData(bytes calldata _evmScriptCallData) external pure - returns (bytes32, string memory) + returns (address, bytes32, string memory) { return _decodeEVMScriptCallData(_evmScriptCallData); } @@ -91,18 +96,20 @@ contract CSMSetVettedGateTree is TrustedCaller, IEVMScriptFactory { function _decodeEVMScriptCallData(bytes calldata _evmScriptCallData) private pure - returns (bytes32, string memory) + returns (address, bytes32, string memory) { - return abi.decode(_evmScriptCallData, (bytes32, string)); + return abi.decode(_evmScriptCallData, (address, bytes32, string)); } function _validateInputData( + address gate, bytes32 treeRoot, string memory treeCid ) private view { + require(allowedMerkleGatesRegistry.isGateAllowed(gate), ERROR_GATE_NOT_ALLOWED); require(treeRoot != bytes32(0), ERROR_EMPTY_TREE_ROOT); require(bytes(treeCid).length > 0, ERROR_EMPTY_TREE_CID); - require(treeRoot != vettedGate.treeRoot(), ERROR_SAME_TREE_ROOT); - require(keccak256(bytes(treeCid)) != keccak256(bytes(vettedGate.treeCid())), ERROR_SAME_TREE_CID); + require(treeRoot != IMerkleGate(gate).treeRoot(), ERROR_SAME_TREE_ROOT); + require(keccak256(bytes(treeCid)) != keccak256(bytes(IMerkleGate(gate).treeCid())), ERROR_SAME_TREE_CID); } } diff --git a/contracts/EVMScriptFactories/SettleGeneralDelayedPenalty.sol b/contracts/EVMScriptFactories/SettleGeneralDelayedPenalty.sol new file mode 100644 index 00000000..9319ab20 --- /dev/null +++ b/contracts/EVMScriptFactories/SettleGeneralDelayedPenalty.sol @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +import "../TrustedCaller.sol"; +import "../libraries/EVMScriptCreator.sol"; +import "../interfaces/IEVMScriptFactory.sol"; +import "../interfaces/ICSModule.sol"; +import "../interfaces/ICSAccounting.sol"; + +/// @author vgorkavenko +/// @notice Creates EVMScript to settle general delayed penalty for a specific node operators +contract SettleGeneralDelayedPenalty is TrustedCaller, IEVMScriptFactory { + + // ------------- + // ERRORS + // ------------- + + string private constant ERROR_EMPTY_NODE_OPERATORS_IDS = + "EMPTY_NODE_OPERATORS_IDS"; + string private constant ERROR_OUT_OF_RANGE_NODE_OPERATOR_ID = + "OUT_OF_RANGE_NODE_OPERATOR_ID"; + string private constant ERROR_NODE_OPERATORS_IDS_AND_MAX_AMOUNTS_LENGTH_MISMATCH = + "NODE_OPERATORS_IDS_AND_MAX_AMOUNTS_LENGTH_MISMATCH"; + string private constant ERROR_MAX_AMOUNT_SHOULD_BE_GREATER_OR_EQUAL_THAN_ACTUAL_LOCKED = + "MAX_AMOUNT_SHOULD_BE_GREATER_OR_EQUAL_THAN_ACTUAL_LOCKED"; + string private constant ERROR_MAX_AMOUNT_SHOULD_BE_GREATER_THAN_ZERO = + "MAX_AMOUNT_SHOULD_BE_GREATER_THAN_ZERO"; + + // ------------- + // VARIABLES + // ------------- + + /// @notice Alias for factory (e.g. "CSMv3") + string public name; + + /// @notice Address of Module Contract + ICSModule public immutable module; + ICSAccounting public immutable accounting; + + // ------------- + // CONSTRUCTOR + // ------------- + + constructor(address _trustedCaller, string memory _name, address _module) + TrustedCaller(_trustedCaller) + { + name = _name; + module = ICSModule(_module); + accounting = ICSAccounting(ICSModule(_module).ACCOUNTING()); + } + + // ------------- + // EXTERNAL METHODS + // ------------- + + /// @notice Creates EVMScript to settle general delayed penalty for the specific node operators + /// @param _creator Address who creates EVMScript + /// @param _evmScriptCallData Encoded: uint256[] memory nodeOperatorIds, uint256[] memory maxAmounts + function createEVMScript(address _creator, bytes memory _evmScriptCallData) + external + view + override + onlyTrustedCaller(_creator) + returns (bytes memory) + { + (uint256[] memory nodeOperatorIds, uint256[] memory maxAmounts) = _decodeEVMScriptCallData(_evmScriptCallData); + + _validateInputData(nodeOperatorIds, maxAmounts); + + return + EVMScriptCreator.createEVMScript( + address(module), + ICSModule.settleGeneralDelayedPenalty.selector, + _evmScriptCallData + ); + } + + /// @notice Decodes call data used by createEVMScript method + /// @param _evmScriptCallData Encoded: uint256[] memory nodeOperatorIds, uint256[] memory maxAmounts + /// @return Node operator IDs and max amounts to settle general delayed penalty + function decodeEVMScriptCallData(bytes memory _evmScriptCallData) + external + pure + returns (uint256[] memory, uint256[] memory) + { + return _decodeEVMScriptCallData(_evmScriptCallData); + } + + // ------------------ + // PRIVATE METHODS + // ------------------ + + function _decodeEVMScriptCallData(bytes memory _evmScriptCallData) + private + pure + returns (uint256[] memory, uint256[] memory) + { + return abi.decode(_evmScriptCallData, (uint256[], uint256[])); + } + + function _validateInputData( + uint256[] memory nodeOperatorsIds, + uint256[] memory maxAmounts + ) private view { + require(nodeOperatorsIds.length > 0, ERROR_EMPTY_NODE_OPERATORS_IDS); + require( + nodeOperatorsIds.length == maxAmounts.length, + ERROR_NODE_OPERATORS_IDS_AND_MAX_AMOUNTS_LENGTH_MISMATCH + ); + uint256 nodeOperatorsCount = module.getNodeOperatorsCount(); + for (uint256 i = 0; i < nodeOperatorsIds.length; ++i) { + (uint256 nodeOperatorId, uint256 maxAmount) = (nodeOperatorsIds[i], maxAmounts[i]); + require(nodeOperatorId < nodeOperatorsCount, ERROR_OUT_OF_RANGE_NODE_OPERATOR_ID); + uint256 actualLocked = accounting.getActualLockedBond( + nodeOperatorId + ); + require(maxAmount > 0, ERROR_MAX_AMOUNT_SHOULD_BE_GREATER_THAN_ZERO); + require(maxAmount >= actualLocked, ERROR_MAX_AMOUNT_SHOULD_BE_GREATER_OR_EQUAL_THAN_ACTUAL_LOCKED); + } + } +} diff --git a/contracts/interfaces/IAllowedMerkleGatesRegistry.sol b/contracts/interfaces/IAllowedMerkleGatesRegistry.sol new file mode 100644 index 00000000..9bbd1e21 --- /dev/null +++ b/contracts/interfaces/IAllowedMerkleGatesRegistry.sol @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +interface IAllowedMerkleGatesRegistry { + function isGateAllowed(address _gate) external view returns (bool); +} + diff --git a/contracts/interfaces/ICSAccounting.sol b/contracts/interfaces/ICSAccounting.sol new file mode 100644 index 00000000..d2334cab --- /dev/null +++ b/contracts/interfaces/ICSAccounting.sol @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + + pragma solidity 0.8.6; + +/// @title Lido's CSM accounting interface +interface ICSAccounting { + /// @notice Get amount of the locked bond in ETH (stETH) by the given Node Operator + /// @param nodeOperatorId ID of the Node Operator + /// @return Amount of the actual locked bond + function getActualLockedBond( + uint256 nodeOperatorId + ) external view returns (uint256); +} diff --git a/contracts/interfaces/ICSModule.sol b/contracts/interfaces/ICSModule.sol index 5b581e35..15b87f39 100644 --- a/contracts/interfaces/ICSModule.sol +++ b/contracts/interfaces/ICSModule.sol @@ -1,16 +1,34 @@ // SPDX-FileCopyrightText: 2024 Lido // SPDX-License-Identifier: GPL-3.0 - pragma solidity 0.8.6; +pragma solidity 0.8.6; + +struct WithdrawnValidatorInfo { + uint256 nodeOperatorId; + uint256 keyIndex; + uint256 exitBalance; + uint256 slashingPenalty; + bool isSlashed; +} /// @title Lido's Community Staking Module interface interface ICSModule { + + function ACCOUNTING() external view returns (address); + /// @notice Settles blocked bond for the given Node Operators /// @dev Should be called by the Easy Track /// @param nodeOperatorIds IDs of the Node Operators - function settleELRewardsStealingPenalty( - uint256[] memory nodeOperatorIds + /// @param maxAmounts Maximum amounts to settle for each Node Operator + function settleGeneralDelayedPenalty( + uint256[] memory nodeOperatorIds, + uint256[] memory maxAmounts ) external; function getNodeOperatorsCount() external view returns (uint256); + + /// @notice Report withdrawn validators that have been slashed. + /// @notice Called by the Easy Track EVM script executor via a motion started by the dedicated committee. + /// @param validatorInfos An array WithdrawnValidatorInfo structs + function reportSlashedWithdrawnValidators(WithdrawnValidatorInfo[] calldata validatorInfos) external; } diff --git a/contracts/interfaces/IMerkleGate.sol b/contracts/interfaces/IMerkleGate.sol new file mode 100644 index 00000000..d67dbca1 --- /dev/null +++ b/contracts/interfaces/IMerkleGate.sol @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.6; + +/// @title Merkle Gate Interface +/// @notice Common surface for gates that guard node operator creation via Merkle proofs. +interface IMerkleGate { + /// @return treeRoot Current Merkle tree root + function treeRoot() external view returns (bytes32); + + /// @return treeCid Current Merkle tree CID + function treeCid() external view returns (string memory); + + /// @notice Update Merkle tree params + /// @param _treeRoot New root + /// @param _treeCid New CID + function setTreeParams( + bytes32 _treeRoot, + string calldata _treeCid + ) external; +} \ No newline at end of file diff --git a/contracts/interfaces/IVettedGate.sol b/contracts/interfaces/IVettedGate.sol deleted file mode 100644 index cd132a82..00000000 --- a/contracts/interfaces/IVettedGate.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Lido -// SPDX-License-Identifier: GPL-3.0 - - pragma solidity 0.8.6; - -/// @title Lido's Community Staking Module Vetted Gate interface -interface IVettedGate { - - function treeRoot() external view returns (bytes32); - function treeCid() external view returns (string memory); - - /// @notice Set the root of the eligible members Merkle Tree - /// @param _treeRoot New root of the Merkle Tree - /// @param _treeCid New CID of the Merkle Tree - function setTreeParams( - bytes32 _treeRoot, - string calldata _treeCid - ) external; -} diff --git a/contracts/test/CSLikeModuleStub.sol b/contracts/test/CSLikeModuleStub.sol new file mode 100644 index 00000000..cc9275c2 --- /dev/null +++ b/contracts/test/CSLikeModuleStub.sol @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.4; + +import {WithdrawnValidatorInfo} from "../interfaces/ICSModule.sol"; + +contract CSLikeModuleStub { + uint256 internal _nodeOperatorsCount; + + event GotValidatorInfo(WithdrawnValidatorInfo info); + + function reportSlashedWithdrawnValidators(WithdrawnValidatorInfo[] calldata validatorInfos) external { + for (uint256 i; i < validatorInfos.length; ++i) { + emit GotValidatorInfo(validatorInfos[i]); + } + } + + function getNodeOperatorsCount() external view returns (uint256) { + return _nodeOperatorsCount; + } + + function mock_setNodeOperatorsCount(uint256 nodeOperatorsCount) external { + _nodeOperatorsCount = nodeOperatorsCount; + } +} diff --git a/contracts/test/VettedGateStub.sol b/contracts/test/MerkleGateStub.sol similarity index 90% rename from contracts/test/VettedGateStub.sol rename to contracts/test/MerkleGateStub.sol index 0e751e13..4bc81183 100644 --- a/contracts/test/VettedGateStub.sol +++ b/contracts/test/MerkleGateStub.sol @@ -6,8 +6,8 @@ pragma solidity ^0.8.4; import "OpenZeppelin/openzeppelin-contracts@4.3.2/contracts/access/AccessControl.sol"; /// @author vgorkavenko -/// @notice Helper contract with stub implementation of VettedGate for testing -contract VettedGateStub is AccessControl { +/// @notice Helper contract with stub implementation of MerkleGate for testing +contract MerkleGateStub is AccessControl { bytes32 public constant SET_TREE_ROLE = keccak256("SET_TREE_ROLE"); bytes32 public treeRoot; diff --git a/interfaces/CSModule.json b/interfaces/CSModule.json index c2dfaf11..58053c92 100644 --- a/interfaces/CSModule.json +++ b/interfaces/CSModule.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"minSlashingPenaltyQuotient","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingFine","type":"uint256","internalType":"uint256"},{"name":"maxKeysPerOperatorEA","type":"uint256","internalType":"uint256"},{"name":"maxKeyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"lidoLocator","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EL_REWARDS_STEALING_FINE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"INITIAL_SLASHING_PENALTY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MAX_KEY_REMOVAL_CHARGE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"activatePublicRelease","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addNodeOperatorStETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"earlyAdoption","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEarlyAdoption"}],"stateMutability":"view"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"_earlyAdoption","type":"address","internalType":"address"},{"name":"_keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorSlashed","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"keyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"normalizeQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitInitialSlashing","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"InitialSlashingSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PublicRelease","inputs":[],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyActivated","inputs":[]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadySubmitted","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MaxSigningKeysCountExceeded","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToJoinYet","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanNonExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"},{"name":"_accounting","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MAX_PENALTY_MULTIPLIER","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_ACTIVATION_BALANCE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PENALTY_QUOTIENT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_GENERAL_DELAYED_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_GENERAL_DELAYED_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelGeneralDelayedPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateGeneralDelayedPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"},{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositQueuePointers","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"exitDeadlineThreshold","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"},{"name":"usedPriorityQueue","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorManagementProperties","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorOwner","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorTotalDepositedKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"totalDepositedKeys","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorExitDelayPenaltyApplicable","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorSlashed","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onValidatorExitTriggered","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"withdrawalRequestPaidFee","type":"uint256","internalType":"uint256"},{"name":"exitType","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onValidatorSlashed","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportGeneralDelayedPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"penaltyType","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"details","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportValidatorExitDelay","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleGeneralDelayedPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"},{"name":"maxAmounts","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateDepositableValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"queuePriority","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ValidatorSlashingReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"exitBalance","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"slashingPenalty","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"CannotAddKeys","inputs":[]},{"type":"error","name":"DepositQueueHasUnsupportedWithdrawalCredentials","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"KeysLimitExceeded","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NoQueuedKeysToMigrate","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEligibleForPriorityQueue","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"PriorityQueueAlreadyUsed","inputs":[]},{"type":"error","name":"PriorityQueueMaxDepositsUsed","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"SlashingPenaltyIsNotApplicable","inputs":[]},{"type":"error","name":"ValidatorSlashingAlreadyReported","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroExitBalance","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroManagerAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]},{"type":"function","name":"REPORT_REGULAR_WITHDRAWN_VALIDATORS_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_SLASHED_WITHDRAWN_VALIDATORS_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"reportRegularWithdrawnValidators","inputs":[{"name":"validatorInfos","type":"tuple[]","internalType":"struct WithdrawnValidatorInfo[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"exitBalance","type":"uint256","internalType":"uint256"},{"name":"slashingPenalty","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportSlashedWithdrawnValidators","inputs":[{"name":"validatorInfos","type":"tuple[]","internalType":"struct WithdrawnValidatorInfo[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"exitBalance","type":"uint256","internalType":"uint256"},{"name":"slashingPenalty","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"}] \ No newline at end of file diff --git a/scripts/acceptance_test_csm_set_vetted_gate_tree_setup.py b/scripts/acceptance_test_csm_set_vetted_gate_tree_setup.py deleted file mode 100644 index a69dc14b..00000000 --- a/scripts/acceptance_test_csm_set_vetted_gate_tree_setup.py +++ /dev/null @@ -1,55 +0,0 @@ -from dataclasses import dataclass - -from brownie import chain, CSMSetVettedGateTree - -from utils import log - - -@dataclass -class DeployConfig: - trusted_caller: str - factory_name: str - vetted_gate_address: str - - -deploy_config = DeployConfig( - trusted_caller="", - factory_name="", - vetted_gate_address="" -) - - -deployment_tx_hash = "" - - -def main(): - - tx = chain.get_transaction(deployment_tx_hash) - - log.br() - - log.nb("tx of creation", deployment_tx_hash) - - log.br() - - log.nb("trusted_caller", deploy_config.trusted_caller) - log.nb("factory_name", deploy_config.factory_name) - log.nb("vetted_gate_address", deploy_config.vetted_gate_address) - - log.br() - - set_vetted_gate_tree_factory = CSMSetVettedGateTree.at(tx.contract_address) - log.nb('CSMSetVettedGateTree address (from tx)', set_vetted_gate_tree_factory) - - log.br() - - assert set_vetted_gate_tree_factory.vettedGate() == deploy_config.vetted_gate_address - log.nb('VettedGate address is correct') - - assert set_vetted_gate_tree_factory.trustedCaller() == deploy_config.trusted_caller - log.nb('Trusted caller is correct') - - assert set_vetted_gate_tree_factory.name() == deploy_config.factory_name - log.nb('Factory name is correct') - - log.br() diff --git a/scripts/acceptance_test_csm_settle_el_stealing_setup.py b/scripts/acceptance_test_csm_settle_el_stealing_setup.py deleted file mode 100644 index 9af66a69..00000000 --- a/scripts/acceptance_test_csm_settle_el_stealing_setup.py +++ /dev/null @@ -1,49 +0,0 @@ -from dataclasses import dataclass - -from brownie import chain, CSMSettleElStealingPenalty - -from utils import log - - -@dataclass -class DeployConfig: - trusted_caller: str - csm_address: str - - -deploy_config = DeployConfig( - trusted_caller="", - csm_address="" -) - - -deployment_tx_hash = "" - - -def main(): - - tx = chain.get_transaction(deployment_tx_hash) - - log.br() - - log.nb("tx of creation", deployment_tx_hash) - - log.br() - - log.nb("trusted_caller", deploy_config.trusted_caller) - log.nb("csm_address", deploy_config.csm_address) - - log.br() - - settle_el_stealing_factory = CSMSettleElStealingPenalty.at(tx.contract_address) - log.nb('CSMSettleElStealingPenalty address (from tx)', settle_el_stealing_factory) - - log.br() - - assert settle_el_stealing_factory.csm() == deploy_config.csm_address - log.nb('CSM address is correct') - - assert settle_el_stealing_factory.trustedCaller() == deploy_config.trusted_caller - log.nb('Trusted caller is correct') - - log.br() diff --git a/scripts/acceptance_test_set_merkle_gate_tree_setup.py b/scripts/acceptance_test_set_merkle_gate_tree_setup.py new file mode 100644 index 00000000..8da486b7 --- /dev/null +++ b/scripts/acceptance_test_set_merkle_gate_tree_setup.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass + +from brownie import chain, SetMerkleGateTree + +from utils import log + + +@dataclass +class DeployConfig: + trusted_caller: str + allowed_merkle_gates_registry: str + factory_name: str + + +deploy_config = DeployConfig( + trusted_caller="", + allowed_merkle_gates_registry="", + factory_name="", +) + + +deployment_tx_hash = "" + + +def main(): + tx = chain.get_transaction(deployment_tx_hash) + + log.br() + log.nb("tx of creation", deployment_tx_hash) + + log.br() + log.nb("trusted_caller", deploy_config.trusted_caller) + log.nb("allowed_merkle_gates_registry", deploy_config.allowed_merkle_gates_registry) + log.nb("factory_name", deploy_config.factory_name) + + log.br() + + set_merkle_gate_tree_factory = SetMerkleGateTree.at(tx.contract_address) + log.nb('SetMerkleGateTree address (from tx)', set_merkle_gate_tree_factory) + + log.br() + + assert set_merkle_gate_tree_factory.trustedCaller() == deploy_config.trusted_caller + log.nb('Trusted caller is correct') + + assert set_merkle_gate_tree_factory.allowedMerkleGatesRegistry() == deploy_config.allowed_merkle_gates_registry + log.nb('AllowedMerkleGatesRegistry is correct') + + assert set_merkle_gate_tree_factory.name() == deploy_config.factory_name + log.nb('Factory name is correct') + + log.br() diff --git a/scripts/acceptance_test_settle_general_delayed_penalty_setup.py b/scripts/acceptance_test_settle_general_delayed_penalty_setup.py new file mode 100644 index 00000000..4e1581a9 --- /dev/null +++ b/scripts/acceptance_test_settle_general_delayed_penalty_setup.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass + +from brownie import chain, SettleGeneralDelayedPenalty + +from utils import log + + +@dataclass +class DeployConfig: + trusted_caller: str + factory_name: str + module_address: str + + +deploy_config = DeployConfig( + trusted_caller="", + factory_name="", + module_address="" +) + + +deployment_tx_hash = "" + + +def main(): + + tx = chain.get_transaction(deployment_tx_hash) + + log.br() + + log.nb("tx of creation", deployment_tx_hash) + + log.br() + + log.nb("trusted_caller", deploy_config.trusted_caller) + log.nb("factory_name", deploy_config.factory_name) + log.nb("module_address", deploy_config.module_address) + + log.br() + + settle_general_delayed_penalty_factory = SettleGeneralDelayedPenalty.at(tx.contract_address) + log.nb('SettleGeneralDelayedPenalty address (from tx)', settle_general_delayed_penalty_factory) + + log.br() + + assert settle_general_delayed_penalty_factory.module() == deploy_config.module_address + log.nb('Module address is correct') + + assert settle_general_delayed_penalty_factory.trustedCaller() == deploy_config.trusted_caller + log.nb('Trusted caller is correct') + + log.br() diff --git a/scripts/deploy_allowed_merkle_gates_registry.py b/scripts/deploy_allowed_merkle_gates_registry.py new file mode 100644 index 00000000..49dd89e4 --- /dev/null +++ b/scripts/deploy_allowed_merkle_gates_registry.py @@ -0,0 +1,79 @@ +import os +import json +from brownie import chain, network + +from utils.config import ( + get_env, + get_is_live, + get_deployer_account, + get_network_name, + prompt_bool, +) +from utils import log + +from brownie import AllowedMerkleGatesRegistry + + +def main(): + network_name = get_network_name() + deployer = get_deployer_account(get_is_live(), network=network_name) + + admin = os.environ.get("ALLOWED_MERKLE_GATES_ADMIN") + if not admin: + raise EnvironmentError("Please set ALLOWED_MERKLE_GATES_ADMIN env variable") + + log.br() + log.nb("Current network", network.show_active(), color_hl=log.color_magenta) + log.ok("chain id", chain.id) + log.ok("Deployer", deployer) + log.br() + log.nb("Admin", admin) + + + log.br() + print("Proceed? [yes/no]: ") + if not prompt_bool(): + log.nb("Aborting") + return + + tx_params = {"from": deployer} + if get_is_live(): + # project-wide gas defaults + tx_params["priority_fee"] = "2 gwei" + tx_params["max_fee"] = "50 gwei" + + registry = AllowedMerkleGatesRegistry.deploy(admin, tx_params) + + log.br() + log.ok("AllowedMerkleGatesRegistry deployed", registry.address) + + if get_is_live(): + # Save artifacts into deployed-sm-.json + deployment_artifacts = { + "AllowedMerkleGatesRegistry": { + "contract": "AllowedMerkleGatesRegistry", + "address": registry.address, + "constructorArgs": [admin], + } + } + + artifacts_path = f"deployed-sm-{network_name}.json" + if os.path.exists(artifacts_path): + try: + with open(artifacts_path, "r") as prev: + existing = json.load(prev) + except Exception: + existing = {} + existing.update(deployment_artifacts) + deployment_artifacts = existing + + with open(artifacts_path, "w") as out: + json.dump(deployment_artifacts, out, indent=4) + + if get_env("FORCE_VERIFY", False): + log.ok("Verifying AllowedMerkleGatesRegistry...") + AllowedMerkleGatesRegistry.publish_source(registry) + + log.br() + print("Hit to quit script") + input() diff --git a/scripts/deploy_csm_set_vetted_gate_tree_factory.py b/scripts/deploy_csm_set_vetted_gate_tree_factory.py deleted file mode 100644 index 65b9bc20..00000000 --- a/scripts/deploy_csm_set_vetted_gate_tree_factory.py +++ /dev/null @@ -1,141 +0,0 @@ -import json -import os - -from brownie import ( - chain, - web3, - accounts, - CSMSetVettedGateTree -) - -from utils import log -from utils.config import ( - get_env, - get_is_live, - get_network_name, - get_deployer_account, - prompt_bool, -) - -def check_etherscan_token(): - if "ETHERSCAN_TOKEN" not in os.environ: - raise EnvironmentError("Please set ETHERSCAN_TOKEN env variable") - etherscan_token = os.environ["ETHERSCAN_TOKEN"] - - assert etherscan_token, "Etherscan API token is not valid" - - return etherscan_token - - -def get_trusted_caller(): - if "TRUSTED_CALLER" not in os.environ: - raise EnvironmentError("Please set TRUSTED_CALLER env variable") - trusted_caller = os.environ["TRUSTED_CALLER"] - - assert web3.is_address(trusted_caller), "Trusted caller address is not valid" - - return trusted_caller - - -def get_factory_name(): - if "FACTORY_NAME" not in os.environ: - raise EnvironmentError("Please set FACTORY_NAME env variable") - - factory_name = os.environ["FACTORY_NAME"] - - if not factory_name: - raise ValueError("Factory name cannot be empty") - if not isinstance(factory_name, str): - raise TypeError("Factory name must be a string") - - return factory_name - - -def get_vetted_gate_address(): - if "VETTED_GATE_ADDRESS" not in os.environ: - raise EnvironmentError("Please set VETTED_GATE_ADDRESS env variable") - vetted_gate_address = os.environ["VETTED_GATE_ADDRESS"] - - assert web3.is_address(vetted_gate_address), "VettedGate address is not valid" - - return vetted_gate_address - - -def main(): - if get_is_live() and get_env("FORCE_VERIFY", False): - check_etherscan_token() - - network_name = get_network_name() - - deployer = get_deployer_account(get_is_live(), network=network_name, dev_ldo_transfer=False) - trusted_caller = get_trusted_caller() - factory_name = get_factory_name() - vetted_gate_address = get_vetted_gate_address() - - log.br() - - log.nb("Current network", network_name, color_hl=log.color_magenta) - log.nb("chain id", chain.id) - - log.br() - - log.ok("Deployer", deployer) - - log.br() - - log.ok("VettedGate address", vetted_gate_address) - log.ok("Trusted caller", trusted_caller) - log.ok("Factory name", factory_name) - - log.br() - - print("Proceed? [yes/no]: ") - - if not prompt_bool(): - log.nb("Aborting") - return - - log.br() - - deployment_artifacts = {} - - # Gas parameters following project conventions - tx_params = {"from": deployer, "priority_fee": "2 gwei", "max_fee": "50 gwei"} - - log.nb("Deploying CSMSetVettedGateTree...") - - csm_set_vetted_gate_tree = CSMSetVettedGateTree.deploy( - trusted_caller, - factory_name, - vetted_gate_address, - tx_params - ) - deployment_artifacts["CSMSetVettedGateTree"] = { - "contract": "CSMSetVettedGateTree", - "address": csm_set_vetted_gate_tree.address, - "constructorArgs": [trusted_caller, factory_name, vetted_gate_address], - } - - log.ok("Deployed CSMSetVettedGateTree", csm_set_vetted_gate_tree.address) - - log.br() - log.nb("All factories have been deployed.") - log.nb("Saving artifacts...") - - artifacts_path = f"deployed-csm-{network_name}.json" - - if os.path.exists(artifacts_path): - with open(artifacts_path, "r") as previous_artifacts: - existing_artifacts = json.load(previous_artifacts) - deployment_artifacts.update(existing_artifacts) - - with open(artifacts_path, "w") as outfile: - json.dump(deployment_artifacts, outfile, indent=4) - - if get_is_live() and get_env("FORCE_VERIFY", False): - log.nb("Starting code verification.") - log.br() - - CSMSetVettedGateTree.publish_source(csm_set_vetted_gate_tree) - - log.br() diff --git a/scripts/deploy_csm_settle_el_stealing_factory.py b/scripts/deploy_csm_settle_el_stealing_factory.py deleted file mode 100644 index e6811d26..00000000 --- a/scripts/deploy_csm_settle_el_stealing_factory.py +++ /dev/null @@ -1,89 +0,0 @@ -import json -import os - -from brownie import ( - chain, - CSMSettleElStealingPenalty, - web3, -) - -from utils import csm, log -from utils.config import ( - get_is_live, - get_deployer_account, - prompt_bool, - get_network_name, -) - - -def get_trusted_caller(): - if "TRUSTED_CALLER" not in os.environ: - raise EnvironmentError("Please set TRUSTED_CALLER env variable") - trusted_caller = os.environ["TRUSTED_CALLER"] - - assert web3.is_address(trusted_caller), "Trusted caller address is not valid" - - return trusted_caller - - -def main(): - network_name = get_network_name() - csm_contracts = csm.contracts(network=network_name) - - deployer = get_deployer_account(get_is_live(), network=network_name) - trusted_caller = get_trusted_caller() - cs_module = csm_contracts.module - - log.br() - - log.nb("Current network", network_name, color_hl=log.color_magenta) - log.nb("Using deployed addresses for", network_name, color_hl=log.color_yellow) - log.nb("chain id", chain.id) - - log.br() - - log.ok("Deployer", deployer) - - log.br() - - log.ok("CSModule module address", cs_module) - log.ok("Trusted caller", trusted_caller) - - log.br() - - print("Proceed? [yes/no]: ") - - if not prompt_bool(): - log.nb("Aborting") - return - - - log.br() - - deployment_artifacts = {} - - # CSMSettleElStealingPenalty - csm_settle_el_stealing_penalty = CSMSettleElStealingPenalty.deploy( - trusted_caller, cs_module.address, {"from": deployer} - ) - deployment_artifacts["CSMSettleElStealingPenalty"] = { - "contract": "CSMSettleElStealingPenalty", - "address": csm_settle_el_stealing_penalty.address, - "constructorArgs": [trusted_caller, cs_module.address], - } - - log.ok("Deployed CSMSettleElStealingPenalty", csm_settle_el_stealing_penalty.address) - - log.br() - log.nb("All factories have been deployed.") - log.nb("Saving atrifacts...") - - with open(f"deployed-csm-{network_name}.json", "w") as outfile: - json.dump(deployment_artifacts, outfile) - - log.nb("Starting code verification.") - log.br() - - CSMSettleElStealingPenalty.publish_source(csm_settle_el_stealing_penalty) - - log.br() diff --git a/scripts/deploy_report_withdrawals_for_slashed_validator_factory.py b/scripts/deploy_report_withdrawals_for_slashed_validator_factory.py new file mode 100644 index 00000000..84f3fdcd --- /dev/null +++ b/scripts/deploy_report_withdrawals_for_slashed_validator_factory.py @@ -0,0 +1,118 @@ +import json +import os + +from brownie import ( + ReportWithdrawalsForSlashedValidators, # type: ignore + chain, + web3, +) + +from utils import log +from utils.config import ( + get_deployer_account, + get_env, + get_is_live, + get_network_name, + prompt_bool, +) + + +def main(): + network_name = get_network_name() + assert type(network_name) is str + + deployer = get_deployer_account( + get_is_live(), network=network_name, dev_ldo_transfer=False + ) + trusted_caller = _get_trusted_caller() + module_address = _get_module_address() + factory_name = _get_factory_name() + + log.br() + log.nb("Current network", network_name, color_hl=log.color_magenta) + log.ok("Chain ID", chain.id) + log.ok("Deployer", deployer) + log.br() + log.ok("Trusted caller", trusted_caller) + log.ok("Module address", module_address) + log.ok("Factory name", factory_name) + + log.br() + print("Proceed? [yes/no]: ") + if not prompt_bool(): + log.nb("Aborting") + return + + tx_params = {"from": deployer} + if get_is_live(): + tx_params["priority_fee"] = "2 gwei" + tx_params["max_fee"] = "50 gwei" + + constructor_args = ( + trusted_caller, + factory_name, + module_address, + ) + factory = ReportWithdrawalsForSlashedValidators.deploy( + *constructor_args, {"from": deployer} + ) + + log.br() + log.ok("Deployed ReportWithdrawalsForSlashedValidators", factory.address) + + if get_is_live(): + # Save artifacts into deployed-sm-.json + entry_key = f"ReportWithdrawalsForSlashedValidators:{factory_name}" + new_entry = { + entry_key: { + "contract": "ReportWithdrawalsForSlashedValidators", + "address": factory.address, + "constructorArgs": constructor_args, + } + } + + artifacts_path = f"deployed-sm-{network_name}.json" + + try: + with open(artifacts_path, "r") as prev: + artifacts = json.load(prev) + except FileNotFoundError: + artifacts = {} + + artifacts.update(new_entry) + + with open(artifacts_path, "w") as out: + json.dump(artifacts, out, indent=4) + + if get_env("FORCE_VERIFY", False): + log.ok("Verifying ReportWithdrawalsForSlashedValidators...") + ReportWithdrawalsForSlashedValidators.publish_source(factory) + + log.br() + print("Hit to quit script") + input() + + +def _get_trusted_caller(): + addr = os.environ.get("TRUSTED_CALLER") + if not web3.is_address(addr): + raise ValueError( + f"{addr} is not a valid address, check the TRUSTED_CALLER env variable" + ) + return addr + + +def _get_module_address(): + addr = os.environ.get("MODULE_ADDRESS") + if not web3.is_address(addr): + raise ValueError( + f"{addr} is not a valid address, check the MODULE_ADDRESS env variable" + ) + return addr + + +def _get_factory_name(): + name = os.environ.get("FACTORY_NAME") + if not name: + raise ValueError("Please provide non-empty name via FACTORY_NAME env variable") + return name diff --git a/scripts/deploy_set_merkle_gate_tree_factory.py b/scripts/deploy_set_merkle_gate_tree_factory.py new file mode 100644 index 00000000..bf68a71a --- /dev/null +++ b/scripts/deploy_set_merkle_gate_tree_factory.py @@ -0,0 +1,105 @@ +import os +import json + +from brownie import chain +from brownie import SetMerkleGateTree + +from utils import log +from utils.config import ( + get_env, + get_is_live, + get_network_name, + get_deployer_account, + prompt_bool, +) + + +def _get_trusted_caller(): + addr = os.environ.get("TRUSTED_CALLER") + if not addr: + raise EnvironmentError("Please set TRUSTED_CALLER env variable") + return addr + + +def _get_allowed_registry(): + addr = os.environ.get("ALLOWED_MERKLE_GATES_REGISTRY") + if not addr: + raise EnvironmentError("Please set ALLOWED_MERKLE_GATES_REGISTRY env variable") + return addr + + +def _get_factory_name(): + name = os.environ.get("FACTORY_NAME") + if not name: + raise EnvironmentError("Please set FACTORY_NAME env variable") + return name + + +def main(): + network_name = get_network_name() + deployer = get_deployer_account(get_is_live(), network=network_name, dev_ldo_transfer=False) + trusted_caller = _get_trusted_caller() + allowed_registry = _get_allowed_registry() + factory_name = _get_factory_name() + + log.br() + log.nb("Current network", network_name, color_hl=log.color_magenta) + log.ok("chain id", chain.id) + log.ok("Deployer", deployer) + log.br() + log.ok("Trusted caller", trusted_caller) + log.ok("AllowedMerkleGatesRegistry", allowed_registry) + log.ok("Factory name", factory_name) + + log.br() + print("Proceed? [yes/no]: ") + if not prompt_bool(): + log.nb("Aborting") + return + + tx_params = {"from": deployer} + if get_is_live(): + tx_params["priority_fee"] = "2 gwei" + tx_params["max_fee"] = "50 gwei" + + factory = SetMerkleGateTree.deploy( + trusted_caller, + factory_name, + allowed_registry, + tx_params, + ) + + log.br() + log.ok("Deployed SetMerkleGateTree", factory.address) + + if get_is_live(): + # Save artifacts into deployed-sm-.json + entry_key = f"SetMerkleGateTree:{factory_name}" + deployment_artifacts = { + entry_key: { + "contract": "SetMerkleGateTree", + "address": factory.address, + "constructorArgs": [trusted_caller, factory_name, allowed_registry], + } + } + + artifacts_path = f"deployed-sm-{network_name}.json" + if os.path.exists(artifacts_path): + try: + with open(artifacts_path, "r") as prev: + existing = json.load(prev) + except Exception: + existing = {} + existing.update(deployment_artifacts) + deployment_artifacts = existing + + with open(artifacts_path, "w") as out: + json.dump(deployment_artifacts, out, indent=4) + + if get_env("FORCE_VERIFY", False): + log.ok("Verifying SetMerkleGateTree...") + SetMerkleGateTree.publish_source(factory) + + log.br() + print("Hit to quit script") + input() diff --git a/scripts/deploy_settle_general_delayed_penalty_factory.py b/scripts/deploy_settle_general_delayed_penalty_factory.py new file mode 100644 index 00000000..a8f0df7d --- /dev/null +++ b/scripts/deploy_settle_general_delayed_penalty_factory.py @@ -0,0 +1,110 @@ +import json +import os + +from brownie import SettleGeneralDelayedPenalty, chain, web3 + +from utils import log +from utils.config import ( + get_deployer_account, + get_env, + get_is_live, + get_network_name, + prompt_bool, +) + + +def main(): + network_name = get_network_name() + assert isinstance(network_name, str) + + deployer = get_deployer_account( + get_is_live(), network=network_name, dev_ldo_transfer=False + ) + trusted_caller = _get_trusted_caller() + module_address = _get_module_address() + factory_name = _get_factory_name() + + log.br() + log.nb("Current network", network_name, color_hl=log.color_magenta) + log.ok("Chain ID", chain.id) + log.ok("Deployer", deployer) + log.br() + log.ok("Trusted caller", trusted_caller) + log.ok("Module address", module_address) + log.ok("Factory name", factory_name) + + log.br() + print("Proceed? [yes/no]: ") + if not prompt_bool(): + log.nb("Aborting") + return + + tx_params = {"from": deployer} + if get_is_live(): + tx_params["priority_fee"] = "2 gwei" + tx_params["max_fee"] = "50 gwei" + + constructor_args = ( + trusted_caller, + factory_name, + module_address, + ) + factory = SettleGeneralDelayedPenalty.deploy(*constructor_args, tx_params) + + log.br() + log.ok("Deployed SettleGeneralDelayedPenalty", factory.address) + + if get_is_live(): + entry_key = f"SettleGeneralDelayedPenalty:{factory_name}" + new_entry = { + entry_key: { + "contract": "SettleGeneralDelayedPenalty", + "address": factory.address, + "constructorArgs": constructor_args, + } + } + + artifacts_path = f"deployed-sm-{network_name}.json" + try: + with open(artifacts_path, "r") as prev: + artifacts = json.load(prev) + except FileNotFoundError: + artifacts = {} + + artifacts.update(new_entry) + + with open(artifacts_path, "w") as out: + json.dump(artifacts, out, indent=4) + + if get_env("FORCE_VERIFY", False): + log.ok("Verifying SettleGeneralDelayedPenalty...") + SettleGeneralDelayedPenalty.publish_source(factory) + + log.br() + print("Hit to quit script") + input() + + +def _get_trusted_caller(): + addr = os.environ.get("TRUSTED_CALLER") + if not web3.is_address(addr): + raise ValueError( + f"{addr} is not a valid address, check the TRUSTED_CALLER env variable" + ) + return addr + + +def _get_module_address(): + addr = os.environ.get("MODULE_ADDRESS") + if not web3.is_address(addr): + raise ValueError( + f"{addr} is not a valid address, check the MODULE_ADDRESS env variable" + ) + return addr + + +def _get_factory_name(): + name = os.environ.get("FACTORY_NAME") + if not name: + raise ValueError("Please provide non-empty name via FACTORY_NAME env variable") + return name diff --git a/tests/conftest.py b/tests/conftest.py index a9bd506a..357fc243 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import constants from utils.lido import contracts as lido_contracts_ from utils.csm import contracts as csm_contracts_ +from utils.cm import contracts as cm_contracts_ from utils import deployed_date_time from utils.test_helpers import set_account_balance from utils.submit_exit_requests_test_helpers import MAX_REQUESTS @@ -111,6 +112,11 @@ def csm_contracts(): return csm_contracts_(network=brownie.network.show_active()) +@pytest.fixture(scope="module") +def cm_contracts(): + return cm_contracts_(network=brownie.network.show_active()) + + @pytest.fixture(scope="module") def motion_settings(owner, MotionSettings): return owner.deploy( @@ -431,6 +437,11 @@ def cs_module(csm_contracts): return csm_contracts.module +@pytest.fixture(scope="module") +def curated_module(cm_contracts): + return cm_contracts.curated_module + + @pytest.fixture(scope="module") def voting(lido_contracts): return lido_contracts.aragon.voting diff --git a/tests/evm_script_factories/test_csm_set_vetted_gate_tree.py b/tests/evm_script_factories/test_csm_set_vetted_gate_tree.py deleted file mode 100644 index 5ab96d12..00000000 --- a/tests/evm_script_factories/test_csm_set_vetted_gate_tree.py +++ /dev/null @@ -1,110 +0,0 @@ -import pytest -from brownie import reverts, CSMSetVettedGateTree, VettedGateStub, ZERO_ADDRESS # type: ignore - -from utils.evm_script import encode_call_script, encode_calldata -from utils.test_helpers import set_account_balance - -def create_calldata(tree_root, tree_cid): - return encode_calldata(["bytes32", "string"], [tree_root, tree_cid]) - - -@pytest.fixture(scope="module") -def vetted_gate_stub(owner): - """Create a mock VettedGate contract with setTreeParams method""" - stub = owner.deploy(VettedGateStub) - # Grant SET_TREE_ROLE to owner for testing - set_tree_role = stub.SET_TREE_ROLE() - stub.grantRole(set_tree_role, owner, {"from": owner}) - return stub - - -@pytest.fixture(scope="module") -def csm_set_vetted_gate_tree_factory(owner, vetted_gate_stub): - return CSMSetVettedGateTree.deploy(owner, "IdentifiedCommunityStakerSetTreeParams", vetted_gate_stub, {"from": owner}) - - -def test_deploy(owner, vetted_gate_stub, csm_set_vetted_gate_tree_factory): - """Must deploy contract with correct data""" - assert csm_set_vetted_gate_tree_factory.trustedCaller() == owner - assert csm_set_vetted_gate_tree_factory.vettedGate() == vetted_gate_stub - assert csm_set_vetted_gate_tree_factory.name() == "IdentifiedCommunityStakerSetTreeParams" - - -def test_create_evm_script_called_by_stranger(stranger, csm_set_vetted_gate_tree_factory): - """Must revert with message 'CALLER_IS_FORBIDDEN' if creator isn't trustedCaller""" - EVM_SCRIPT_CALLDATA = "0x" - with reverts("CALLER_IS_FORBIDDEN"): - csm_set_vetted_gate_tree_factory.createEVMScript(stranger, EVM_SCRIPT_CALLDATA) - - -def test_empty_tree_root(owner, csm_set_vetted_gate_tree_factory): - """Must revert with message 'EMPTY_TREE_ROOT' when tree root is empty""" - EMPTY_ROOT_CALLDATA = create_calldata( - b'\x00' * 32, # bytes32 zero value - "test_cid" - ) - with reverts('EMPTY_TREE_ROOT'): - csm_set_vetted_gate_tree_factory.createEVMScript(owner, EMPTY_ROOT_CALLDATA) - - -def test_empty_tree_cid(owner, csm_set_vetted_gate_tree_factory): - """Must revert with message 'EMPTY_TREE_CID' when tree CID is empty""" - EMPTY_CID_CALLDATA = create_calldata( - bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), - "" - ) - with reverts('EMPTY_TREE_CID'): - csm_set_vetted_gate_tree_factory.createEVMScript(owner, EMPTY_CID_CALLDATA) - - -def test_same_tree_root(owner, vetted_gate_stub, csm_set_vetted_gate_tree_factory): - """Must revert with message 'SAME_TREE_ROOT' if tree params are the same as the last set""" - tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - tree_cid = "QmTest123456789" - # Set initial tree params - vetted_gate_stub.setTreeParams(tree_root, tree_cid, {"from": owner}) - - # Create calldata for the same tree root - new_tree_cid = "QmTest1234567890" # Slightly different CID - EVM_SCRIPT_CALLDATA = create_calldata(tree_root, new_tree_cid) - with reverts('SAME_TREE_ROOT'): - csm_set_vetted_gate_tree_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) - -def test_same_tree_cid(owner, vetted_gate_stub, csm_set_vetted_gate_tree_factory): - """Must revert with message 'SAME_TREE_CID' if tree params are the same as the last set""" - tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - tree_cid = "QmTest123456789" - # Set initial tree params - vetted_gate_stub.setTreeParams(tree_root, tree_cid, {"from": owner}) - - # Create calldata for the same CID - new_tree_root = bytes.fromhex("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") - EVM_SCRIPT_CALLDATA = create_calldata(new_tree_root, tree_cid) - with reverts('SAME_TREE_CID'): - csm_set_vetted_gate_tree_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) - - -def test_create_evm_script(owner, csm_set_vetted_gate_tree_factory, vetted_gate_stub): - """Must create correct EVMScript if all requirements are met""" - tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - tree_cid = "QmTest123456789" - - EVM_SCRIPT_CALLDATA = create_calldata(tree_root, tree_cid) - evm_script = csm_set_vetted_gate_tree_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) - expected_evm_script = encode_call_script( - [(vetted_gate_stub.address, vetted_gate_stub.setTreeParams.encode_input(tree_root, tree_cid))] - ) - - assert evm_script == expected_evm_script - - -def test_decode_evm_script_call_data(csm_set_vetted_gate_tree_factory): - """Must decode EVMScript call data correctly""" - tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") - tree_cid = "QmTest123456789" - - EVM_SCRIPT_CALLDATA = create_calldata(tree_root, tree_cid) - decoded_root, decoded_cid = csm_set_vetted_gate_tree_factory.decodeEVMScriptCallData(EVM_SCRIPT_CALLDATA) - - assert decoded_root == "0x" + tree_root.hex() - assert decoded_cid == tree_cid diff --git a/tests/evm_script_factories/test_csm_settle_el_stealing_penalty.py b/tests/evm_script_factories/test_csm_settle_el_stealing_penalty.py deleted file mode 100644 index 069903a3..00000000 --- a/tests/evm_script_factories/test_csm_settle_el_stealing_penalty.py +++ /dev/null @@ -1,81 +0,0 @@ -import pytest -from brownie import reverts, CSMSettleElStealingPenalty, ZERO_ADDRESS # type: ignore - -from utils.evm_script import encode_call_script, encode_calldata -from utils.test_helpers import set_account_balance - -def create_calldata(ids): - return encode_calldata(["uint256[]"], [ids]) - - -@pytest.fixture(scope="module") -def csm_settle_el_stealing_penalty_factory(owner, cs_module): - return CSMSettleElStealingPenalty.deploy(owner, cs_module, {"from": owner}) - -@pytest.fixture() -def fill_cs_module(cs_module, owner): - admin = cs_module.getRoleMember(cs_module.DEFAULT_ADMIN_ROLE(), 0) - set_account_balance(admin) - if cs_module.isPaused(): - cs_module.grantRole(cs_module.RESUME_ROLE(), owner, {"from": admin}) - cs_module.resume({"from": owner}) - if cs_module.getNodeOperatorsCount() == 0: - cs_module.addNodeOperatorETH( - 1, - # some random pubkey and signature - "0x8bb1db218877a42047b953bdc32573445a78d93383ef5fd08f79c066d4781961db4f5ab5a7cc0cf1e4cbcc23fd17f9d7", - "0xad17ef7cdf0c4917aaebc067a785b049d417dda5d4dd66395b21bbd50781d51e28ee750183eca3d32e1f57b324049a06135ad07d1aa243368bca9974e25233f050e0d6454894739f87faace698b90ea65ee4baba2758772e09fec4f1d8d35660", - [ZERO_ADDRESS, ZERO_ADDRESS, False], - [], - ZERO_ADDRESS, - {"from": owner, "value": 32 * 10**18} - ) - - - -def test_deploy(owner, cs_module, csm_settle_el_stealing_penalty_factory): - "Must deploy contract with correct data" - assert csm_settle_el_stealing_penalty_factory.trustedCaller() == owner - assert csm_settle_el_stealing_penalty_factory.csm() == cs_module - - -def test_create_evm_script_called_by_stranger(stranger, csm_settle_el_stealing_penalty_factory): - "Must revert with message 'CALLER_IS_FORBIDDEN' if creator isn't trustedCaller" - EVM_SCRIPT_CALLDATA = "0x" - with reverts("CALLER_IS_FORBIDDEN"): - csm_settle_el_stealing_penalty_factory.createEVMScript(stranger, EVM_SCRIPT_CALLDATA) - - -def test_empty_calldata(owner, csm_settle_el_stealing_penalty_factory): - EMPTY_CALLDATA = create_calldata([]) - with reverts('EMPTY_NODE_OPERATORS_IDS'): - csm_settle_el_stealing_penalty_factory.createEVMScript(owner, EMPTY_CALLDATA) - - -def test_operator_id_out_of_range(owner, csm_settle_el_stealing_penalty_factory, cs_module): - "Must revert with message 'OUT_OF_RANGE_NODE_OPERATOR_ID' when operator id gt operators count" - - node_operators_count = cs_module.getNodeOperatorsCount() - CALLDATA = create_calldata([node_operators_count]) - with reverts('OUT_OF_RANGE_NODE_OPERATOR_ID'): - csm_settle_el_stealing_penalty_factory.createEVMScript(owner, CALLDATA) - -def test_create_evm_script(owner, csm_settle_el_stealing_penalty_factory, cs_module, fill_cs_module): - "Must create correct EVMScript if all requirements are met" - input_params = [0] - - EVM_SCRIPT_CALLDATA = create_calldata(input_params) - evm_script = csm_settle_el_stealing_penalty_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) - expected_evm_script = encode_call_script( - [(cs_module.address, cs_module.settleELRewardsStealingPenalty.encode_input(input_params))] - ) - - assert evm_script == expected_evm_script - - -def test_decode_evm_script_call_data(csm_settle_el_stealing_penalty_factory): - "Must decode EVMScript call data correctly" - input_params = [0, 1, 2] - - EVM_SCRIPT_CALLDATA = create_calldata(input_params) - assert csm_settle_el_stealing_penalty_factory.decodeEVMScriptCallData(EVM_SCRIPT_CALLDATA) == input_params diff --git a/tests/evm_script_factories/test_report_withdrawals_for_slashed_validators.py b/tests/evm_script_factories/test_report_withdrawals_for_slashed_validators.py new file mode 100644 index 00000000..32819a30 --- /dev/null +++ b/tests/evm_script_factories/test_report_withdrawals_for_slashed_validators.py @@ -0,0 +1,291 @@ +from collections import namedtuple +from typing import Iterable + +import pytest +from brownie import ( + CSLikeModuleStub, + ReportWithdrawalsForSlashedValidators, + reverts, +) + +from utils.evm_script import encode_call_script, encode_calldata + +WithdrawnValidatorInfo = namedtuple( + "WithdrawnValidatorInfo", + [ + "no_id", + "key_index", + "exit_balance", + "slashing_penalty", + "is_slashed", + ], +) + +FACTORY_NAME = "MY_LOVELY_FACTORY" + + +def create_calldata(values: Iterable[WithdrawnValidatorInfo]): + return encode_calldata("(uint256,uint256,uint256,uint256,bool)[]", [values]) + + +@pytest.fixture(scope="module") +def module(owner): + module = owner.deploy(CSLikeModuleStub) + module.mock_setNodeOperatorsCount(1000) + return module + + +@pytest.fixture(scope="module") +def factory(owner, module): + return ReportWithdrawalsForSlashedValidators.deploy( + owner, + FACTORY_NAME, + module, + {"from": owner}, + ) + + +def test_deploy(owner, module, factory): + assert factory.trustedCaller() == owner + assert factory.name() == FACTORY_NAME + assert factory.module() == module + + +def test_create_evm_script_reverts_if_called_by_stranger(stranger, factory): + EVM_SCRIPT_CALLDATA = "0x" + with reverts("CALLER_IS_FORBIDDEN"): + factory.createEVMScript(stranger, EVM_SCRIPT_CALLDATA) + + +def test_create_evm_script_reverts_if_empty_withdrawal_list(owner, factory): + EVM_SCRIPT_CALLDATA = create_calldata([]) + with reverts("EMPTY_VALIDATOR_INFO_LIST"): + factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + + +@pytest.mark.parametrize( + "values", + [ + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=1, + key_index=1, + exit_balance=0, + slashing_penalty=1, + is_slashed=True, + ), + ] + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + WithdrawnValidatorInfo( + no_id=1, + key_index=1, + exit_balance=0, + slashing_penalty=1, + is_slashed=True, + ), + ] + ), + ], +) +def test_create_evm_script_reverts_if_zero_exit_balance(owner, factory, values): + EVM_SCRIPT_CALLDATA = create_calldata(values) + with reverts("ZERO_EXIT_BALANCE"): + factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + + +@pytest.mark.parametrize( + "values", + [ + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=1001, + key_index=1, + exit_balance=1, + slashing_penalty=1, + is_slashed=True, + ), + ] + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + WithdrawnValidatorInfo( + no_id=1001, + key_index=3, + exit_balance=30000, + slashing_penalty=0, + is_slashed=True, + ), + ] + ), + ], +) +def test_create_evm_script_reverts_if_non_existing_operator(owner, factory, values): + EVM_SCRIPT_CALLDATA = create_calldata(values) + with reverts("OPERATOR_DOES_NOT_EXIST"): + factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + + +@pytest.mark.parametrize( + "values", + [ + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=1, + exit_balance=1, + slashing_penalty=1, + is_slashed=False, + ), + ] + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=False, + ), + WithdrawnValidatorInfo( + no_id=1, + key_index=3, + exit_balance=30000, + slashing_penalty=0, + is_slashed=False, + ), + ] + ), + ], +) +def test_create_evm_script_reverts_if_not_slashed(owner, factory, values): + EVM_SCRIPT_CALLDATA = create_calldata(values) + with reverts("VALIDATOR_NOT_SLASHED"): + factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + + +@pytest.mark.parametrize( + "values", + [ + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + ], + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + WithdrawnValidatorInfo( + no_id=1, + key_index=3, + exit_balance=30000, + slashing_penalty=0, + is_slashed=True, + ), + ] + ), + ], +) +def test_create_evm_script(owner, factory, module, values): + """Must create correct EVMScript if all requirements are met""" + + EVM_SCRIPT_CALLDATA = create_calldata(values) + evm_script = factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + expected_evm_script = encode_call_script( + [ + ( + module.address, + module.reportSlashedWithdrawnValidators.encode_input(values), + ) + ] + ) + + assert evm_script == expected_evm_script + + +@pytest.mark.parametrize( + "values", + [ + pytest.param([]), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=0, + slashing_penalty=0, + is_slashed=True, + ), + ] + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=1, + key_index=2, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + ] + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + WithdrawnValidatorInfo( + no_id=1, + key_index=3, + exit_balance=30000, + slashing_penalty=0, + is_slashed=True, + ), + ] + ), + ], +) +def test_decode_evm_script_call_data(factory, values): + """Must decode EVMScript call data correctly""" + + EVM_SCRIPT_CALLDATA = create_calldata(values) + decoded_list = factory.decodeEVMScriptCallData(EVM_SCRIPT_CALLDATA) + + assert len(decoded_list) == len(values), "Unexpected length of the decoded list" + for actual, expected in zip(decoded_list, values): + assert actual == expected diff --git a/tests/evm_script_factories/test_set_merkle_gate_tree.py b/tests/evm_script_factories/test_set_merkle_gate_tree.py new file mode 100644 index 00000000..bd5d01c6 --- /dev/null +++ b/tests/evm_script_factories/test_set_merkle_gate_tree.py @@ -0,0 +1,114 @@ +import pytest +from brownie import reverts, SetMerkleGateTree, MerkleGateStub, AllowedMerkleGatesRegistry + +from utils.evm_script import encode_call_script, encode_calldata + + +TEST_FACTORY_NAME = "CSMv3" + +def create_calldata(gate, tree_root, tree_cid): + return encode_calldata(["address", "bytes32", "string"], [gate, tree_root, tree_cid]) + + +@pytest.fixture(scope="module") +def merkle_gate_stub(owner): + """Create a mock MerkleGate contract""" + stub = owner.deploy(MerkleGateStub) + # Grant SET_TREE_ROLE to owner for testing + set_tree_role = stub.SET_TREE_ROLE() + stub.grantRole(set_tree_role, owner, {"from": owner}) + return stub + + +@pytest.fixture(scope="module") +def allowed_gates_registry(owner, merkle_gate_stub): + registry = owner.deploy(AllowedMerkleGatesRegistry, owner) + registry.addGate(merkle_gate_stub, "Test Gate", {"from": owner}) + return registry + + +@pytest.fixture(scope="module") +def set_merkle_gate_tree_factory(owner, allowed_gates_registry): + return SetMerkleGateTree.deploy(owner, TEST_FACTORY_NAME, allowed_gates_registry, {"from": owner}) + + +def test_deploy(owner, allowed_gates_registry, set_merkle_gate_tree_factory): + """Must deploy contract with correct data""" + assert set_merkle_gate_tree_factory.trustedCaller() == owner + assert set_merkle_gate_tree_factory.allowedMerkleGatesRegistry() == allowed_gates_registry + assert set_merkle_gate_tree_factory.name() == TEST_FACTORY_NAME + + +def test_create_evm_script_called_by_stranger(stranger, set_merkle_gate_tree_factory): + """Must revert with message 'CALLER_IS_FORBIDDEN' if creator isn't trustedCaller""" + EVM_SCRIPT_CALLDATA = "0x" + with reverts("CALLER_IS_FORBIDDEN"): + set_merkle_gate_tree_factory.createEVMScript(stranger, EVM_SCRIPT_CALLDATA) + + +def test_empty_tree_root(owner, set_merkle_gate_tree_factory, merkle_gate_stub): + """Must revert with message 'EMPTY_TREE_ROOT' when tree root is empty""" + EMPTY_ROOT_CALLDATA = create_calldata(merkle_gate_stub.address, b'\x00' * 32, "test_cid") + with reverts('EMPTY_TREE_ROOT'): + set_merkle_gate_tree_factory.createEVMScript(owner, EMPTY_ROOT_CALLDATA) + + +def test_empty_tree_cid(owner, set_merkle_gate_tree_factory, merkle_gate_stub): + """Must revert with message 'EMPTY_TREE_CID' when tree CID is empty""" + EMPTY_CID_CALLDATA = create_calldata(merkle_gate_stub.address, bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"), "") + with reverts('EMPTY_TREE_CID'): + set_merkle_gate_tree_factory.createEVMScript(owner, EMPTY_CID_CALLDATA) + + +def test_same_tree_root(owner, merkle_gate_stub, set_merkle_gate_tree_factory): + """Must revert with message 'SAME_TREE_ROOT' if tree params are the same as the last set""" + tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + tree_cid = "QmTest123456789" + # Set initial tree params + merkle_gate_stub.setTreeParams(tree_root, tree_cid, {"from": owner}) + + # Create calldata for the same tree root + new_tree_cid = "QmTest1234567890" # Slightly different CID + EVM_SCRIPT_CALLDATA = create_calldata(merkle_gate_stub.address, tree_root, new_tree_cid) + with reverts('SAME_TREE_ROOT'): + set_merkle_gate_tree_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + +def test_same_tree_cid(owner, merkle_gate_stub, set_merkle_gate_tree_factory): + """Must revert with message 'SAME_TREE_CID' if tree params are the same as the last set""" + tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + tree_cid = "QmTest123456789" + # Set initial tree params + merkle_gate_stub.setTreeParams(tree_root, tree_cid, {"from": owner}) + + # Create calldata for the same CID + new_tree_root = bytes.fromhex("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + EVM_SCRIPT_CALLDATA = create_calldata(merkle_gate_stub.address, new_tree_root, tree_cid) + with reverts('SAME_TREE_CID'): + set_merkle_gate_tree_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + + +def test_create_evm_script(owner, set_merkle_gate_tree_factory, merkle_gate_stub): + """Must create correct EVMScript if all requirements are met""" + tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + tree_cid = "QmTest123456789" + + EVM_SCRIPT_CALLDATA = create_calldata(merkle_gate_stub.address, tree_root, tree_cid) + evm_script = set_merkle_gate_tree_factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + expected_evm_script = encode_call_script( + [(merkle_gate_stub.address, merkle_gate_stub.setTreeParams.encode_input(tree_root, tree_cid))] + ) + + assert evm_script == expected_evm_script + + +def test_decode_evm_script_call_data(set_merkle_gate_tree_factory, merkle_gate_stub): + """Must decode EVMScript call data correctly""" + tree_root = bytes.fromhex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + tree_cid = "QmTest123456789" + + EVM_SCRIPT_CALLDATA = create_calldata(merkle_gate_stub.address, tree_root, tree_cid) + decoded_gate, decoded_root, decoded_cid = set_merkle_gate_tree_factory.decodeEVMScriptCallData(EVM_SCRIPT_CALLDATA) + + assert decoded_gate == merkle_gate_stub.address + assert decoded_root == "0x" + tree_root.hex() + assert decoded_cid == tree_cid diff --git a/tests/evm_script_factories/test_settle_general_delayed_penalty.py b/tests/evm_script_factories/test_settle_general_delayed_penalty.py new file mode 100644 index 00000000..e07f4e8b --- /dev/null +++ b/tests/evm_script_factories/test_settle_general_delayed_penalty.py @@ -0,0 +1,139 @@ +import pytest +from brownie import reverts, SettleGeneralDelayedPenalty, ZERO_ADDRESS # type: ignore + +from utils.evm_script import encode_call_script, encode_calldata +from utils.test_helpers import set_account_balance + + +FACTORY_NAME = "MODULE" +PENALTY_TYPE = "0x" + "11" * 32 + + +def create_calldata(ids, amounts): + return encode_calldata(["uint256[]", "uint256[]"], [ids, amounts]) + + +@pytest.fixture(scope="module", params=["cs_module", "curated_module"]) +def module_case(request): + return request.param + + +@pytest.fixture(scope="module") +def module(request, module_case): + return request.getfixturevalue(module_case) + + +@pytest.fixture(scope="module") +def factory(owner, module): + return SettleGeneralDelayedPenalty.deploy(owner, FACTORY_NAME, module, {"from": owner}) + + +@pytest.fixture() +def fill_module(module, owner): + admin = module.getRoleMember(module.DEFAULT_ADMIN_ROLE(), 0) + set_account_balance(admin) + if module.isPaused(): + module.grantRole(module.RESUME_ROLE(), owner, {"from": admin}) + module.resume({"from": owner}) + if module.getNodeOperatorsCount() == 0: + module.addNodeOperatorETH( + 1, + # some random pubkey and signature + "0x8bb1db218877a42047b953bdc32573445a78d93383ef5fd08f79c066d4781961db4f5ab5a7cc0cf1e4cbcc23fd17f9d7", + "0xad17ef7cdf0c4917aaebc067a785b049d417dda5d4dd66395b21bbd50781d51e28ee750183eca3d32e1f57b324049a06135ad07d1aa243368bca9974e25233f050e0d6454894739f87faace698b90ea65ee4baba2758772e09fec4f1d8d35660", + [ZERO_ADDRESS, ZERO_ADDRESS, False], + [], + ZERO_ADDRESS, + {"from": owner, "value": 32 * 10**18} + ) + reporter = module.getRoleMember(module.REPORT_GENERAL_DELAYED_PENALTY_ROLE(), 0) + module.reportGeneralDelayedPenalty( + 0, + PENALTY_TYPE, + 32 * 10**18, + "test", + {"from": reporter} + ) + + +def test_deploy(owner, module, factory): + "Must deploy contract with correct data" + assert factory.trustedCaller() == owner + assert factory.module() == module + assert factory.name() == FACTORY_NAME + + +def test_create_evm_script_called_by_stranger(stranger, factory): + "Must revert with message 'CALLER_IS_FORBIDDEN' if creator isn't trustedCaller" + EVM_SCRIPT_CALLDATA = "0x" + with reverts("CALLER_IS_FORBIDDEN"): + factory.createEVMScript(stranger, EVM_SCRIPT_CALLDATA) + + +def test_empty_calldata(owner, factory): + EMPTY_CALLDATA = create_calldata([], []) + with reverts("EMPTY_NODE_OPERATORS_IDS"): + factory.createEVMScript(owner, EMPTY_CALLDATA) + + +def test_operator_id_out_of_range(owner, factory, module): + "Must revert with message 'OUT_OF_RANGE_NODE_OPERATOR_ID' when operator id gt operators count" + node_operators_count = module.getNodeOperatorsCount() + CALLDATA = create_calldata([node_operators_count], [1]) + with reverts("OUT_OF_RANGE_NODE_OPERATOR_ID"): + factory.createEVMScript(owner, CALLDATA) + + +def test_create_evm_script(owner, factory, module, fill_module): + "Must create correct EVMScript if all requirements are met" + node_operator_ids = [0] + max_amounts = [1000 * 10 ** 18] + + EVM_SCRIPT_CALLDATA = create_calldata(node_operator_ids, max_amounts) + evm_script = factory.createEVMScript(owner, EVM_SCRIPT_CALLDATA) + expected_evm_script = encode_call_script( + [(module.address, module.settleGeneralDelayedPenalty.encode_input(node_operator_ids, max_amounts))] + ) + + assert evm_script == expected_evm_script + + +def test_decode_evm_script_call_data(factory): + "Must decode EVMScript call data correctly" + node_operator_ids = [0, 1, 2] + max_amounts = [1000, 2000, 3000] + + EVM_SCRIPT_CALLDATA = create_calldata(node_operator_ids, max_amounts) + decoded_ids, decoded_amounts = factory.decodeEVMScriptCallData(EVM_SCRIPT_CALLDATA) + assert decoded_ids == node_operator_ids + assert decoded_amounts == max_amounts + + +def test_node_operators_ids_and_max_amounts_length_mismatch(owner, factory): + "Must revert with message 'NODE_OPERATORS_IDS_AND_MAX_AMOUNTS_LENGTH_MISMATCH' when arrays have different lengths" + node_operator_ids = [0, 1] + max_amounts = [1000] # Different length + + CALLDATA = create_calldata(node_operator_ids, max_amounts) + with reverts("NODE_OPERATORS_IDS_AND_MAX_AMOUNTS_LENGTH_MISMATCH"): + factory.createEVMScript(owner, CALLDATA) + + +def test_max_amount_should_be_greater_than_zero(owner, factory, fill_module): + "Must revert with message 'MAX_AMOUNT_SHOULD_BE_GREATER_THAN_ZERO' when max amount is zero" + node_operator_ids = [0] + max_amounts = [0] # Zero amount + + CALLDATA = create_calldata(node_operator_ids, max_amounts) + with reverts("MAX_AMOUNT_SHOULD_BE_GREATER_THAN_ZERO"): + factory.createEVMScript(owner, CALLDATA) + + +def test_max_amount_should_be_greater_than_actual_locked(owner, factory, module, fill_module): + "Must revert with message 'MAX_AMOUNT_SHOULD_BE_GREATER_OR_EQUAL_THAN_ACTUAL_LOCKED' when max amount is less than actual locked" + node_operator_ids = [0] + max_amounts = [1] + + CALLDATA = create_calldata(node_operator_ids, max_amounts) + with reverts("MAX_AMOUNT_SHOULD_BE_GREATER_OR_EQUAL_THAN_ACTUAL_LOCKED"): + factory.createEVMScript(owner, CALLDATA) diff --git a/tests/scenario/conftest.py b/tests/scenario/conftest.py index 1adda228..42472ecc 100644 --- a/tests/scenario/conftest.py +++ b/tests/scenario/conftest.py @@ -59,6 +59,8 @@ def helper(creator, factory, calldata): ) print("enactment costs: ", etx.gas_used) + return etx + return helper diff --git a/tests/scenario/test_csm_vetted_gate_scenario.py b/tests/scenario/test_merkle_gate_scenario.py similarity index 61% rename from tests/scenario/test_csm_vetted_gate_scenario.py rename to tests/scenario/test_merkle_gate_scenario.py index 8df3f9a1..116a10d9 100644 --- a/tests/scenario/test_csm_vetted_gate_scenario.py +++ b/tests/scenario/test_merkle_gate_scenario.py @@ -2,19 +2,19 @@ from utils.evm_script import encode_calldata -def create_calldata(tree_root, tree_cid): +def create_calldata(gate, tree_root, tree_cid): """Helper function to create encoded calldata for setTreeParams""" - return encode_calldata(["bytes32", "string"], [tree_root, tree_cid]) + return encode_calldata(["address", "bytes32", "string"], [gate, tree_root, tree_cid]) @pytest.fixture(scope="module") -def vetted_gate_stub(owner, et_contracts): +def merkle_gate_stub(owner, et_contracts): """ - Create a mock VettedGate contract with setTreeParams method + Create a mock MerkleGate contract with setTreeParams method and grant SET_TREE_ROLE to the owner for testing. """ - from brownie import VettedGateStub + from brownie import MerkleGateStub - stub = owner.deploy(VettedGateStub) + stub = owner.deploy(MerkleGateStub) # Initial tree parameters initial_tree_root = bytes.fromhex("1111111111111111111111111111111111111111111111111111111111111111") @@ -30,22 +30,26 @@ def vetted_gate_stub(owner, et_contracts): return stub @pytest.fixture(scope="module") -def vetted_gate_set_tree_factory(owner, commitee_multisig, voting, et_contracts, vetted_gate_stub): +def merkle_gate_set_tree_factory(owner, commitee_multisig, voting, et_contracts, merkle_gate_stub): """ - Deploy the CSMSetVettedGateTree factory with the VettedGateStub + Deploy the SetMerkleGateTree factory with the MerkleGateStub """ - from brownie import CSMSetVettedGateTree + from brownie import SetMerkleGateTree, AllowedMerkleGatesRegistry + + # Deploy SetMerkleGateTree factory + # Deploy registry and list the gate + registry = owner.deploy(AllowedMerkleGatesRegistry, owner) + registry.addGate(merkle_gate_stub, "Scenario Gate", {"from": owner}) - # Deploy CSMSetVettedGateTree factory factory = owner.deploy( - CSMSetVettedGateTree, + SetMerkleGateTree, commitee_multisig, # Trusted caller. It should be CSM committee multisig - "IdentifiedCommunityStakerSetTreeParams", - vetted_gate_stub.address + "CSMv3", + registry.address, ) # And add the factory to EasyTrack to activate it. It should be done on CSM v2 voting - permissions = vetted_gate_stub.address + vetted_gate_stub.setTreeParams.signature[2:] + permissions = merkle_gate_stub.address + merkle_gate_stub.setTreeParams.signature[2:] et_contracts.easy_track.addEVMScriptFactory( factory.address, permissions, @@ -55,10 +59,10 @@ def vetted_gate_set_tree_factory(owner, commitee_multisig, voting, et_contracts, return factory -def test_csm_vetted_gate_scenario( +def test_merkle_gate_scenario( commitee_multisig, - vetted_gate_stub, - vetted_gate_set_tree_factory, + merkle_gate_stub, + merkle_gate_set_tree_factory, easytrack_executor, ): tree_updates = [ @@ -78,12 +82,12 @@ def test_csm_vetted_gate_scenario( for update in tree_updates: # Create EVM script for this update - evm_script_calldata = create_calldata(update["root"], update["cid"]) + evm_script_calldata = create_calldata(merkle_gate_stub.address, update["root"], update["cid"]) easytrack_executor( - commitee_multisig, vetted_gate_set_tree_factory, evm_script_calldata + commitee_multisig, merkle_gate_set_tree_factory, evm_script_calldata ) # Verify the update was applied - assert vetted_gate_stub.treeRoot() == "0x" + update["root"].hex() - assert vetted_gate_stub.treeCid() == update["cid"] + assert merkle_gate_stub.treeRoot() == "0x" + update["root"].hex() + assert merkle_gate_stub.treeCid() == update["cid"] diff --git a/tests/scenario/test_report_withdrawals_for_slashed_validators_scenario.py b/tests/scenario/test_report_withdrawals_for_slashed_validators_scenario.py new file mode 100644 index 00000000..ced14c3f --- /dev/null +++ b/tests/scenario/test_report_withdrawals_for_slashed_validators_scenario.py @@ -0,0 +1,105 @@ +from collections import namedtuple +from typing import Iterable + +import pytest +from brownie import ( + CSLikeModuleStub, + ReportWithdrawalsForSlashedValidators, +) + +from utils.evm_script import encode_calldata + +WithdrawnValidatorInfo = namedtuple( + "WithdrawnValidatorInfo", + [ + "no_id", + "key_index", + "exit_balance", + "slashing_penalty", + "is_slashed", + ], +) + + +def create_calldata(values: Iterable[WithdrawnValidatorInfo]): + return encode_calldata("(uint256,uint256,uint256,uint256,bool)[]", [values]) + + +@pytest.fixture(scope="module") +def module(owner): + module = owner.deploy(CSLikeModuleStub) + module.mock_setNodeOperatorsCount(1000) + return module + + +@pytest.fixture(scope="module") +def factory(owner, et_contracts, voting, module): + factory = ReportWithdrawalsForSlashedValidators.deploy( + owner, + "MY_LOVELY_FACTORY", + module, + {"from": owner}, + ) + + permissions = module.address + module.reportSlashedWithdrawnValidators.signature[2:] + et_contracts.easy_track.addEVMScriptFactory( + factory.address, + permissions, + {"from": voting}, + ) + + return factory + + +@pytest.mark.parametrize( + "values", + [ + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + ], + ), + pytest.param( + [ + WithdrawnValidatorInfo( + no_id=0, + key_index=0, + exit_balance=100500, + slashing_penalty=16, + is_slashed=True, + ), + WithdrawnValidatorInfo( + no_id=1, + key_index=3, + exit_balance=30000, + slashing_penalty=0, + is_slashed=True, + ), + ] + ), + ], +) +def test_submit_withdrawals_scenario( + easytrack_executor, + owner, + factory, + values: list[WithdrawnValidatorInfo], +): + """Must create correct EVMScript if all requirements are met""" + + EVM_SCRIPT_CALLDATA = create_calldata(values) + tx = easytrack_executor(owner, factory, EVM_SCRIPT_CALLDATA) + withdrawal_evts: list[dict] = tx.events["GotValidatorInfo"] + assert len(withdrawal_evts) == len(values) + for evt, req in zip(withdrawal_evts, values): + assert evt["info"]["nodeOperatorId"] == req.no_id + assert evt["info"]["keyIndex"] == req.key_index + assert evt["info"]["exitBalance"] == req.exit_balance + assert evt["info"]["slashingPenalty"] == req.slashing_penalty + assert evt["info"]["isSlashed"] == req.is_slashed diff --git a/tests/test_allowed_merkle_gates_registry.py b/tests/test_allowed_merkle_gates_registry.py new file mode 100644 index 00000000..2bd03a15 --- /dev/null +++ b/tests/test_allowed_merkle_gates_registry.py @@ -0,0 +1,115 @@ +import pytest +from brownie import accounts, reverts, ZERO_ADDRESS + +from utils.test_helpers import access_revert_message + + +GATE_TITLE = "New Allowed Merkle Gate" + + +@pytest.fixture(scope="module") +def merkle_gate_with_interface(owner, MerkleGateStub): + return owner.deploy(MerkleGateStub) + + +@pytest.fixture(scope="module") +def allowed_merkle_gates_registry(owner, AllowedMerkleGatesRegistry): + registry = owner.deploy(AllowedMerkleGatesRegistry, owner) + return (registry, owner) + + +def test_registry_initial_state(owner, AllowedMerkleGatesRegistry): + registry = owner.deploy(AllowedMerkleGatesRegistry, owner) + + # Only admin role is set + assert registry.hasRole(registry.DEFAULT_ADMIN_ROLE(), owner) + + # Empty list by default + assert len(registry.getAllowedGates()) == 0 + + +def test_add_gate_success(allowed_merkle_gates_registry, merkle_gate_with_interface): + registry, admin = allowed_merkle_gates_registry + + tx = registry.addGate(merkle_gate_with_interface, GATE_TITLE, {"from": admin}) + + assert registry.isGateAllowed(merkle_gate_with_interface) + assert registry.getAllowedGates()[0] == merkle_gate_with_interface + assert tx.events["GateAdded"]["_gate"] == merkle_gate_with_interface + assert tx.events["GateAdded"]["_title"] == GATE_TITLE + + +def test_add_gate_preserves_insertion_order(allowed_merkle_gates_registry, owner, MerkleGateStub): + registry, admin = allowed_merkle_gates_registry + + gate_list = [] + for _ in range(10): + gate_list.append(owner.deploy(MerkleGateStub)) + + for gate in gate_list: + registry.addGate(gate, GATE_TITLE, {"from": admin}) + + assert registry.getAllowedGates() == gate_list + + +def test_add_gate_duplicate_reverts(allowed_merkle_gates_registry, merkle_gate_with_interface): + registry, admin = allowed_merkle_gates_registry + + registry.addGate(merkle_gate_with_interface, GATE_TITLE, {"from": admin}) + assert registry.isGateAllowed(merkle_gate_with_interface) + + with reverts("GATE_ALREADY_ADDED_TO_ALLOWED_LIST"): + registry.addGate(merkle_gate_with_interface, GATE_TITLE, {"from": admin}) + + +def test_remove_gate_success(allowed_merkle_gates_registry, merkle_gate_with_interface): + registry, admin = allowed_merkle_gates_registry + + registry.addGate(merkle_gate_with_interface, GATE_TITLE, {"from": admin}) + assert registry.isGateAllowed(merkle_gate_with_interface) + + tx = registry.removeGate(merkle_gate_with_interface, {"from": admin}) + assert not registry.isGateAllowed(merkle_gate_with_interface) + assert len(registry.getAllowedGates()) == 0 + assert tx.events["GateRemoved"]["_gate"] == merkle_gate_with_interface + + +def test_remove_not_last_gate_uses_swap_and_pop(allowed_merkle_gates_registry, owner, MerkleGateStub): + registry, admin = allowed_merkle_gates_registry + + gate_list = [] + for _ in range(10): + gate_list.append(owner.deploy(MerkleGateStub)) + + for gate in gate_list: + registry.addGate(gate, GATE_TITLE, {"from": admin}) + + def swap_and_pop(gate_list: list, idx): + gate_list[idx] = gate_list[-1] + gate_list.pop(-1) + + for idx in (0, 1, 3, 4): + registry.removeGate(gate_list[idx], {"from": admin}) + swap_and_pop(gate_list, idx) + + assert registry.getAllowedGates() == gate_list + + +def test_remove_missing_gate_reverts(allowed_merkle_gates_registry, merkle_gate_with_interface): + registry, admin = allowed_merkle_gates_registry + + with reverts("GATE_NOT_FOUND_IN_ALLOWED_LIST"): + registry.removeGate(merkle_gate_with_interface, {"from": admin}) + + +def test_access_control_enforced(owner, stranger, AllowedMerkleGatesRegistry, merkle_gate_with_interface): + registry = owner.deploy(AllowedMerkleGatesRegistry, owner) + + # Only DEFAULT_ADMIN_ROLE can add/remove + for caller in [stranger]: + with reverts(access_revert_message(caller)): + registry.addGate(merkle_gate_with_interface, GATE_TITLE, {"from": caller}) + + for caller in [stranger]: + with reverts(access_revert_message(caller)): + registry.removeGate(merkle_gate_with_interface, {"from": caller}) diff --git a/utils/cm.py b/utils/cm.py new file mode 100644 index 00000000..c239cc9c --- /dev/null +++ b/utils/cm.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +import brownie + +DEFAULT_NETWORK = "mainnet" + + +def addresses(network=DEFAULT_NETWORK): + if network == "mainnet" or network == "mainnet-fork": + return CMAddressesSetup( + module="", + ) + if network == "holesky" or network == "holesky-fork": + return CMAddressesSetup( + module="", + ) + if network == "hoodi" or network == "hoodi-fork": + return CMAddressesSetup( + module="" + ) + raise NameError( + f"Unknown network '{network}'. Supported networks: mainnet, mainnet-fork, hoodi, hoodi-fork, holesky, holesky-fork" + ) + + +def contracts(network=DEFAULT_NETWORK): + return CMContractsSetup(brownie.interface, cm_addresses=addresses(network)) + + +class CMContractsSetup: + def __init__(self, interface, cm_addresses): + self.module = interface.CSModule(cm_addresses.module) + + +@dataclass +class CMAddressesSetup: + module: str