Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/VettedGateFactory.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { VettedGate } from "./VettedGate.sol";
import { OssifiableProxy } from "./lib/proxy/OssifiableProxy.sol";
import { IVettedGateFactory } from "./interfaces/IVettedGateFactory.sol";
contract VettedGateFactory is IVettedGateFactory {
address public immutable VETTED_GATE_IMPL;
constructor(address vettedGateImpl) {
if (vettedGateImpl == address(0)) {
revert ZeroImplementationAddress();
}
VETTED_GATE_IMPL = vettedGateImpl;
}
/// @inheritdoc IVettedGateFactory
function create(
uint256 curveId,
bytes32 treeRoot,
string calldata treeCid,
address admin
) external returns (address instance) {
instance = address(
new OssifiableProxy({
implementation_: VETTED_GATE_IMPL,
data_: new bytes(0),
admin_: admin
})
);
VettedGate(instance).initialize(curveId, treeRoot, treeCid, admin);
emit VettedGateCreated(instance);
}
}
"
},
"src/VettedGate.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { AssetRecoverer } from "./abstract/AssetRecoverer.sol";
import { PausableUntil } from "./lib/utils/PausableUntil.sol";
import { ICSAccounting } from "./interfaces/ICSAccounting.sol";
import { ICSModule, NodeOperatorManagementProperties } from "./interfaces/ICSModule.sol";
import { IVettedGate } from "./interfaces/IVettedGate.sol";
contract VettedGate is
IVettedGate,
AccessControlEnumerableUpgradeable,
PausableUntil,
AssetRecoverer
{
bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE");
bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE");
bytes32 public constant RECOVERER_ROLE = keccak256("RECOVERER_ROLE");
bytes32 public constant SET_TREE_ROLE = keccak256("SET_TREE_ROLE");
bytes32 public constant START_REFERRAL_SEASON_ROLE =
keccak256("START_REFERRAL_SEASON_ROLE");
bytes32 public constant END_REFERRAL_SEASON_ROLE =
keccak256("END_REFERRAL_SEASON_ROLE");
/// @dev Address of the Staking Module
ICSModule public immutable MODULE;
/// @dev Address of the CS Accounting
ICSAccounting public immutable ACCOUNTING;
/// @dev Id of the bond curve to be assigned for the eligible members
uint256 public curveId;
/// @dev Root of the eligible members Merkle Tree
bytes32 public treeRoot;
/// @dev CID of the eligible members Merkle Tree
string public treeCid;
mapping(address => bool) internal _consumedAddresses;
/////////////////////////////////
/// Optional referral program ///
/////////////////////////////////
bool public isReferralProgramSeasonActive;
uint256 public referralProgramSeasonNumber;
/// @dev Id of the bond curve for referral program
uint256 public referralCurveId;
/// @dev Number of referrals required for bond curve claim
uint256 public referralsThreshold;
/// @dev Referral counts for referrers for seasons
mapping(bytes32 => uint256) internal _referralCounts;
mapping(bytes32 => bool) internal _consumedReferrers;
constructor(address module) {
if (module == address(0)) {
revert ZeroModuleAddress();
}
MODULE = ICSModule(module);
ACCOUNTING = ICSAccounting(MODULE.accounting());
_disableInitializers();
}
function initialize(
uint256 _curveId,
bytes32 _treeRoot,
string calldata _treeCid,
address admin
) external initializer {
__AccessControlEnumerable_init();
if (_curveId == ACCOUNTING.DEFAULT_BOND_CURVE_ID()) {
revert InvalidCurveId();
}
// @dev there is no check for curve existence as this contract might be created before the curve is added
curveId = _curveId;
if (admin == address(0)) {
revert ZeroAdminAddress();
}
_setTreeParams(_treeRoot, _treeCid);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
/// @inheritdoc IVettedGate
function resume() external onlyRole(RESUME_ROLE) {
_resume();
}
/// @inheritdoc IVettedGate
function pauseFor(uint256 duration) external onlyRole(PAUSE_ROLE) {
_pauseFor(duration);
}
/// @inheritdoc IVettedGate
function startNewReferralProgramSeason(
uint256 _referralCurveId,
uint256 _referralsThreshold
) external onlyRole(START_REFERRAL_SEASON_ROLE) returns (uint256 season) {
if (isReferralProgramSeasonActive) {
revert ReferralProgramIsActive();
}
if (_referralCurveId == ACCOUNTING.DEFAULT_BOND_CURVE_ID()) {
revert InvalidCurveId();
}
if (_referralsThreshold == 0) {
revert InvalidReferralsThreshold();
}
referralCurveId = _referralCurveId;
referralsThreshold = _referralsThreshold;
isReferralProgramSeasonActive = true;
season = referralProgramSeasonNumber + 1;
referralProgramSeasonNumber = season;
emit ReferralProgramSeasonStarted(
season,
_referralCurveId,
_referralsThreshold
);
}
/// @inheritdoc IVettedGate
function endCurrentReferralProgramSeason()
external
onlyRole(END_REFERRAL_SEASON_ROLE)
{
if (
!isReferralProgramSeasonActive || referralProgramSeasonNumber == 0
) {
revert ReferralProgramIsNotActive();
}
isReferralProgramSeasonActive = false;
emit ReferralProgramSeasonEnded(referralProgramSeasonNumber);
}
/// @inheritdoc IVettedGate
function addNodeOperatorETH(
uint256 keysCount,
bytes calldata publicKeys,
bytes calldata signatures,
NodeOperatorManagementProperties calldata managementProperties,
bytes32[] calldata proof,
address referrer
) external payable whenResumed returns (uint256 nodeOperatorId) {
_consume(proof);
nodeOperatorId = MODULE.createNodeOperator({
from: msg.sender,
managementProperties: managementProperties,
referrer: referrer
});
ACCOUNTING.setBondCurve(nodeOperatorId, curveId);
MODULE.addValidatorKeysETH{ value: msg.value }({
from: msg.sender,
nodeOperatorId: nodeOperatorId,
keysCount: keysCount,
publicKeys: publicKeys,
signatures: signatures
});
_bumpReferralCount(referrer, nodeOperatorId);
}
/// @inheritdoc IVettedGate
function addNodeOperatorStETH(
uint256 keysCount,
bytes calldata publicKeys,
bytes calldata signatures,
NodeOperatorManagementProperties calldata managementProperties,
ICSAccounting.PermitInput calldata permit,
bytes32[] calldata proof,
address referrer
) external whenResumed returns (uint256 nodeOperatorId) {
_consume(proof);
nodeOperatorId = MODULE.createNodeOperator({
from: msg.sender,
managementProperties: managementProperties,
referrer: referrer
});
ACCOUNTING.setBondCurve(nodeOperatorId, curveId);
MODULE.addValidatorKeysStETH({
from: msg.sender,
nodeOperatorId: nodeOperatorId,
keysCount: keysCount,
publicKeys: publicKeys,
signatures: signatures,
permit: permit
});
_bumpReferralCount(referrer, nodeOperatorId);
}
/// @inheritdoc IVettedGate
function addNodeOperatorWstETH(
uint256 keysCount,
bytes calldata publicKeys,
bytes calldata signatures,
NodeOperatorManagementProperties calldata managementProperties,
ICSAccounting.PermitInput calldata permit,
bytes32[] calldata proof,
address referrer
) external whenResumed returns (uint256 nodeOperatorId) {
_consume(proof);
nodeOperatorId = MODULE.createNodeOperator({
from: msg.sender,
managementProperties: managementProperties,
referrer: referrer
});
ACCOUNTING.setBondCurve(nodeOperatorId, curveId);
MODULE.addValidatorKeysWstETH({
from: msg.sender,
nodeOperatorId: nodeOperatorId,
keysCount: keysCount,
publicKeys: publicKeys,
signatures: signatures,
permit: permit
});
_bumpReferralCount(referrer, nodeOperatorId);
}
/// @inheritdoc IVettedGate
function claimBondCurve(
uint256 nodeOperatorId,
bytes32[] calldata proof
) external whenResumed {
_onlyNodeOperatorOwner(nodeOperatorId);
_consume(proof);
ACCOUNTING.setBondCurve(nodeOperatorId, curveId);
}
/// @inheritdoc IVettedGate
function claimReferrerBondCurve(
uint256 nodeOperatorId,
bytes32[] calldata proof
) external whenResumed {
_onlyNodeOperatorOwner(nodeOperatorId);
// @dev Only members from the current merkle tree can claim the referral bond curve
if (!verifyProof(msg.sender, proof)) {
revert InvalidProof();
}
if (!isReferralProgramSeasonActive) {
revert ReferralProgramIsNotActive();
}
uint256 season = referralProgramSeasonNumber;
bytes32 referrer = _seasonedAddress(msg.sender, season);
if (_referralCounts[referrer] < referralsThreshold) {
revert NotEnoughReferrals();
}
if (_consumedReferrers[referrer]) {
revert AlreadyConsumed();
}
_consumedReferrers[referrer] = true;
emit ReferrerConsumed(msg.sender, season);
ACCOUNTING.setBondCurve(nodeOperatorId, referralCurveId);
}
/// @inheritdoc IVettedGate
function setTreeParams(
bytes32 _treeRoot,
string calldata _treeCid
) external onlyRole(SET_TREE_ROLE) {
_setTreeParams(_treeRoot, _treeCid);
}
/// @inheritdoc IVettedGate
function getReferralsCount(
address referrer
) external view returns (uint256) {
return _referralCounts[_seasonedAddress(referrer)];
}
/// @inheritdoc IVettedGate
function getReferralsCount(
address referrer,
uint256 season
) external view returns (uint256) {
return _referralCounts[_seasonedAddress(referrer, season)];
}
/// @inheritdoc IVettedGate
function getInitializedVersion() external view returns (uint64) {
return _getInitializedVersion();
}
/// @inheritdoc IVettedGate
function isReferrerConsumed(address referrer) external view returns (bool) {
return _consumedReferrers[_seasonedAddress(referrer)];
}
/// @inheritdoc IVettedGate
function isConsumed(address member) public view returns (bool) {
return _consumedAddresses[member];
}
/// @inheritdoc IVettedGate
function verifyProof(
address member,
bytes32[] calldata proof
) public view returns (bool) {
return MerkleProof.verifyCalldata(proof, treeRoot, hashLeaf(member));
}
/// @inheritdoc IVettedGate
function hashLeaf(address member) public pure returns (bytes32) {
return keccak256(bytes.concat(keccak256(abi.encode(member))));
}
function _consume(bytes32[] calldata proof) internal {
if (isConsumed(msg.sender)) {
revert AlreadyConsumed();
}
if (!verifyProof(msg.sender, proof)) {
revert InvalidProof();
}
_consumedAddresses[msg.sender] = true;
emit Consumed(msg.sender);
}
function _setTreeParams(
bytes32 _treeRoot,
string calldata _treeCid
) internal {
if (_treeRoot == bytes32(0)) {
revert InvalidTreeRoot();
}
if (_treeRoot == treeRoot) {
revert InvalidTreeRoot();
}
if (bytes(_treeCid).length == 0) {
revert InvalidTreeCid();
}
if (keccak256(bytes(_treeCid)) == keccak256(bytes(treeCid))) {
revert InvalidTreeCid();
}
treeRoot = _treeRoot;
treeCid = _treeCid;
emit TreeSet(_treeRoot, _treeCid);
}
function _bumpReferralCount(
address referrer,
uint256 referralNodeOperatorId
) internal {
uint256 season = referralProgramSeasonNumber;
if (
isReferralProgramSeasonActive &&
referrer != address(0) &&
referrer != msg.sender
) {
_referralCounts[_seasonedAddress(referrer, season)] += 1;
emit ReferralRecorded(referrer, season, referralNodeOperatorId);
}
}
function _seasonedAddress(
address referrer
) internal view returns (bytes32) {
return _seasonedAddress(referrer, referralProgramSeasonNumber);
}
/// @dev Verifies that the sender is the owner of the node operator
function _onlyNodeOperatorOwner(uint256 nodeOperatorId) internal view {
address owner = MODULE.getNodeOperatorOwner(nodeOperatorId);
if (owner == address(0)) {
revert NodeOperatorDoesNotExist();
}
if (owner != msg.sender) {
revert NotAllowedToClaim();
}
}
function _onlyRecoverer() internal view override {
_checkRole(RECOVERER_ROLE);
}
function _seasonedAddress(
address referrer,
uint256 season
) internal pure returns (bytes32) {
return keccak256(abi.encode(referrer, season));
}
}
"
},
"src/lib/proxy/OssifiableProxy.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { StorageSlot } from "@openzeppelin/contracts/utils/StorageSlot.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
/// @notice An ossifiable proxy contract. Extends the ERC1967Proxy contract by
/// adding admin functionality
contract OssifiableProxy is ERC1967Proxy {
event ProxyOssified();
error NotAdmin();
error ProxyIsOssified();
/// @dev Validates that proxy is not ossified and that method is called by the admin
/// of the proxy
modifier onlyAdmin() {
address admin = ERC1967Utils.getAdmin();
if (admin == address(0)) {
revert ProxyIsOssified();
}
if (admin != msg.sender) {
revert NotAdmin();
}
_;
}
/// @dev Initializes the upgradeable proxy with the initial implementation and admin
/// @param implementation_ Address of the implementation
/// @param admin_ Address of the admin of the proxy
/// @param data_ Data used in a delegate call to implementation. The delegate call will be
/// skipped if the data is empty bytes
constructor(
address implementation_,
address admin_,
bytes memory data_
) ERC1967Proxy(implementation_, data_) {
ERC1967Utils.changeAdmin(admin_);
}
/// @notice Fallback function that delegates calls to the address returned by `_implementation()`.
// Will run if call data is empty.
// The only use of this function is to suppress the solidity warning "This contract has a payable fallback function, but no receive ether function"
// See https://forum.openzeppelin.com/t/proxy-sol-fallback/36951/7 for details
// Previously it was implemented in the Proxy contract, but it was removed in the OZ 5.0
receive() external payable virtual {
_fallback();
}
/// @notice Allows to transfer admin rights to zero address and prevent future
/// upgrades of the proxy
// solhint-disable-next-line func-name-mixedcase
function proxy__ossify() external onlyAdmin {
address prevAdmin = ERC1967Utils.getAdmin();
StorageSlot.getAddressSlot(ERC1967Utils.ADMIN_SLOT).value = address(0);
emit ERC1967Utils.AdminChanged(prevAdmin, address(0));
emit ProxyOssified();
}
/// @notice Changes the admin of the proxy
/// @param newAdmin_ Address of the new admin
// solhint-disable-next-line func-name-mixedcase
function proxy__changeAdmin(address newAdmin_) external onlyAdmin {
ERC1967Utils.changeAdmin(newAdmin_);
}
/// @notice Upgrades the implementation of the proxy
/// @param newImplementation_ Address of the new implementation
// solhint-disable-next-line func-name-mixedcase
function proxy__upgradeTo(address newImplementation_) external onlyAdmin {
ERC1967Utils.upgradeToAndCall(newImplementation_, bytes(""));
}
/// @notice Upgrades the proxy to a new implementation, optionally performing an additional
/// setup call.
/// @param newImplementation_ Address of the new implementation
/// @param setupCalldata_ Data for the setup call. The call is skipped if setupCalldata_ is empty
// solhint-disable-next-line func-name-mixedcase
function proxy__upgradeToAndCall(
address newImplementation_,
bytes calldata setupCalldata_
) external onlyAdmin {
ERC1967Utils.upgradeToAndCall(newImplementation_, setupCalldata_);
}
/// @notice Returns the current admin of the proxy
// solhint-disable-next-line func-name-mixedcase
function proxy__getAdmin() external view returns (address) {
return ERC1967Utils.getAdmin();
}
/// @notice Returns the current implementation address
// solhint-disable-next-line func-name-mixedcase
function proxy__getImplementation() external view returns (address) {
return _implementation();
}
/// @notice Returns whether the implementation is locked forever
// solhint-disable-next-line func-name-mixedcase
function proxy__getIsOssified() external view returns (bool) {
return ERC1967Utils.getAdmin() == address(0);
}
}
"
},
"src/interfaces/IVettedGateFactory.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
interface IVettedGateFactory {
event VettedGateCreated(address indexed gate);
error ZeroImplementationAddress();
/// @dev address of the VettedGate implementation to be used for the new instances
/// @return address of the VettedGate implementation
function VETTED_GATE_IMPL() external view returns (address);
/// @dev Creates a new VettedGate instance behind the OssifiableProxy based on known implementation address
/// @param curveId Id of the bond curve to be assigned for the eligible members
/// @param treeRoot Root of the eligible members Merkle Tree
/// @param treeCid CID of the eligible members Merkle Tree
/// @param admin Address of the admin role
function create(
uint256 curveId,
bytes32 treeRoot,
string calldata treeCid,
address admin
) external returns (address instance);
}
"
},
"node_modules/@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/AccessControlEnumerable.sol)
pragma solidity ^0.8.20;
import {IAccessControlEnumerable} from "@openzeppelin/contracts/access/extensions/IAccessControlEnumerable.sol";
import {AccessControlUpgradeable} from "../AccessControlUpgradeable.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {Initializable} from "../../proxy/utils/Initializable.sol";
/**
* @dev Extension of {AccessControl} that allows enumerating the members of each role.
*/
abstract contract AccessControlEnumerableUpgradeable is Initializable, IAccessControlEnumerable, AccessControlUpgradeable {
using EnumerableSet for EnumerableSet.AddressSet;
/// @custom:storage-location erc7201:openzeppelin.storage.AccessControlEnumerable
struct AccessControlEnumerableStorage {
mapping(bytes32 role => EnumerableSet.AddressSet) _roleMembers;
}
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.AccessControlEnumerable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant AccessControlEnumerableStorageLocation = 0xc1f6fe24621ce81ec5827caf0253cadb74709b061630e6b55e82371705932000;
function _getAccessControlEnumerableStorage() private pure returns (AccessControlEnumerableStorage storage $) {
assembly {
$.slot := AccessControlEnumerableStorageLocation
}
}
function __AccessControlEnumerable_init() internal onlyInitializing {
}
function __AccessControlEnumerable_init_unchained() internal onlyInitializing {
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IAccessControlEnumerable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* @dev Returns one of the accounts that have `role`. `index` must be a
* value between 0 and {getRoleMemberCount}, non-inclusive.
*
* Role bearers are not sorted in any particular way, and their ordering may
* change at any point.
*
* WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure
* you perform all queries on the same block. See the following
* https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post]
* for more information.
*/
function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
return $._roleMembers[role].at(index);
}
/**
* @dev Returns the number of accounts that have `role`. Can be used
* together with {getRoleMember} to enumerate all bearers of a role.
*/
function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
return $._roleMembers[role].length();
}
/**
* @dev Overload {AccessControl-_grantRole} to track enumerable memberships
*/
function _grantRole(bytes32 role, address account) internal virtual override returns (bool) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
bool granted = super._grantRole(role, account);
if (granted) {
$._roleMembers[role].add(account);
}
return granted;
}
/**
* @dev Overload {AccessControl-_revokeRole} to track enumerable memberships
*/
function _revokeRole(bytes32 role, address account) internal virtual override returns (bool) {
AccessControlEnumerableStorage storage $ = _getAccessControlEnumerableStorage();
bool revoked = super._revokeRole(role, account);
if (revoked) {
$._roleMembers[role].remove(account);
}
return revoked;
}
}
"
},
"node_modules/@openzeppelin/contracts/utils/cryptography/MerkleProof.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MerkleProof.sol)
pragma solidity ^0.8.20;
/**
* @dev These functions deal with verification of Merkle Tree proofs.
*
* The tree and the proofs can be generated using our
* https://github.com/OpenZeppelin/merkle-tree[JavaScript library].
* You will find a quickstart guide in the readme.
*
* WARNING: You should avoid using leaf values that are 64 bytes long prior to
* hashing, or use a hash function other than keccak256 for hashing leaves.
* This is because the concatenation of a sorted pair of internal nodes in
* the Merkle tree could be reinterpreted as a leaf value.
* OpenZeppelin's JavaScript library generates Merkle trees that are safe
* against this attack out of the box.
*/
library MerkleProof {
/**
*@dev The multiproof provided is not valid.
*/
error MerkleProofInvalidMultiproof();
/**
* @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
* defined by `root`. For this, a `proof` must be provided, containing
* sibling hashes on the branch from the leaf to the root of the tree. Each
* pair of leaves and each pair of pre-images are assumed to be sorted.
*/
function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProof(proof, leaf) == root;
}
/**
* @dev Calldata version of {verify}
*/
function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
return processProofCalldata(proof, leaf) == root;
}
/**
* @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
* from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
* hash matches the root of the tree. When processing the proof, the pairs
* of leafs & pre-images are assumed to be sorted.
*/
function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Calldata version of {processProof}
*/
function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
bytes32 computedHash = leaf;
for (uint256 i = 0; i < proof.length; i++) {
computedHash = _hashPair(computedHash, proof[i]);
}
return computedHash;
}
/**
* @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
* `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*/
function multiProofVerify(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProof(proof, proofFlags, leaves) == root;
}
/**
* @dev Calldata version of {multiProofVerify}
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*/
function multiProofVerifyCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32 root,
bytes32[] memory leaves
) internal pure returns (bool) {
return processMultiProofCalldata(proof, proofFlags, leaves) == root;
}
/**
* @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
* proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
* leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
* respectively.
*
* CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
* is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
* tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
*/
function processMultiProof(
bytes32[] memory proof,
bool[] memory proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofLen = proof.length;
uint256 totalHashes = proofFlags.length;
// Check proof validity.
if (leavesLen + proofLen != totalHashes + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < totalHashes; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = _hashPair(a, b);
}
if (totalHashes > 0) {
if (proofPos != proofLen) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[totalHashes - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Calldata version of {processMultiProof}.
*
* CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
*/
function processMultiProofCalldata(
bytes32[] calldata proof,
bool[] calldata proofFlags,
bytes32[] memory leaves
) internal pure returns (bytes32 merkleRoot) {
// This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
// consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
// `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
// the Merkle tree.
uint256 leavesLen = leaves.length;
uint256 proofLen = proof.length;
uint256 totalHashes = proofFlags.length;
// Check proof validity.
if (leavesLen + proofLen != totalHashes + 1) {
revert MerkleProofInvalidMultiproof();
}
// The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
// `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
bytes32[] memory hashes = new bytes32[](totalHashes);
uint256 leafPos = 0;
uint256 hashPos = 0;
uint256 proofPos = 0;
// At each step, we compute the next hash using two values:
// - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
// get the next hash.
// - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
// `proof` array.
for (uint256 i = 0; i < totalHashes; i++) {
bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
bytes32 b = proofFlags[i]
? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
: proof[proofPos++];
hashes[i] = _hashPair(a, b);
}
if (totalHashes > 0) {
if (proofPos != proofLen) {
revert MerkleProofInvalidMultiproof();
}
unchecked {
return hashes[totalHashes - 1];
}
} else if (leavesLen > 0) {
return leaves[0];
} else {
return proof[0];
}
}
/**
* @dev Sorts the pair (a, b) and hashes the result.
*/
function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
}
/**
* @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory.
*/
function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, a)
mstore(0x20, b)
value := keccak256(0x00, 0x40)
}
}
}
"
},
"src/abstract/AssetRecoverer.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { AssetRecovererLib } from "../lib/AssetRecovererLib.sol";
/// @title AssetRecoverer
/// @dev Abstract contract providing mechanisms for recovering various asset types (ETH, ERC20, ERC721, ERC1155) from a contract.
/// This contract is designed to allow asset recovery by an authorized address by implementing the onlyRecovererRole guardian
/// @notice Assets can be sent only to the `msg.sender`
abstract contract AssetRecoverer {
/// @dev Allows sender to recover Ether held by the contract
/// Emits an EtherRecovered event upon success
function recoverEther() external {
_onlyRecoverer();
AssetRecovererLib.recoverEther();
}
/// @dev Allows sender to recover ERC20 tokens held by the contract
/// @param token The address of the ERC20 token to recover
/// @param amount The amount of the ERC20 token to recover
/// Emits an ERC20Recovered event upon success
/// Optionally, the inheriting contract can override this function to add additional restrictions
function recoverERC20(address token, uint256 amount) external virtual {
_onlyRecoverer();
AssetRecovererLib.recoverERC20(token, amount);
}
/// @dev Allows sender to recover ERC721 tokens held by the contract
/// @param token The address of the ERC721 token to recover
/// @param tokenId The token ID of the ERC721 token to recover
/// Emits an ERC721Recovered event upon success
function recoverERC721(address token, uint256 tokenId) external {
_onlyRecoverer();
AssetRecovererLib.recoverERC721(token, tokenId);
}
/// @dev Allows sender to recover ERC1155 tokens held by the contract.
/// @param token The address of the ERC1155 token to recover.
/// @param tokenId The token ID of the ERC1155 token to recover.
/// Emits an ERC1155Recovered event upon success.
function recoverERC1155(address token, uint256 tokenId) external {
_onlyRecoverer();
AssetRecovererLib.recoverERC1155(token, tokenId);
}
/// @dev Guardian to restrict access to the recover methods.
/// Should be implemented by the inheriting contract
function _onlyRecoverer() internal view virtual;
}
"
},
"src/lib/utils/PausableUntil.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { UnstructuredStorage } from "../UnstructuredStorage.sol";
contract PausableUntil {
using UnstructuredStorage for bytes32;
/// Contract resume/pause control storage slot
bytes32 internal constant RESUME_SINCE_TIMESTAMP_POSITION =
keccak256("lido.PausableUntil.resumeSinceTimestamp");
/// Special value for the infinite pause
uint256 public constant PAUSE_INFINITELY = type(uint256).max;
/// @notice Emitted when paused by the `pauseFor` or `pauseUntil` call
event Paused(uint256 duration);
/// @notice Emitted when resumed by the `resume` call
event Resumed();
error ZeroPauseDuration();
error PausedExpected();
error ResumedExpected();
error PauseUntilMustBeInFuture();
/// @notice Reverts when resumed
modifier whenPaused() {
_checkPaused();
_;
}
/// @notice Reverts when paused
modifier whenResumed() {
_checkResumed();
_;
}
/// @notice Returns one of:
/// - PAUSE_INFINITELY if paused infinitely returns
/// - first second when get contract get resumed if paused for specific duration
/// - some timestamp in past if not paused
function getResumeSinceTimestamp() external view returns (uint256) {
return RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256();
}
/// @notice Returns whether the contract is paused
function isPaused() public view returns (bool) {
return
block.timestamp <
RESUME_SINCE_TIMESTAMP_POSITION.getStorageUint256();
}
function _resume() internal {
_checkPaused();
RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(block.timestamp);
emit Resumed();
}
function _pauseFor(uint256 duration) internal {
_checkResumed();
if (duration == 0) {
revert ZeroPauseDuration();
}
uint256 resumeSince;
if (duration == PAUSE_INFINITELY) {
resumeSince = PAUSE_INFINITELY;
} else {
resumeSince = block.timestamp + duration;
}
_setPausedState(resumeSince);
}
function _pauseUntil(uint256 pauseUntilInclusive) internal {
_checkResumed();
if (pauseUntilInclusive < block.timestamp) {
revert PauseUntilMustBeInFuture();
}
uint256 resumeSince;
if (pauseUntilInclusive != PAUSE_INFINITELY) {
resumeSince = pauseUntilInclusive + 1;
} else {
resumeSince = PAUSE_INFINITELY;
}
_setPausedState(resumeSince);
}
function _setPausedState(uint256 resumeSince) internal {
RESUME_SINCE_TIMESTAMP_POSITION.setStorageUint256(resumeSince);
if (resumeSince == PAUSE_INFINITELY) {
emit Paused(PAUSE_INFINITELY);
} else {
emit Paused(resumeSince - block.timestamp);
}
}
function _checkPaused() internal view {
if (!isPaused()) {
revert PausedExpected();
}
}
function _checkResumed() internal view {
if (isPaused()) {
revert ResumedExpected();
}
}
}
"
},
"src/interfaces/ICSAccounting.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { ICSBondCore } from "./ICSBondCore.sol";
import { ICSBondCurve } from "./ICSBondCurve.sol";
import { ICSBondLock } from "./ICSBondLock.sol";
import { ICSFeeDistributor } from "./ICSFeeDistributor.sol";
import { IAssetRecovererLib } from "../lib/AssetRecovererLib.sol";
import { ICSModule } from "./ICSModule.sol";
interface ICSAccounting is
ICSBondCore,
ICSBondCurve,
ICSBondLock,
IAssetRecovererLib
{
struct PermitInput {
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
event BondLockCompensated(uint256 indexed nodeOperatorId, uint256 amount);
event ChargePenaltyRecipientSet(address chargePenaltyRecipient);
error SenderIsNotModule();
error SenderIsNotEligible();
error ZeroModuleAddress();
error ZeroAdminAddress();
error ZeroFeeDistributorAddress();
error ZeroChargePenaltyRecipientAddress();
error NodeOperatorDoesNotExist();
error ElRewardsVaultReceiveFailed();
error InvalidBondCurvesLength();
function PAUSE_ROLE() external view returns (bytes32);
function RESUME_ROLE() external view returns (bytes32);
function MANAGE_BOND_CURVES_ROLE() external view returns (bytes32);
function SET_BOND_CURVE_ROLE() external view returns (bytes32);
function RECOVERER_ROLE() external view returns (bytes32);
function MODULE() external view returns (ICSModule);
function FEE_DISTRIBUTOR() external view returns (ICSFeeDistributor);
function feeDistributor() external view returns (ICSFeeDistributor);
function chargePenaltyRecipient() external view returns (address);
/// @notice Get the initialized version of the contract
function getInitializedVersion() external view returns (uint64);
/// @notice Resume reward claims and deposits
function resume() external;
/// @notice Pause reward claims and deposits for `duration` seconds
/// @dev Must be called together with `CSModule.pauseFor`
/// @dev Passing MAX_UINT_256 as `duration` pauses indefinitely
/// @param duration Duration of the pause in seconds
function pauseFor(uint256 duration) external;
/// @notice Set charge recipient address
/// @param _chargePenaltyRecipient Charge recipient address
function setChargePenaltyRecipient(
address _chargePenaltyRecipient
) external;
/// @notice Set bond lock period
/// @param period Period in seconds to retain bond lock
function setBondLockPeriod(uint256 period) external;
/// @notice Add a new bond curve
/// @param bondCurve Bond curve definition to add
/// @return id Id of the added curve
function addBondCurve(
BondCurveIntervalInput[] calldata bondCurve
) external returns (uint256 id);
/// @notice Update existing bond curve
/// @dev If the curve is updated to a curve with higher values for any point,
/// Extensive checks and actions should be performed by the method caller to avoid
/// inconsistency in the keys accounting. A manual update of the depositable validators count
/// in CSM might be required to ensure that the keys pointers are consistent.
/// @param curveId Bond curve ID to update
/// @param bondCurve Bond curve definition
function updateBondCurve(
uint256 curveId,
BondCurveIntervalInput[] calldata bondCurve
) external;
/// @notice Get the required bond in ETH (inc. missed and excess) for the given Node Operator to upload new deposit data
/// @param nodeOperatorId ID of the Node Operator
/// @param additionalKeys Number of new keys to add
/// @return Required bond amount in ETH
function getRequiredBondForNextKeys(
uint256 nodeOperatorId,
uint256 additionalKeys
) external view returns (uint256);
/// @notice Get the bond amount in wstETH required for the `keysCount` keys using the default bond curve
/// @param keysCount Keys count to calculate the required bond amount
/// @param curveId Id of the curve to perform calculations against
/// @return wstETH amount required for the `keysCount`
function getBondAmountByKeysCountWstETH(
uint256 keysCount,
uint256 curveId
) external view returns (uint256);
/// @notice Get the required bond in wstETH (inc. missed and excess) for the given Node Operator to upload new keys
/// @param nodeOperatorId ID of the Node Operator
/// @param additionalKeys Number of new keys to add
/// @return Required bond in wstETH
function getRequiredBondForNextKeysWstETH(
uint256 nodeOperatorId,
uint256 additionalKeys
) external view returns (uint256);
/// @notice Get the number of the unbonded keys
/// @param nodeOperatorId ID of the Node Operator
/// @return Unbonded keys count
function getUnbondedKeysCount(
uint256 nodeOperatorId
) external view returns (uint256);
/// @notice Get the number of the unbonded keys to be ejected using a forcedTargetLimit
/// Locked bond is not considered for this calculation to allow Node Operators to
/// compensate the locked bond via `compensateLockedBondETH` method before the ejection happens
/// @param nodeOperatorId ID of the Node Operator
/// @return Unbonded keys count
function getUnbondedKeysCountToEject(
uint256 nodeOperatorId
) external view returns (uint256);
/// @notice Get current and required bond amounts in ETH (stETH) for the given Node Operator
/// @dev To calculate excess bond amount subtract `required` from `current` value.
/// To calculate missed bond amount subtract `current` from `required` value
/// @param nodeOperatorId ID of the Node Operator
/// @return current Current bond amount in ETH
/// @return required Required bond amount in ETH
function getBondSummary(
uint256 nodeOperatorId
) external view returns (uint256 current, uint256 required);
/// @notice Get current and required bond amounts in stETH shares for the given Node Operator
/// @dev To calculate excess bond amount subtract `required` from `current` value.
/// To calculate missed bond amount subtract `current` from `required` value
/// @param nodeOperatorId ID of the Node Operator
/// @return current Current bond amount in stETH shares
/// @return required Required bond amount in stETH shares
function getBondSummaryShares(
uint256 nodeOperatorId
) external view returns (uint256 current, uint256 required);
/// @notice Get current claimable bond in stETH shares for the given Node Operator
/// @param nodeOperatorId ID of the Node Operator
/// @return Current claimable bond in stETH shares
function getClaimableBondShares(
uint256 nodeOperatorId
) external view returns (uint256);
/// @notice Get current claimable bond in stETH shares for the given Node Operator
/// Includes potential rewards distributed by the Fee Distributor
/// @param nodeOperatorId ID of the Node Operator
/// @param cumulativeFeeShares Cumulative fee stETH shares for the Node Operator
/// @param rewardsProof Merkle proof of the rewards
/// @return Current claimable bond in stETH shares
function getClaimableRewardsAndBondShares(
uint256 nodeOperatorId,
uint256 cumulativeFeeShares,
bytes32[] calldata rewardsProof
) external view returns (uint256);
/// @notice Unwrap the user's wstETH and deposit stETH to the bond for the given Node Operator
/// @dev Called by CSM exclusively. CSM should check node operator existence and update depositable validators count
/// @param from Address to unwrap wstETH from
/// @param nodeOperatorId ID of the Node Operator
/// @param wstETHAmount Amount of wstETH to deposit
/// @param permit wstETH permit for the contract
function depositWstETH(
address from,
uint256 nodeOperatorId,
uint256 wstETHAmount,
PermitInput calldata permit
) external;
/// @notice Unwrap the user's wstETH and deposit stETH to the bond for the given Node Operator
/// @dev Permissionless. Enqueues Node Operator's keys if needed
/// @param nodeOperatorId ID of the Node Operator
/// @param wstETHAmount Amount of wstETH to deposit
/// @param permit wstETH permit for the contract
function depositWstETH(
uint256 nodeOperatorId,
uint256 wstETHAmount,
PermitInput calldata permit
) external;
/// @notice Deposit user's stETH to the bond for the given Node Operator
/// @dev Called by CSM exclusively. CSM should check node operator existence and update depositable validators count
/// @param from Address to deposit stETH from.
/// @param nodeOperatorId ID of the Node Operator
/// @param stETHAmount Amount of stETH to deposit
/// @param permit stETH permit for the contract
function depositStETH(
address from,
uint256 nodeOperatorId,
uint256 stETHAmount,
PermitInput calldata permit
) external;
/// @notice Deposit user's stETH to the bond for the given Node Operator
/// @dev Permissionless. Enqueues Node Operator's keys if needed
/// @param nodeOperatorId ID of the Node Operator
/// @param stETHAmount Amount of stETH to deposit
/// @param permit stETH permit for the contract
function depositStETH(
uint256 nodeOperatorId,
uint256 stETHAmount,
PermitInput calldata permit
) external;
/// @notice Stake user's ETH with Lido and deposit stETH to the bond
/// @dev Called by CSM exclusively. CSM should check node operator existence and update depositable validators count
/// @param from Address to stake ETH and deposit stETH from
/// @param nodeOperatorId ID of the Node Operator
function depositETH(address from, uint256 nodeOperatorId) external payable;
/// @notice Stake user's ETH with Lido and deposit stETH to the bond
/// @dev Permissionless. Enqueues Node Operator's keys if needed
/// @param nodeOperatorId ID of the Node Operator
function depositETH(uint256 nodeOperatorId) external payable;
/// @notice Claim full reward (fee + bond) in stETH for the given Node Operator with desirable value.
/// `rewardsProof` and `cumulativeFeeShares` might be empty in order to claim only excess bond
/// @param nodeOperatorId ID of the Node Operator
/// @param stETHAmount Amount of stETH to claim
/// @param cumulativeFeeShares Cumulative fee stETH shares for the Node Operator
/// @param rewardsProof Merkle proof of the rewards
/// @return shares Amount of stETH shares claimed
/// @dev It's impossible to use single-leaf proof via this method, so this case should be treated carefully by
/// off-chain tooling, e.g. to make sure a tree has at least 2 leafs.
function claimRewardsStETH(
uint256 nodeOperatorId,
uint256 stETHAmount,
uint256 cumulativeFeeShares,
bytes32[] calldata rewardsProof
) external returns (uint256 shares);
/// @notice Claim full reward (fee + bond) in wstETH for the given Node Operator available for this moment.
/// `rewardsProof` and `cumulativeFeeShares` might be empty in order to claim only excess bond
/// @param nodeOperatorId ID of the Node Operator
/// @param wstETHAmount Amount of wstETH to claim
/// @param cumulativeFeeShares Cumulative fee stETH shares for the Node Operator
/// @param rewardsProof Merkle proof of the rewards
/// @return claimedWstETHAmount Amount of wstETH claimed
/// @dev It's impossible to use single-leaf proof via this method, so this case should be treated carefully by
/// off-chain tooling, e.g. to make sure a tree has at least 2 leafs.
function claimRewardsWstETH(
uint256 nodeOperatorId,
uint256 wstETHAmount,
uint256 cumulativeFeeShares,
bytes32[] calldata rewardsProof
) external returns (uint256 claimedWstETHAmount);
/// @notice Request full reward (fee + bond) in Withdrawal NFT (unstETH) for the given Node Operator available for this moment.
/// `rewardsProof` and `cumulativeFeeShares` might be empty in order to claim only excess bond
/// @dev Reverts if amount isn't between `MIN_STETH_WITHDRAWAL_AMOUNT` and `MAX_STETH_WITHDRAWAL_AMOUNT`
/// @param nodeOperatorId ID of the Node Operator
/// @param stETHAmount Amount of ETH to request
/// @param cumulativeFeeShares Cumulative fee stETH shares for the Node Operator
/// @param rewardsProof Merkle proof of the rewards
/// @return requestId Withdrawal NFT ID
/// @dev It's impossible to use single-leaf proof via this method, so this case should be treated carefully by
/// off-chain tooling, e.g. to make sure a tree has at least 2 leafs.
function claimRewardsUnstETH(
uint256 nodeOperatorId,
uint256 stETHAmount,
uint256 cumulativeFeeShares,
bytes32[] calldata rewardsProof
) external returns (uint256 requestId);
/// @notice Lock bond in ETH for the given Node Operator
/// @dev Called by CSM exclusively
/// @param nodeOperatorId ID of the Node Operator
/// @param amount Amount to lock in ETH (stETH)
function lockBondETH(uint256 nodeOperatorId, uint256 amount) external;
/// @notice Release locked bond in ETH for the given Node Operator
/// @dev Called by CSM exclusively
/// @param nodeOperatorId ID of the Node Operator
/// @param amount Amount to release in ETH (stETH)
function releaseLockedBondETH(
uint256 nodeOperatorId,
uint256 amount
) external;
/// @notice Settle locked bond ETH for the given Node Operator
/// @dev Called by CSM exclusively
/// @param nodeOperatorId ID of the Node Operator
function settleLockedBondETH(
uint256 nodeOperatorId
) external returns (bool);
/// @notice Compensate locked bond ETH for the given Node Operator
/// @dev Called by CSM exclusively
/// @param nodeOperatorId ID of the Node Operator
function compensateLockedBondETH(uint256 nodeOperatorId) external payable;
/// @notice Set the bond curve for the given Node Operator
/// @dev Updates depositable validators count in CSM to ensure key pointers consistency
/// @param nodeOperatorId ID of the Node Operator
/// @param curveId ID of the bond curve to set
function setBondCurve(uint256 nodeOperatorId, uint256 curveId) external;
/// @notice Penalize bond by burning stETH shares of the given Node Operator
/// @dev Penalty application has a priority over the locked bond.
/// Method call can result in the remaining bond being lower than the locked bond.
/// @param nodeOperatorId ID of the Node Operator
/// @param amount Amount to penalize in ETH (stETH)
function penalize(uint256 nodeOperatorId, uint256 amount) external;
/// @notice Charge fee from bond by transferring stETH shares of the given Node Operator to the charge recipient
/// @dev Charge confiscation has a priority over the locked bond.
/// Method call can result in the remaining bond being lower than the locked bond.
/// @param nodeOperatorId ID of the Node Operator
/// @param amount Amount to charge in ETH (stETH)
function chargeFee(uint256 nodeOperatorId, uint256 amount) external;
/// @notice Pull fees from CSFeeDistributor to the Node Operator's bond
/// @dev Permissionless method. Can be called before penalty application to ensure that rewards are also penalized
/// @param nodeOperatorId ID of the Node Operator
/// @param cumulativeFeeShares Cumulative fee stETH shares for the Node Operator
/// @param rewardsProof Merkle proof of the rewards
function pullFeeRewards(
uint256 nodeOperatorId,
uint256 cumulativeFeeShares,
bytes32[] calldata rewardsProof
) external;
/// @notice Service method to update allowance to Burner in case it has changed
function renewBurnerAllowance() external;
}
"
},
"src/interfaces/ICSModule.sol": {
"content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;
import { IStakingModule } from "./IStakingModule.sol";
import { ICSAccounting } from "./ICSAccounting.sol";
import { IQueueLib } from "../lib/QueueLib.sol";
import { INOAddresses } from "../lib/NOAddresses.sol";
import { IAssetRecovererLib } from "../lib/AssetRecovererLib.sol";
import { Batch } from "../lib/QueueLib.sol";
import { ILidoLocator } from "./ILidoLocator.sol";
import { IStETH } from "./IStETH.sol";
import { ICSParametersRegistry } from "./ICSParametersRegistry.sol";
import { ICSExitPenalties } from "./ICSExitPenalties.sol";
struct NodeOperator {
// All the counters below are used together e.g. in the _updateDepositableValidatorsCount
/* 1 */ uint32 totalAddedKeys; // @dev increased and decreased when removed
/* 1 */ uint32 totalWithdrawnKeys; // @dev only increased
/* 1 */ uint32 totalDepositedKeys; // @dev only increased
/* 1 */ uint32 totalVettedKeys; // @dev both increased and decreased
/* 1 */ uint32 stuckValidatorsCount; // @dev both increased and decreased
/* 1 */ uint32 depositableValidatorsCount; // @dev any value
/* 1 */ uint32 targetLimit;
/* 1 */ uint8 targetLimitMode;
/* 2 */ uint32 totalExitedKeys; // @dev only increased except for the unsafe updates
/* 2 */ uint32 enqueuedCount; // Tracks how many places are occupied by the node operator's keys in the queue.
/* 2 */ address managerAddress;
/* 3 */ address proposedManagerAddress;
/* 4 */ address rewardAddress;
/* 5 */ address proposedRewardAddress;
/* 5 */ bool extendedManagerPermissions;
/* 5 */ bool usedPriorityQueue;
}
struct NodeOperatorManagementProperties {
address managerAddress;
address rewardAddress;
bool extendedManagerPermissions;
}
struct ValidatorWithdrawalInfo {
uint256 nodeOperatorId; // @dev ID of the Node Operator
uint256 keyIndex; // @dev Index of the withdrawn key in the Node Operator's keys storage
uint256 amount; // @dev Amount of withdrawn ETH in wei
}
/// @title Lido's Community Staking Module interface
interface ICSModule is
IQueueLib,
INOAddresses,
IAssetRecovererLib,
IStakingModule
{
error CannotAddKeys();
error NodeOperatorDoesNotExist();
error SenderIsNotEligible();
error InvalidVetKeysPointer();
error ExitedKeysHigherThanTotalDeposited();
error ExitedKeysDecrease();
error InvalidInput();
error NotEnoughKeys();
error PriorityQueueAlreadyUsed();
error NotEligibleForPriorityQueue();
error PriorityQueueMaxDepositsUsed();
error NoQueuedKeysToMigrate();
error KeysLimitExceeded();
error SigningKeysInvalidOffset();
error InvalidAmount();
error ZeroLocatorAddress();
error ZeroAccountingAddress();
error ZeroExitPenaltiesAddress();
error ZeroAdminAddress();
error ZeroSenderAddress();
error ZeroParametersRegistryAddress();
event NodeOperatorAdded(
uint256 indexed nodeOperatorId,
address indexed managerAddress,
address indexed rewardAddress,
bool extendedManagerPermissions
);
event ReferrerSet(uint256 indexed nodeOperatorId, address indexed referrer);
event DepositableSigningKeysCountChanged(
uint256 indexed nodeOperatorId,
uint256 depositableKeysCount
);
event VettedSigningKeysCountChanged(
uint256 indexed nodeOperatorId,
uint256 vettedKeysCount
);
event VettedSigningKeysCountDecreased(uint256 indexed nodeOperatorId);
event DepositedSigningKeysCountChanged(
uint256 indexed nodeOperatorId,
uint256 depositedKeysCount
);
event ExitedSigningKeysCountChanged(
uint256 indexed nodeOperatorId,
uint256 exitedKeysCount
);
event TotalSigningKeysCountChanged(
uint256 indexed nodeOperatorId,
uint256 totalKeysCount
);
event TargetValidatorsCountChanged(
uint256 indexed nodeOperatorId,
uint256 targetLimitMode,
uint256 targetValidatorsCount
);
event WithdrawalSubmitted(
uint256 indexed nodeOperatorId,
uint256 keyIndex,
uint256 amount,
bytes pubkey
);
event BatchEnqueued(
uint256 indexed queuePriority,
uint256 indexed nodeOperatorId,
uint256 count
);
event KeyRemovalChargeApplied(uint256 indexed nodeOperatorId);
event ELRewardsStealingPenaltyReported(
uint256 indexed nodeOperatorId,
bytes32 proposedBlockHash,
uint256 stolenAmount
);
event ELRewardsStealingPenaltyCancelled(
uint256 indexed nodeOperatorId,
uint256 amount
);
event ELRewardsStealingPenaltyCompensated(
uint256 indexed nodeOperatorId,
uint256 amount
);
event ELRewardsStealingPenaltySettled(uint256 indexed nodeOperatorId);
function PAUSE_ROLE() external view returns (bytes32);
function RESUME_ROLE() external view returns (bytes32);
function STAKING_ROUTER_ROLE() external view returns (bytes32);
function REPORT_EL_REWARDS_STEALING_PENALTY_ROLE()
external
view
returns (bytes32);
function SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE()
external
view
returns (bytes32);
function VERIFIER_ROLE() external view returns (bytes32);
function RECOVERER_ROLE() external view returns (bytes32);
function CREATE_NODE_OPERATOR_ROLE() external view returns (bytes32);
function DEPOSIT_SIZE() external view returns (uint256);
function LIDO_LOCATOR() external view returns (ILidoLocator);
function STETH() external view returns (IStETH);
function PARAMETERS_REGISTRY()
external
view
returns (ICSParametersRegistry);
function ACCOUNTING() external view returns (ICSAccounting);
function EXIT_PENALTIES() external view returns (ICSExitPenalties);
function FEE_DISTRIBUTOR() external view returns (address);
function QUEUE_LOWEST_PRIORITY() external view returns (uint256);
function QUEUE_LEGACY_PRIORITY() external view returns (uint256);
/// @notice Returns the address of the accounting contract
function accounting() external view returns (ICSAccounting);
/// @notice Pause creation of the Node Operators and keys upload for `duration` seconds.
/// Existing NO management and reward claims are still available.
/// To pause reward claims use pause method on CSAccounting
/// @param duration Duration of the pause in seconds
function pauseFor(uint256 duration) external;
/// @notice Resume creation of the Node Operators and keys upload
function resume() external;
/// @notice Returns the initialized version of the contract
function getInitializedVersion() external view returns (uint64);
/// @notice Permissioned method to add a new Node Operator
/// Should be called by `*Gate.sol` contracts. See `PermissionlessGate.sol` and `VettedGate.sol` for examples
/// @param from Sender address. Initial sender address to be used as a default manager and reward addresses.
/// Gates must pass the correct address in order to specify which address should be the owner of the Node Operator.
/// @param managementProperties Optional. Management properties to be used for the Node Operator.
/// managerAddress: Used as `managerAddress` for the Node Operator. If not passed `from` will be used.
/// rewardAddress: Used as `rewardAddress` for the Node Operator. If not passed `from` will be used.
/// extendedManagerPermissions: Flag indicating that `managerAddress` will be able to change `rewardAddress`.
/// If se
Submitted on: 2025-09-17 15:44:24
Comments
Log in to comment.
No comments yet.