CSAccounting

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/CSAccounting.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 { CSBondCore } from "./abstract/CSBondCore.sol";
import { CSBondCurve } from "./abstract/CSBondCurve.sol";
import { CSBondLock } from "./abstract/CSBondLock.sol";
import { AssetRecoverer } from "./abstract/AssetRecoverer.sol";

import { PausableUntil } from "./lib/utils/PausableUntil.sol";
import { AssetRecovererLib } from "./lib/AssetRecovererLib.sol";

import { IStakingModule } from "./interfaces/IStakingModule.sol";
import { ICSModule, NodeOperatorManagementProperties } from "./interfaces/ICSModule.sol";
import { ICSAccounting } from "./interfaces/ICSAccounting.sol";
import { ICSFeeDistributor } from "./interfaces/ICSFeeDistributor.sol";

/// @author vgorkavenko
/// @notice This contract stores the Node Operators' bonds in the form of stETH shares,
///         so it should be considered in the recovery process
contract CSAccounting is
    ICSAccounting,
    CSBondCore,
    CSBondCurve,
    CSBondLock,
    PausableUntil,
    AccessControlEnumerableUpgradeable,
    AssetRecoverer
{
    bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE");
    bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE");
    bytes32 public constant MANAGE_BOND_CURVES_ROLE =
        keccak256("MANAGE_BOND_CURVES_ROLE");
    bytes32 public constant SET_BOND_CURVE_ROLE =
        keccak256("SET_BOND_CURVE_ROLE");
    bytes32 public constant RECOVERER_ROLE = keccak256("RECOVERER_ROLE");

    ICSModule public immutable MODULE;
    ICSFeeDistributor public immutable FEE_DISTRIBUTOR;
    /// @dev DEPRECATED
    /// @custom:oz-renamed-from feeDistributor
    ICSFeeDistributor internal _feeDistributorOld;
    address public chargePenaltyRecipient;

    modifier onlyModule() {
        if (msg.sender != address(MODULE)) {
            revert SenderIsNotModule();
        }

        _;
    }

    /// @param lidoLocator Lido locator contract address
    /// @param module Community Staking Module contract address
    /// @param _feeDistributor Fee Distributor contract address
    /// @param minBondLockPeriod Min time in seconds for the bondLock period
    /// @param maxBondLockPeriod Max time in seconds for the bondLock period
    constructor(
        address lidoLocator,
        address module,
        address _feeDistributor,
        uint256 minBondLockPeriod,
        uint256 maxBondLockPeriod
    ) CSBondCore(lidoLocator) CSBondLock(minBondLockPeriod, maxBondLockPeriod) {
        if (module == address(0)) {
            revert ZeroModuleAddress();
        }
        if (_feeDistributor == address(0)) {
            revert ZeroFeeDistributorAddress();
        }

        MODULE = ICSModule(module);
        FEE_DISTRIBUTOR = ICSFeeDistributor(_feeDistributor);

        _disableInitializers();
    }

    /// @param bondCurve Initial bond curve
    /// @param admin Admin role member address
    /// @param bondLockPeriod Bond lock period in seconds
    /// @param _chargePenaltyRecipient Recipient of the charge penalty type
    function initialize(
        BondCurveIntervalInput[] calldata bondCurve,
        address admin,
        uint256 bondLockPeriod,
        address _chargePenaltyRecipient
    ) external reinitializer(2) {
        __AccessControlEnumerable_init();
        __CSBondCurve_init(bondCurve);
        __CSBondLock_init(bondLockPeriod);

        if (admin == address(0)) {
            revert ZeroAdminAddress();
        }

        _grantRole(DEFAULT_ADMIN_ROLE, admin);

        _setChargePenaltyRecipient(_chargePenaltyRecipient);

        LIDO.approve(address(WSTETH), type(uint256).max);
        LIDO.approve(address(WITHDRAWAL_QUEUE), type(uint256).max);
        LIDO.approve(LIDO_LOCATOR.burner(), type(uint256).max);
    }

    /// @dev This method is expected to be called only when the contract is upgraded from version 1 to version 2 for the existing version 1 deployment.
    ///      If the version 2 contract is deployed from scratch, the `initialize` method should be used instead.
    function finalizeUpgradeV2(
        BondCurveIntervalInput[][] calldata bondCurvesInputs
    ) external reinitializer(2) {
        assembly ("memory-safe") {
            sstore(_feeDistributorOld.slot, 0x00)
        }

        // NOTE: This method is not for adding new bond curves, but for migration of the existing ones to the new format (`BondCurve` to `BondCurveInterval[]`). However, bond values can be different from the current.
        if (bondCurvesInputs.length != _getLegacyBondCurvesLength()) {
            revert InvalidBondCurvesLength();
        }

        // NOTE: Re-init `CSBondCurve` due to the new format. Contains a check that the first added curve id is `DEFAULT_BOND_CURVE_ID`
        __CSBondCurve_init(bondCurvesInputs[0]);
        for (uint256 i = 1; i < bondCurvesInputs.length; ++i) {
            _addBondCurve(bondCurvesInputs[i]);
        }
    }

    /// @inheritdoc ICSAccounting
    function resume() external onlyRole(RESUME_ROLE) {
        _resume();
    }

    /// @inheritdoc ICSAccounting
    function pauseFor(uint256 duration) external onlyRole(PAUSE_ROLE) {
        _pauseFor(duration);
    }

    /// @inheritdoc ICSAccounting
    function setChargePenaltyRecipient(
        address _chargePenaltyRecipient
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        _setChargePenaltyRecipient(_chargePenaltyRecipient);
    }

    /// @inheritdoc ICSAccounting
    function setBondLockPeriod(
        uint256 period
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        CSBondLock._setBondLockPeriod(period);
    }

    /// @inheritdoc ICSAccounting
    function addBondCurve(
        BondCurveIntervalInput[] calldata bondCurve
    ) external onlyRole(MANAGE_BOND_CURVES_ROLE) returns (uint256 id) {
        id = CSBondCurve._addBondCurve(bondCurve);
    }

    /// @inheritdoc ICSAccounting
    function updateBondCurve(
        uint256 curveId,
        BondCurveIntervalInput[] calldata bondCurve
    ) external onlyRole(MANAGE_BOND_CURVES_ROLE) {
        CSBondCurve._updateBondCurve(curveId, bondCurve);
    }

    /// @inheritdoc ICSAccounting
    function setBondCurve(
        uint256 nodeOperatorId,
        uint256 curveId
    ) external onlyRole(SET_BOND_CURVE_ROLE) {
        _onlyExistingNodeOperator(nodeOperatorId);
        CSBondCurve._setBondCurve(nodeOperatorId, curveId);
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function depositETH(
        address from,
        uint256 nodeOperatorId
    ) external payable whenResumed onlyModule {
        CSBondCore._depositETH(from, nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function depositETH(uint256 nodeOperatorId) external payable whenResumed {
        _onlyExistingNodeOperator(nodeOperatorId);
        CSBondCore._depositETH(msg.sender, nodeOperatorId);
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function depositStETH(
        address from,
        uint256 nodeOperatorId,
        uint256 stETHAmount,
        PermitInput calldata permit
    ) external whenResumed onlyModule {
        _unwrapStETHPermitIfRequired(from, permit);
        CSBondCore._depositStETH(from, nodeOperatorId, stETHAmount);
    }

    /// @inheritdoc ICSAccounting
    function depositStETH(
        uint256 nodeOperatorId,
        uint256 stETHAmount,
        PermitInput calldata permit
    ) external whenResumed {
        _onlyExistingNodeOperator(nodeOperatorId);
        _unwrapStETHPermitIfRequired(msg.sender, permit);
        CSBondCore._depositStETH(msg.sender, nodeOperatorId, stETHAmount);
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function depositWstETH(
        address from,
        uint256 nodeOperatorId,
        uint256 wstETHAmount,
        PermitInput calldata permit
    ) external whenResumed onlyModule {
        _unwrapWstETHPermitIfRequired(from, permit);
        CSBondCore._depositWstETH(from, nodeOperatorId, wstETHAmount);
    }

    /// @inheritdoc ICSAccounting
    function depositWstETH(
        uint256 nodeOperatorId,
        uint256 wstETHAmount,
        PermitInput calldata permit
    ) external whenResumed {
        _onlyExistingNodeOperator(nodeOperatorId);
        _unwrapWstETHPermitIfRequired(msg.sender, permit);
        CSBondCore._depositWstETH(msg.sender, nodeOperatorId, wstETHAmount);
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function claimRewardsStETH(
        uint256 nodeOperatorId,
        uint256 stETHAmount,
        uint256 cumulativeFeeShares,
        bytes32[] calldata rewardsProof
    ) external whenResumed returns (uint256 claimedShares) {
        NodeOperatorManagementProperties memory no = MODULE
            .getNodeOperatorManagementProperties(nodeOperatorId);
        _onlyNodeOperatorManagerOrRewardAddresses(no);

        if (rewardsProof.length != 0) {
            _pullFeeRewards(nodeOperatorId, cumulativeFeeShares, rewardsProof);
        }
        claimedShares = CSBondCore._claimStETH(
            nodeOperatorId,
            stETHAmount,
            no.rewardAddress
        );
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function claimRewardsWstETH(
        uint256 nodeOperatorId,
        uint256 wstETHAmount,
        uint256 cumulativeFeeShares,
        bytes32[] calldata rewardsProof
    ) external whenResumed returns (uint256 claimedWstETH) {
        NodeOperatorManagementProperties memory no = MODULE
            .getNodeOperatorManagementProperties(nodeOperatorId);
        _onlyNodeOperatorManagerOrRewardAddresses(no);

        if (rewardsProof.length != 0) {
            _pullFeeRewards(nodeOperatorId, cumulativeFeeShares, rewardsProof);
        }
        claimedWstETH = CSBondCore._claimWstETH(
            nodeOperatorId,
            wstETHAmount,
            no.rewardAddress
        );
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function claimRewardsUnstETH(
        uint256 nodeOperatorId,
        uint256 stETHAmount,
        uint256 cumulativeFeeShares,
        bytes32[] calldata rewardsProof
    ) external whenResumed returns (uint256 requestId) {
        NodeOperatorManagementProperties memory no = MODULE
            .getNodeOperatorManagementProperties(nodeOperatorId);
        _onlyNodeOperatorManagerOrRewardAddresses(no);

        if (rewardsProof.length != 0) {
            _pullFeeRewards(nodeOperatorId, cumulativeFeeShares, rewardsProof);
        }
        requestId = CSBondCore._claimUnstETH(
            nodeOperatorId,
            stETHAmount,
            no.rewardAddress
        );
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function lockBondETH(
        uint256 nodeOperatorId,
        uint256 amount
    ) external onlyModule {
        CSBondLock._lock(nodeOperatorId, amount);
    }

    /// @inheritdoc ICSAccounting
    function releaseLockedBondETH(
        uint256 nodeOperatorId,
        uint256 amount
    ) external onlyModule {
        CSBondLock._reduceAmount(nodeOperatorId, amount);
    }

    /// @inheritdoc ICSAccounting
    function compensateLockedBondETH(
        uint256 nodeOperatorId
    ) external payable onlyModule {
        (bool success, ) = LIDO_LOCATOR.elRewardsVault().call{
            value: msg.value
        }("");
        if (!success) {
            revert ElRewardsVaultReceiveFailed();
        }

        CSBondLock._reduceAmount(nodeOperatorId, msg.value);
        emit BondLockCompensated(nodeOperatorId, msg.value);
    }

    /// @inheritdoc ICSAccounting
    function settleLockedBondETH(
        uint256 nodeOperatorId
    ) external onlyModule returns (bool applied) {
        applied = false;

        uint256 lockedAmount = CSBondLock.getActualLockedBond(nodeOperatorId);
        if (lockedAmount > 0) {
            CSBondCore._burn(nodeOperatorId, lockedAmount);
            // reduce all locked bond even if bond isn't covered lock fully
            CSBondLock._remove(nodeOperatorId);
            applied = true;
        }
    }

    /// @inheritdoc ICSAccounting
    function penalize(
        uint256 nodeOperatorId,
        uint256 amount
    ) external onlyModule {
        CSBondCore._burn(nodeOperatorId, amount);
    }

    /// @inheritdoc ICSAccounting
    function chargeFee(
        uint256 nodeOperatorId,
        uint256 amount
    ) external onlyModule {
        CSBondCore._charge(nodeOperatorId, amount, chargePenaltyRecipient);
    }

    /// @inheritdoc ICSAccounting
    function pullFeeRewards(
        uint256 nodeOperatorId,
        uint256 cumulativeFeeShares,
        bytes32[] calldata rewardsProof
    ) external {
        _onlyExistingNodeOperator(nodeOperatorId);
        _pullFeeRewards(nodeOperatorId, cumulativeFeeShares, rewardsProof);
        MODULE.updateDepositableValidatorsCount(nodeOperatorId);
    }

    /// @inheritdoc AssetRecoverer
    function recoverERC20(address token, uint256 amount) external override {
        _onlyRecoverer();
        if (token == address(LIDO)) {
            revert NotAllowedToRecover();
        }
        AssetRecovererLib.recoverERC20(token, amount);
    }

    /// @notice Recover all stETH shares from the contract
    /// @dev Accounts for the bond funds stored during recovery
    function recoverStETHShares() external {
        _onlyRecoverer();
        uint256 shares = LIDO.sharesOf(address(this)) - totalBondShares();
        AssetRecovererLib.recoverStETHShares(address(LIDO), shares);
    }

    /// @inheritdoc ICSAccounting
    function renewBurnerAllowance() external {
        LIDO.approve(LIDO_LOCATOR.burner(), type(uint256).max);
    }

    /// @inheritdoc ICSAccounting
    function getInitializedVersion() external view returns (uint64) {
        return _getInitializedVersion();
    }

    /// @inheritdoc ICSAccounting
    function getBondSummary(
        uint256 nodeOperatorId
    ) external view returns (uint256 current, uint256 required) {
        current = CSBondCore.getBond(nodeOperatorId);
        required = _getRequiredBond(nodeOperatorId, 0);
    }

    /// @inheritdoc ICSAccounting
    function getUnbondedKeysCount(
        uint256 nodeOperatorId
    ) external view returns (uint256) {
        return
            _getUnbondedKeysCount({
                nodeOperatorId: nodeOperatorId,
                includeLockedBond: true
            });
    }

    /// @inheritdoc ICSAccounting
    function getUnbondedKeysCountToEject(
        uint256 nodeOperatorId
    ) external view returns (uint256) {
        return
            _getUnbondedKeysCount({
                nodeOperatorId: nodeOperatorId,
                includeLockedBond: false
            });
    }

    /// @inheritdoc ICSAccounting
    function getBondAmountByKeysCountWstETH(
        uint256 keysCount,
        uint256 curveId
    ) external view returns (uint256) {
        return
            _sharesByEth(
                CSBondCurve.getBondAmountByKeysCount(keysCount, curveId)
            );
    }

    /// @inheritdoc ICSAccounting
    function getRequiredBondForNextKeysWstETH(
        uint256 nodeOperatorId,
        uint256 additionalKeys
    ) external view returns (uint256) {
        return
            _sharesByEth(
                getRequiredBondForNextKeys(nodeOperatorId, additionalKeys)
            );
    }

    /// @inheritdoc ICSAccounting
    function getClaimableBondShares(
        uint256 nodeOperatorId
    ) external view returns (uint256) {
        return _getClaimableBondShares(nodeOperatorId);
    }

    /// @inheritdoc ICSAccounting
    function getClaimableRewardsAndBondShares(
        uint256 nodeOperatorId,
        uint256 cumulativeFeeShares,
        bytes32[] calldata rewardsProof
    ) external view returns (uint256 claimableShares) {
        uint256 feesToDistribute = FEE_DISTRIBUTOR.getFeesToDistribute(
            nodeOperatorId,
            cumulativeFeeShares,
            rewardsProof
        );

        (uint256 current, uint256 required) = getBondSummaryShares(
            nodeOperatorId
        );
        current = current + feesToDistribute;

        return current > required ? current - required : 0;
    }

    /// @dev TODO: Remove in the next major release
    /// @inheritdoc ICSAccounting
    function feeDistributor() external view returns (ICSFeeDistributor) {
        return FEE_DISTRIBUTOR;
    }

    /// @inheritdoc ICSAccounting
    function getBondSummaryShares(
        uint256 nodeOperatorId
    ) public view returns (uint256 current, uint256 required) {
        current = CSBondCore.getBondShares(nodeOperatorId);
        required = _getRequiredBondShares(nodeOperatorId, 0);
    }

    /// @inheritdoc ICSAccounting
    function getRequiredBondForNextKeys(
        uint256 nodeOperatorId,
        uint256 additionalKeys
    ) public view returns (uint256) {
        uint256 current = CSBondCore.getBond(nodeOperatorId);
        uint256 totalRequired = _getRequiredBond(
            nodeOperatorId,
            additionalKeys
        );

        unchecked {
            return totalRequired > current ? totalRequired - current : 0;
        }
    }

    function _pullFeeRewards(
        uint256 nodeOperatorId,
        uint256 cumulativeFeeShares,
        bytes32[] calldata rewardsProof
    ) internal {
        uint256 distributed = FEE_DISTRIBUTOR.distributeFees(
            nodeOperatorId,
            cumulativeFeeShares,
            rewardsProof
        );
        CSBondCore._increaseBond(nodeOperatorId, distributed);
    }

    function _unwrapStETHPermitIfRequired(
        address from,
        PermitInput calldata permit
    ) internal {
        if (
            permit.value > 0 &&
            LIDO.allowance(from, address(this)) < permit.value
        ) {
            LIDO.permit({
                owner: from,
                spender: address(this),
                value: permit.value,
                deadline: permit.deadline,
                v: permit.v,
                r: permit.r,
                s: permit.s
            });
        }
    }

    function _unwrapWstETHPermitIfRequired(
        address from,
        PermitInput calldata permit
    ) internal {
        if (
            permit.value > 0 &&
            WSTETH.allowance(from, address(this)) < permit.value
        ) {
            WSTETH.permit({
                owner: from,
                spender: address(this),
                value: permit.value,
                deadline: permit.deadline,
                v: permit.v,
                r: permit.r,
                s: permit.s
            });
        }
    }

    /// @dev Overrides the original implementation to account for a locked bond and withdrawn validators
    function _getClaimableBondShares(
        uint256 nodeOperatorId
    ) internal view override returns (uint256) {
        unchecked {
            (
                uint256 currentShares,
                uint256 requiredShares
            ) = getBondSummaryShares(nodeOperatorId);
            return
                currentShares > requiredShares
                    ? currentShares - requiredShares
                    : 0;
        }
    }

    function _getRequiredBond(
        uint256 nodeOperatorId,
        uint256 additionalKeys
    ) internal view returns (uint256) {
        uint256 curveId = CSBondCurve.getBondCurveId(nodeOperatorId);
        uint256 nonWithdrawnKeys = MODULE.getNodeOperatorNonWithdrawnKeys(
            nodeOperatorId
        );
        uint256 requiredBondForKeys = CSBondCurve.getBondAmountByKeysCount(
            nonWithdrawnKeys + additionalKeys,
            curveId
        );
        uint256 actualLockedBond = CSBondLock.getActualLockedBond(
            nodeOperatorId
        );

        return requiredBondForKeys + actualLockedBond;
    }

    function _getRequiredBondShares(
        uint256 nodeOperatorId,
        uint256 additionalKeys
    ) internal view returns (uint256) {
        return _sharesByEth(_getRequiredBond(nodeOperatorId, additionalKeys));
    }

    /// @dev Unbonded stands for the amount of keys not fully covered with bond
    function _getUnbondedKeysCount(
        uint256 nodeOperatorId,
        bool includeLockedBond
    ) internal view returns (uint256) {
        uint256 nonWithdrawnKeys = MODULE.getNodeOperatorNonWithdrawnKeys(
            nodeOperatorId
        );
        unchecked {
            uint256 currentBond = CSBondCore.getBond(nodeOperatorId);
            if (includeLockedBond) {
                uint256 lockedBond = CSBondLock.getActualLockedBond(
                    nodeOperatorId
                );
                // We use strict condition here since in rare case of equality the outcome of the function will not change
                if (lockedBond > currentBond) {
                    return nonWithdrawnKeys;
                }

                currentBond -= lockedBond;
            }
            // 10 wei is added to account for possible stETH rounding errors
            // https://github.com/lidofinance/lido-dao/issues/442#issuecomment-1182264205.
            // Should be sufficient for ~ 40 years
            uint256 bondedKeys = CSBondCurve.getKeysCountByBondAmount(
                currentBond + 10 wei,
                CSBondCurve.getBondCurveId(nodeOperatorId)
            );
            return
                nonWithdrawnKeys > bondedKeys
                    ? nonWithdrawnKeys - bondedKeys
                    : 0;
        }
    }

    function _onlyRecoverer() internal view override {
        _checkRole(RECOVERER_ROLE);
    }

    function _onlyExistingNodeOperator(uint256 nodeOperatorId) internal view {
        if (
            nodeOperatorId <
            IStakingModule(address(MODULE)).getNodeOperatorsCount()
        ) {
            return;
        }

        revert NodeOperatorDoesNotExist();
    }

    function _onlyNodeOperatorManagerOrRewardAddresses(
        NodeOperatorManagementProperties memory no
    ) internal view {
        if (no.managerAddress == address(0)) {
            revert NodeOperatorDoesNotExist();
        }

        if (no.managerAddress == msg.sender || no.rewardAddress == msg.sender) {
            return;
        }

        revert SenderIsNotEligible();
    }

    function _setChargePenaltyRecipient(
        address _chargePenaltyRecipient
    ) private {
        if (_chargePenaltyRecipient == address(0)) {
            revert ZeroChargePenaltyRecipientAddress();
        }
        chargePenaltyRecipient = _chargePenaltyRecipient;
        emit ChargePenaltyRecipientSet(_chargePenaltyRecipient);
    }
}
"
    },
    "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;
    }
}
"
    },
    "src/abstract/CSBondCore.sol": {
      "content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.24;

import { ILidoLocator } from "../interfaces/ILidoLocator.sol";
import { ILido } from "../interfaces/ILido.sol";
import { IBurner } from "../interfaces/IBurner.sol";
import { IWstETH } from "../interfaces/IWstETH.sol";
import { IWithdrawalQueue } from "../interfaces/IWithdrawalQueue.sol";
import { ICSBondCore } from "../interfaces/ICSBondCore.sol";

/// @dev Bond core mechanics abstract contract
///
/// It gives basic abilities to manage bond shares (stETH) of the Node Operator.
///
/// It contains:
///  - store bond shares (stETH)
///  - get bond shares (stETH) and bond amount
///  - deposit ETH/stETH/wstETH
///  - claim ETH/stETH/wstETH
///  - burn
///
/// Should be inherited by Module contract, or Module-related contract.
/// Internal non-view methods should be used in Module contract with additional requirements (if any).
///
/// @author vgorkavenko
abstract contract CSBondCore is ICSBondCore {
    /// @custom:storage-location erc7201:CSBondCore
    struct CSBondCoreStorage {
        mapping(uint256 nodeOperatorId => uint256 shares) bondShares;
        uint256 totalBondShares;
    }

    ILidoLocator public immutable LIDO_LOCATOR;
    ILido public immutable LIDO;
    IWithdrawalQueue public immutable WITHDRAWAL_QUEUE;
    IWstETH public immutable WSTETH;

    // keccak256(abi.encode(uint256(keccak256("CSBondCore")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant CS_BOND_CORE_STORAGE_LOCATION =
        0x23f334b9eb5378c2a1573857b8f9d9ca79959360a69e73d3f16848e56ec92100;

    constructor(address lidoLocator) {
        if (lidoLocator == address(0)) {
            revert ZeroLocatorAddress();
        }
        LIDO_LOCATOR = ILidoLocator(lidoLocator);
        LIDO = ILido(LIDO_LOCATOR.lido());
        WITHDRAWAL_QUEUE = IWithdrawalQueue(LIDO_LOCATOR.withdrawalQueue());
        WSTETH = IWstETH(WITHDRAWAL_QUEUE.WSTETH());
    }

    /// @inheritdoc ICSBondCore
    function totalBondShares() public view returns (uint256) {
        return _getCSBondCoreStorage().totalBondShares;
    }

    /// @inheritdoc ICSBondCore
    function getBondShares(
        uint256 nodeOperatorId
    ) public view returns (uint256) {
        return _getCSBondCoreStorage().bondShares[nodeOperatorId];
    }

    /// @inheritdoc ICSBondCore
    function getBond(uint256 nodeOperatorId) public view returns (uint256) {
        return _ethByShares(getBondShares(nodeOperatorId));
    }

    /// @dev Stake user's ETH with Lido and stores stETH shares as Node Operator's bond shares
    function _depositETH(address from, uint256 nodeOperatorId) internal {
        if (msg.value == 0) {
            return;
        }

        uint256 shares = LIDO.submit{ value: msg.value }({
            _referral: address(0)
        });
        _increaseBond(nodeOperatorId, shares);
        emit BondDepositedETH(nodeOperatorId, from, msg.value);
    }

    /// @dev Transfer user's stETH to the contract and stores stETH shares as Node Operator's bond shares
    function _depositStETH(
        address from,
        uint256 nodeOperatorId,
        uint256 amount
    ) internal {
        if (amount == 0) {
            return;
        }

        uint256 shares = _sharesByEth(amount);
        LIDO.transferSharesFrom(from, address(this), shares);
        _increaseBond(nodeOperatorId, shares);
        emit BondDepositedStETH(nodeOperatorId, from, amount);
    }

    /// @dev Transfer user's wstETH to the contract, unwrap and store stETH shares as Node Operator's bond shares
    function _depositWstETH(
        address from,
        uint256 nodeOperatorId,
        uint256 amount
    ) internal {
        if (amount == 0) {
            return;
        }

        WSTETH.transferFrom(from, address(this), amount);
        uint256 sharesBefore = LIDO.sharesOf(address(this));
        WSTETH.unwrap(amount);
        uint256 sharesAfter = LIDO.sharesOf(address(this));
        _increaseBond(nodeOperatorId, sharesAfter - sharesBefore);
        emit BondDepositedWstETH(nodeOperatorId, from, amount);
    }

    function _increaseBond(uint256 nodeOperatorId, uint256 shares) internal {
        if (shares == 0) {
            return;
        }

        CSBondCoreStorage storage $ = _getCSBondCoreStorage();
        unchecked {
            $.bondShares[nodeOperatorId] += shares;
            $.totalBondShares += shares;
        }
    }

    /// @dev Claim Node Operator's excess bond shares (stETH) in ETH by requesting withdrawal from the protocol
    ///      As a usual withdrawal request, this claim might be processed on the next stETH rebase
    function _claimUnstETH(
        uint256 nodeOperatorId,
        uint256 requestedAmountToClaim,
        address to
    ) internal returns (uint256 requestId) {
        uint256 claimableShares = _getClaimableBondShares(nodeOperatorId);
        uint256 sharesToClaim = requestedAmountToClaim <
            _ethByShares(claimableShares)
            ? _sharesByEth(requestedAmountToClaim)
            : claimableShares;
        if (sharesToClaim == 0) {
            revert NothingToClaim();
        }

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = _ethByShares(sharesToClaim);

        uint256 sharesBefore = LIDO.sharesOf(address(this));
        requestId = WITHDRAWAL_QUEUE.requestWithdrawals(amounts, to)[0];
        uint256 sharesAfter = LIDO.sharesOf(address(this));

        _unsafeReduceBond(nodeOperatorId, sharesBefore - sharesAfter);
        emit BondClaimedUnstETH(nodeOperatorId, to, amounts[0], requestId);
    }

    /// @dev Claim Node Operator's excess bond shares (stETH) in stETH by transferring shares from the contract
    function _claimStETH(
        uint256 nodeOperatorId,
        uint256 requestedAmountToClaim,
        address to
    ) internal returns (uint256 sharesToClaim) {
        uint256 claimableShares = _getClaimableBondShares(nodeOperatorId);
        sharesToClaim = requestedAmountToClaim < _ethByShares(claimableShares)
            ? _sharesByEth(requestedAmountToClaim)
            : claimableShares;
        if (sharesToClaim == 0) {
            revert NothingToClaim();
        }

        _unsafeReduceBond(nodeOperatorId, sharesToClaim);

        uint256 ethAmount = LIDO.transferShares(to, sharesToClaim);
        emit BondClaimedStETH(nodeOperatorId, to, ethAmount);
    }

    /// @dev Claim Node Operator's excess bond shares (stETH) in wstETH by wrapping stETH from the contract and transferring wstETH
    function _claimWstETH(
        uint256 nodeOperatorId,
        uint256 requestedAmountToClaim,
        address to
    ) internal returns (uint256 wstETHAmount) {
        uint256 claimableShares = _getClaimableBondShares(nodeOperatorId);
        uint256 sharesToClaim = requestedAmountToClaim < claimableShares
            ? requestedAmountToClaim
            : claimableShares;
        if (sharesToClaim == 0) {
            revert NothingToClaim();
        }

        uint256 sharesBefore = LIDO.sharesOf(address(this));
        wstETHAmount = WSTETH.wrap(_ethByShares(sharesToClaim));
        uint256 sharesAfter = LIDO.sharesOf(address(this));
        _unsafeReduceBond(nodeOperatorId, sharesBefore - sharesAfter);
        WSTETH.transfer(to, wstETHAmount);
        emit BondClaimedWstETH(nodeOperatorId, to, wstETHAmount);
    }

    /// @dev Burn Node Operator's bond shares (stETH). Shares will be burned on the next stETH rebase
    /// @dev The contract that uses this implementation should be granted `Burner.REQUEST_BURN_SHARES_ROLE` and have stETH allowance for `Burner`
    /// @param amount Bond amount to burn in ETH (stETH)
    function _burn(uint256 nodeOperatorId, uint256 amount) internal {
        uint256 sharesToBurn = _sharesByEth(amount);
        uint256 burnedShares = _reduceBond(nodeOperatorId, sharesToBurn);
        // If no bond already or the amount to burn is zero
        if (burnedShares == 0) {
            return;
        }

        // TODO: Replace with `requestBurnMyShares` (https://github.com/lidofinance/core/pull/1142) in the next major release
        IBurner(LIDO_LOCATOR.burner()).requestBurnShares(
            address(this),
            burnedShares
        );
        emit BondBurned(
            nodeOperatorId,
            _ethByShares(sharesToBurn),
            _ethByShares(burnedShares)
        );
    }

    /// @dev Transfer Node Operator's bond shares (stETH) to charge recipient
    /// @param amount Bond amount to charge in ETH (stETH)
    /// @param recipient Address to send charged shares
    function _charge(
        uint256 nodeOperatorId,
        uint256 amount,
        address recipient
    ) internal {
        uint256 toChargeShares = _sharesByEth(amount);
        uint256 chargedShares = _reduceBond(nodeOperatorId, toChargeShares);
        // If no bond already or the amount to charge is zero
        if (chargedShares == 0) {
            return;
        }

        uint256 chargedEth = LIDO.transferShares(recipient, chargedShares);

        emit BondCharged(
            nodeOperatorId,
            _ethByShares(toChargeShares),
            chargedEth
        );
    }

    /// @dev Must be overridden in case of additional restrictions on a claimable bond amount
    function _getClaimableBondShares(
        uint256 nodeOperatorId
    ) internal view virtual returns (uint256) {
        return _getCSBondCoreStorage().bondShares[nodeOperatorId];
    }

    /// @dev Shortcut for Lido's getSharesByPooledEth
    function _sharesByEth(uint256 ethAmount) internal view returns (uint256) {
        if (ethAmount == 0) {
            return 0;
        }

        return LIDO.getSharesByPooledEth(ethAmount);
    }

    /// @dev Shortcut for Lido's getPooledEthByShares
    function _ethByShares(uint256 shares) internal view returns (uint256) {
        if (shares == 0) {
            return 0;
        }

        return LIDO.getPooledEthByShares(shares);
    }

    /// @dev Unsafe reduce bond shares (stETH) (possible underflow). Safety checks should be done outside
    function _unsafeReduceBond(uint256 nodeOperatorId, uint256 shares) private {
        CSBondCoreStorage storage $ = _getCSBondCoreStorage();
        $.bondShares[nodeOperatorId] -= shares;
        $.totalBondShares -= shares;
    }

    /// @dev Safe reduce bond shares (stETH). The maximum shares to reduce is the current bond shares
    function _reduceBond(
        uint256 nodeOperatorId,
        uint256 shares
    ) private returns (uint256 reducedShares) {
        uint256 currentShares = getBondShares(nodeOperatorId);
        reducedShares = shares < currentShares ? shares : currentShares;
        _unsafeReduceBond(nodeOperatorId, reducedShares);
    }

    function _getCSBondCoreStorage()
        private
        pure
        returns (CSBondCoreStorage storage $)
    {
        assembly {
            $.slot := CS_BOND_CORE_STORAGE_LOCATION
        }
    }
}
"
    },
    "src/abstract/CSBondCurve.sol": {
      "content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.24;

import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

import { ICSBondCurve } from "../interfaces/ICSBondCurve.sol";

/// @dev Bond curve mechanics abstract contract
///
/// It gives the ability to build bond curves for flexible bond math.
/// There is a default bond curve for all Node Operators, which might be 'overridden' for a particular Node Operator.
///
/// It contains:
///  - add bond curve
///  - get bond curve info
///  - set default bond curve
///  - set bond curve for the given Node Operator
///  - get bond curve for the given Node Operator
///  - get required bond amount for the given keys count
///  - get keys count for the given bond amount
///
/// It should be inherited by a module contract or a module-related contract.
/// Internal non-view methods should be used in the Module contract with additional requirements (if any).
///
/// @author vgorkavenko
abstract contract CSBondCurve is ICSBondCurve, Initializable {
    /// @custom:storage-location erc7201:CSBondCurve
    struct CSBondCurveStorage {
        /// @dev DEPRECATED. DO NOT USE. Preserves storage layout.
        bytes32[] legacyBondCurves;
        /// @dev Mapping of Node Operator id to bond curve id
        mapping(uint256 nodeOperatorId => uint256 bondCurveId) operatorBondCurveId;
        BondCurve[] bondCurves;
    }

    // keccak256(abi.encode(uint256(keccak256("CSBondCurve")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant CS_BOND_CURVE_STORAGE_LOCATION =
        0x8f22e270e477f5becb8793b61d439ab7ae990ed8eba045eb72061c0e6cfe1500;

    uint256 public constant MIN_CURVE_LENGTH = 1;
    uint256 public constant DEFAULT_BOND_CURVE_ID = 0;
    uint256 public constant MAX_CURVE_LENGTH = 100;

    // @inheritdoc ICSBondCurve
    function getCurvesCount() external view returns (uint256) {
        return _getCSBondCurveStorage().bondCurves.length;
    }

    /// @inheritdoc ICSBondCurve
    function getCurveInfo(
        uint256 curveId
    ) external view returns (BondCurve memory) {
        return _getCurveInfo(curveId);
    }

    /// @inheritdoc ICSBondCurve
    function getBondCurve(
        uint256 nodeOperatorId
    ) external view returns (BondCurve memory) {
        return _getCurveInfo(getBondCurveId(nodeOperatorId));
    }

    /// @inheritdoc ICSBondCurve
    function getBondCurveId(
        uint256 nodeOperatorId
    ) public view returns (uint256) {
        return _getCSBondCurveStorage().operatorBondCurveId[nodeOperatorId];
    }

    /// @inheritdoc ICSBondCurve
    function getBondAmountByKeysCount(
        uint256 keys,
        uint256 curveId
    ) public view returns (uint256) {
        return _getBondAmountByKeysCount(keys, _getCurveInfo(curveId));
    }

    /// @inheritdoc ICSBondCurve
    function getKeysCountByBondAmount(
        uint256 amount,
        uint256 curveId
    ) public view returns (uint256) {
        return _getKeysCountByBondAmount(amount, _getCurveInfo(curveId));
    }

    // solhint-disable-next-line func-name-mixedcase
    function __CSBondCurve_init(
        BondCurveIntervalInput[] calldata defaultBondCurveIntervals
    ) internal onlyInitializing {
        uint256 addedId = _addBondCurve(defaultBondCurveIntervals);
        if (addedId != DEFAULT_BOND_CURVE_ID) {
            revert InvalidInitializationCurveId();
        }
    }

    /// @dev Add a new bond curve to the array
    function _addBondCurve(
        BondCurveIntervalInput[] calldata intervals
    ) internal returns (uint256 curveId) {
        CSBondCurveStorage storage $ = _getCSBondCurveStorage();

        _checkBondCurve(intervals);

        curveId = $.bondCurves.length;
        _addIntervalsToBondCurve($.bondCurves.push(), intervals);

        emit BondCurveAdded(curveId, intervals);
    }

    /// @dev Update existing bond curve
    function _updateBondCurve(
        uint256 curveId,
        BondCurveIntervalInput[] calldata intervals
    ) internal {
        CSBondCurveStorage storage $ = _getCSBondCurveStorage();
        unchecked {
            if (curveId > $.bondCurves.length - 1) {
                revert InvalidBondCurveId();
            }
        }

        _checkBondCurve(intervals);

        delete $.bondCurves[curveId];

        _addIntervalsToBondCurve($.bondCurves[curveId], intervals);

        emit BondCurveUpdated(curveId, intervals);
    }

    /// @dev Sets bond curve for the given Node Operator
    ///      It will be used for the Node Operator instead of the previously set curve
    function _setBondCurve(uint256 nodeOperatorId, uint256 curveId) internal {
        CSBondCurveStorage storage $ = _getCSBondCurveStorage();
        unchecked {
            if (curveId > $.bondCurves.length - 1) {
                revert InvalidBondCurveId();
            }
        }
        $.operatorBondCurveId[nodeOperatorId] = curveId;
        emit BondCurveSet(nodeOperatorId, curveId);
    }

    function _getBondAmountByKeysCount(
        uint256 keys,
        BondCurve storage curve
    ) internal view returns (uint256) {
        BondCurveInterval[] storage intervals = curve.intervals;
        if (keys == 0) {
            return 0;
        }

        unchecked {
            uint256 low = 0;
            uint256 high = intervals.length - 1;
            while (low < high) {
                uint256 mid = (low + high + 1) / 2;
                if (keys < intervals[mid].minKeysCount) {
                    high = mid - 1;
                } else {
                    low = mid;
                }
            }
            BondCurveInterval storage interval = intervals[low];
            return
                interval.minBond +
                (keys - interval.minKeysCount) *
                interval.trend;
        }
    }

    function _getKeysCountByBondAmount(
        uint256 amount,
        BondCurve storage curve
    ) internal view returns (uint256) {
        BondCurveInterval[] storage intervals = curve.intervals;

        // intervals[0].minBond is essentially the amount of bond required for the very first key
        if (amount < intervals[0].minBond) {
            return 0;
        }

        unchecked {
            uint256 low = 0;
            uint256 high = intervals.length - 1;
            while (low < high) {
                uint256 mid = (low + high + 1) / 2;
                if (amount < intervals[mid].minBond) {
                    high = mid - 1;
                } else {
                    low = mid;
                }
            }

            BondCurveInterval storage interval;

            //
            // Imagine we have:
            //  Interval 0: minKeysCount = 1, minBond = 2 ETH, trend = 2 ETH
            //  Interval 1: minKeysCount = 4, minBond = 9 ETH, trend = 3 ETH (more expensive than Interval 0)
            //  Amount = 8.5 ETH
            // In this case low = 0, and if we count the keys count using data from Interval 0 we will get 4 keys, which is wrong.
            // So we need a special check for bond amounts between Interval 0 maxBond and Interval 1 minBond.
            //
            if (low < intervals.length - 1) {
                interval = intervals[low + 1];
                if (amount > interval.minBond - interval.trend) {
                    return interval.minKeysCount - 1;
                }
            }
            interval = intervals[low];
            return
                interval.minKeysCount +
                (amount - interval.minBond) /
                interval.trend;
        }
    }

    // Deprecated. To be removed in the next upgrade
    function _getLegacyBondCurvesLength() internal view returns (uint256) {
        return _getCSBondCurveStorage().legacyBondCurves.length;
    }

    function _addIntervalsToBondCurve(
        BondCurve storage bondCurve,
        BondCurveIntervalInput[] calldata intervals
    ) private {
        BondCurveInterval storage interval = bondCurve.intervals.push();

        interval.minKeysCount = intervals[0].minKeysCount;
        interval.trend = intervals[0].trend;
        interval.minBond = intervals[0].trend;

        for (uint256 i = 1; i < intervals.length; ++i) {
            BondCurveInterval storage prev = interval;
            interval = bondCurve.intervals.push();
            interval.minKeysCount = intervals[i].minKeysCount;
            interval.trend = intervals[i].trend;
            interval.minBond =
                intervals[i].trend +
                prev.minBond +
                (intervals[i].minKeysCount - prev.minKeysCount - 1) *
                prev.trend;
        }
    }

    function _getCurveInfo(
        uint256 curveId
    ) private view returns (BondCurve storage) {
        CSBondCurveStorage storage $ = _getCSBondCurveStorage();
        unchecked {
            if (curveId > $.bondCurves.length - 1) {
                revert InvalidBondCurveId();
            }
        }

        return $.bondCurves[curveId];
    }

    function _checkBondCurve(
        BondCurveIntervalInput[] calldata intervals
    ) private pure {
        if (
            intervals.length < MIN_CURVE_LENGTH ||
            intervals.length > MAX_CURVE_LENGTH
        ) {
            revert InvalidBondCurveLength();
        }

        if (intervals[0].minKeysCount != 1) {
            revert InvalidBondCurveValues();
        }

        if (intervals[0].trend == 0) {
            revert InvalidBondCurveValues();
        }

        for (uint256 i = 1; i < intervals.length; ++i) {
            unchecked {
                if (
                    intervals[i].minKeysCount <= intervals[i - 1].minKeysCount
                ) {
                    revert InvalidBondCurveValues();
                }
                if (intervals[i].trend == 0) {
                    revert InvalidBondCurveValues();
                }
            }
        }
    }

    function _getCSBondCurveStorage()
        private
        pure
        returns (CSBondCurveStorage storage $)
    {
        assembly {
            $.slot := CS_BOND_CURVE_STORAGE_LOCATION
        }
    }
}
"
    },
    "src/abstract/CSBondLock.sol": {
      "content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.24;

import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";

import { ICSBondLock } from "../interfaces/ICSBondLock.sol";

/// @dev Bond lock mechanics abstract contract.
///
/// It gives the ability to lock the bond amount of the Node Operator.
/// There is a period of time during which the module can settle the lock in any way (for example, by penalizing the bond).
/// After that period, the lock is removed, and the bond amount is considered unlocked.
///
/// The contract contains:
///  - set default bond lock period
///  - get default bond lock period
///  - lock bond
///  - get locked bond info
///  - get actual locked bond amount
///  - reduce locked bond amount
///  - remove bond lock
///
/// It should be inherited by a module contract or a module-related contract.
/// Internal non-view methods should be used in the Module contract with additional requirements (if any).
///
/// @author vgorkavenko
abstract contract CSBondLock is ICSBondLock, Initializable {
    using SafeCast for uint256;

    /// @custom:storage-location erc7201:CSBondLock
    struct CSBondLockStorage {
        /// @dev Default bond lock period for all locks
        ///      After this period the bond lock is removed and no longer valid
        uint256 bondLockPeriod;
        /// @dev Mapping of the Node Operator id to the bond lock
        mapping(uint256 nodeOperatorId => BondLock) bondLock;
    }

    // keccak256(abi.encode(uint256(keccak256("CSBondLock")) - 1)) & ~bytes32(uint256(0xff))
    bytes32 private constant CS_BOND_LOCK_STORAGE_LOCATION =
        0x78c5a36767279da056404c09083fca30cf3ea61c442cfaba6669f76a37393f00;

    uint256 public immutable MIN_BOND_LOCK_PERIOD;
    uint256 public immutable MAX_BOND_LOCK_PERIOD;

    constructor(uint256 minBondLockPeriod, uint256 maxBondLockPeriod) {
        if (minBondLockPeriod == 0) {
            revert InvalidBondLockPeriod();
        }
        if (minBondLockPeriod > maxBondLockPeriod) {
            revert InvalidBondLockPeriod();
        }
        // period can not be more than type(uint64).max to avoid overflow when setting bond lock
        if (maxBondLockPeriod > type(uint64).max) {
            revert InvalidBondLockPeriod();
        }
        MIN_BOND_LOCK_PERIOD = minBondLockPeriod;
        MAX_BOND_LOCK_PERIOD = maxBondLockPeriod;
    }

    /// @inheritdoc ICSBondLock
    function getBondLockPeriod() external view returns (uint256) {
        return _getCSBondLockStorage().bondLockPeriod;
    }

    /// @inheritdoc ICSBondLock
    function getLockedBondInfo(
        uint256 nodeOperatorId
    ) external view returns (BondLock memory) {
        return _getCSBondLockStorage().bondLock[nodeOperatorId];
    }

    /// @inheritdoc ICSBondLock
    function getActualLockedBond(
        uint256 nodeOperatorId
    ) public view returns (uint256) {
        BondLock storage bondLock = _getCSBondLockStorage().bondLock[
            nodeOperatorId
        ];
        return bondLock.until > block.timestamp ? bondLock.amount : 0;
    }

    /// @dev Lock bond amount for the given Node Operator until the period.
    function _lock(uint256 nodeOperatorId, uint256 amount) internal {
        CSBondLockStorage storage $ = _getCSBondLockStorage();
        if (amount == 0) {
            revert InvalidBondLockAmount();
        }
        BondLock memory lock = $.bondLock[nodeOperatorId];
        if (lock.until > block.timestamp) {
            amount += lock.amount;
        }
        unchecked {
            _changeBondLock({
                nodeOperatorId: nodeOperatorId,
                amount: amount,
                until: block.timestamp + $.bondLockPeriod
            });
        }
    }

    /// @dev Reduce the locked bond amount for the given Node Operator without changing the lock period
    function _reduceAmount(uint256 nodeOperatorId, uint256 amount) internal {
        if (amount == 0) {
            revert InvalidBondLockAmount();
        }
        uint256 locked = getActualLockedBond(nodeOperatorId);
        if (locked < amount) {
            revert InvalidBondLockAmount();
        }
        unchecked {
            _changeBondLock(
                nodeOperatorId,
                locked - amount,
                _getCSBondLockStorage().bondLock[nodeOperatorId].until
            );
        }
    }

    /// @dev Remove bond lock for the given Node Operator
    function _remove(uint256 nodeOperatorId) internal {
        delete _getCSBondLockStorage().bondLock[nodeOperatorId];
        emit BondLockRemoved(nodeOperatorId);
    }

    // solhint-disable-next-line func-name-mixedcase
    function __CSBondLock_init(uint256 period) internal onlyInitializing {
        _setBondLockPeriod(period);
    }

    /// @dev Set default bond lock period. That period will be added to the block timestamp of the lock translation to determine the bond lock duration
    function _setBondLockPeriod(uint256 period) internal {
        if (period < MIN_BOND_LOCK_PERIOD || period > MAX_BOND_LOCK_PERIOD) {
            revert InvalidBondLockPeriod();
        }
        _getCSBondLockStorage().bondLockPeriod = period;
        emit BondLockPeriodChanged(period);
    }

    function _changeBondLock(
        uint256 nodeOperatorId,
        uint256 amount,
        uint256 until
    ) private {
        if (amount == 0) {
            _remove(nodeOperatorId);
            return;
        }
        _getCSBondLockStorage().bondLock[nodeOperatorId] = BondLock({
            amount: amount.toUint128(),
            until: until.toUint128()
        });
        emit BondLockChanged(nodeOperatorId, amount, until);
    }

    function _getCSBondLockStorage()
        private
        pure
        returns (CSBondLockStorage storage $)
    {
        assembly {
            $.slot := CS_BOND_LOCK_STORAGE_LOCATION
        }
    }
}
"
    },
    "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/lib/AssetRecovererLib.sol": {
      "content": "// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.24;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ILido } from "../interfaces/ILido.sol";

interface IAssetRecovererLib {
    event EtherRecovered(address indexed recipient, uint256 amount);
    event ERC20Recovered(
        address indexed token,
        address indexed recipient,
        uint256 amount
    );
    event StETHSharesRecovered(address indexed recipient, uint256 shares);
    event ERC721Recovered(
        address indexed token,
        uint256 tokenId,
        address indexed recipient
    );
    event ERC1155Recovered(
        address indexed token,
        uint256 tokenId,
        address indexed recipient,
        uint256 amount
    );

    error FailedToSendEther();
    error NotAllowedToRecover();
}

/*
 * @title AssetRecovererLib
 * @dev Library providing mechanisms for recovering various asset types (ETH, ERC20, ERC721, ERC1155).
 * This library is designed to be used by a contract that implements the AssetRecoverer interface.
 */
library AssetRecovererLib {
    using SafeERC20 for IERC20;

    /**
     * @dev Allows the sender to recover Ether held by the contract.
     * Emits an EtherRecovered event upon success.
     */
    function recoverEther() external {
        uint256 amount = address(this).balance;
        (bool success, ) = msg.sender.call{ value: amount }("");
        if (!success) {
            revert IAssetRecovererLib.FailedToSendEther();
        }

        emit IAssetRecovererLib.EtherRecovered(msg.sender, amount);
    }

    /**
     * @dev Allows the 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.
     */
    function recoverERC20(address token, uint256 amount) external {
        IERC20(token).safeTransfer(msg.sender, amount);
        emit IAssetRecovererLib.ERC20Recovered(token, msg.sender, amount);
    }

    /**
     * @dev Allows the sender to recover stETH shares held by the contract.
     * The use of a separate method for stETH is to avoid rounding problems when converting shares to stETH.
     * @param lido The address of the Lido contract.
     * @param shares The amount of stETH shares to recover.
     * Emits an StETHSharesRecovered event upon success.
     */
    function recoverStETHShares(address lido, uint256 shares) external {
        ILido(lido).transferShares(msg.sender, shares);
        emit IAssetRecovererLib.StETHSharesRecovered(msg.sender, shares);
    }

    /**
     * @dev Allows the 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 {
        IERC721(toke

Tags:
ERC20, ERC721, ERC1155, ERC165, Multisig, Non-Fungible, Swap, Staking, Voting, Upgradeable, Multi-Signature, Factory, Oracle|addr:0x6f09d2426c7405c5546413e6059f884d2d03f449|verified:true|block:23382823|tx:0xaf1e0703e0b08841eea842ef2ab1d2eff3572338d2407faadecd9ede4c726d6a|first_check:1758119791

Submitted on: 2025-09-17 16:36:32

Comments

Log in to comment.

No comments yet.