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
Submitted on: 2025-09-17 16:36:32
Comments
Log in to comment.
No comments yet.