diff --git a/script/DeployBase.s.sol b/script/DeployBase.s.sol index 310d9d3d..ea80429e 100644 --- a/script/DeployBase.s.sol +++ b/script/DeployBase.s.sol @@ -74,15 +74,18 @@ abstract contract DeployBase is Script { moduleType: "community-staking-module", locator: address(locator) }); + uint256[] memory curve = new uint256[](2); + curve[0] = 2 ether; + curve[1] = 4 ether; CSAccounting accounting = new CSAccounting({ - commonBondSize: 2 ether, + bondCurve: curve, admin: deployer, lidoLocator: address(locator), communityStakingModule: address(csm), wstETH: address(wstETH), // todo: arguable. should be discussed - _blockedBondRetentionPeriod: 8 weeks, - _blockedBondManagementPeriod: 1 weeks + bondLockRetentionPeriod: 8 weeks, + bondLockManagementPeriod: 1 weeks }); CSFeeOracle oracleImpl = new CSFeeOracle({ diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index c07952ed..1d2036f3 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -5,6 +5,9 @@ pragma solidity 0.8.21; import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import { CSBondCurve } from "./CSBondCurve.sol"; +import { CSBondLock } from "./CSBondLock.sol"; + import { ILidoLocator } from "./interfaces/ILidoLocator.sol"; import { ICSModule } from "./interfaces/ICSModule.sol"; import { ILido } from "./interfaces/ILido.sol"; @@ -43,38 +46,29 @@ contract CSAccountingBase { address to, uint256 amount ); - event ELRewardsStealingPenaltyInitiated( - uint256 indexed nodeOperatorId, - uint256 proposedBlockNumber, - uint256 stolenAmount - ); - event BlockedBondChanged( - uint256 indexed nodeOperatorId, - uint256 newAmountETH, - uint256 retentionUntil - ); - event BlockedBondCompensated( - uint256 indexed nodeOperatorId, - uint256 amountETH - ); - event BlockedBondReleased( - uint256 indexed nodeOperatorId, - uint256 amountETH - ); event BondPenalized( uint256 indexed nodeOperatorId, uint256 penaltyETH, uint256 coveringETH ); + event ELRewardsStealingPenaltyInitiated( + uint256 indexed nodeOperatorId, + uint256 proposedBlockNumber, + uint256 stolenAmount + ); + event BondLockCompensated(uint256 indexed nodeOperatorId, uint256 amount); + event BondLockReleased(uint256 indexed nodeOperatorId, uint256 amount); error NotOwnerToClaim(address msgSender, address owner); - error InvalidBlockedBondRetentionPeriod(); - error InvalidStolenAmount(); error InvalidSender(); - error InvalidMultiplier(); } -contract CSAccounting is CSAccountingBase, AccessControlEnumerable { +contract CSAccounting is + CSAccountingBase, + CSBondCurve, + CSBondLock, + AccessControlEnumerable +{ struct PermitInput { uint256 value; uint256 deadline; @@ -82,10 +76,6 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { bytes32 r; bytes32 s; } - struct BlockedBond { - uint256 ETHAmount; - uint256 retentionUntil; - } bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); // 0x139c2898040ef16910dc9f44dc697df79363da767d8bc92f2e310312b816e46d bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); // 0x2fc10cc8ae19568712f7a176fb4978616a610650813c9d05326c34abb62749c7 @@ -96,21 +86,15 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { keccak256("INSTANT_PENALIZE_BOND_ROLE"); // 0x9909cf24c2d3bafa8c229558d86a1b726ba57c3ef6350848dcf434a4181b56c7 bytes32 public constant EL_REWARDS_STEALING_PENALTY_INIT_ROLE = keccak256("EL_REWARDS_STEALING_PENALTY_INIT_ROLE"); // 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f + bytes32 public constant EL_REWARDS_STEALING_PENALTY_RELEASE_ROLE = + keccak256("EL_REWARDS_STEALING_PENALTY_RELEASE_ROLE"); // 0x8d78671045c549f09e0cf6e7e9856c36698f72f93962abf8e1955dc595a592ee bytes32 public constant EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE = keccak256("EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE"); // 0xdf6226649a1ca132f86d419e46892001284368a8f7445b5eb0d3fadf91329fe6 + bytes32 public constant SET_BOND_CURVE_ROLE = + keccak256("SET_BOND_CURVE_ROLE"); // 0x645c9e6d2a86805cb5a28b1e4751c0dab493df7cf935070ce405489ba1a7bf72 bytes32 public constant SET_BOND_MULTIPLIER_ROLE = keccak256("SET_BOND_MULTIPLIER_ROLE"); // 0x62131145aee19b18b85aa8ead52ba87f0efb6e61e249155edc68a2c24e8f79b5 - // todo: should be reconsidered - uint256 public constant MIN_BLOCKED_BOND_RETENTION_PERIOD = 4 weeks; - uint256 public constant MAX_BLOCKED_BOND_RETENTION_PERIOD = 365 days; - uint256 public constant MIN_BLOCKED_BOND_MANAGEMENT_PERIOD = 1 days; - uint256 public constant MAX_BLOCKED_BOND_MANAGEMENT_PERIOD = 7 days; - - uint256 public constant TOTAL_BASIS_POINTS = 10000; - - uint256 public immutable COMMON_BOND_SIZE; - ILidoLocator private immutable LIDO_LOCATOR; ICSModule private immutable CSM; IWstETH private immutable WSTETH; @@ -118,31 +102,27 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address public FEE_DISTRIBUTOR; uint256 public totalBondShares; - uint256 public blockedBondRetentionPeriod; - uint256 public blockedBondManagementPeriod; - mapping(uint256 => uint256) internal _bondShares; - mapping(uint256 => BlockedBond) internal _blockedBondEther; - /// This mapping contains bond multiplier points (in basis points) for Node Operator's bond. - /// By default, all Node Operators have x1 multiplier (10000 basis points). - mapping(uint256 => uint256) internal _bondMultiplierBasisPoints; - /// @param commonBondSize common bond size in ETH for all node operators. + /// @param bondCurve initial bond curve /// @param admin admin role member address /// @param lidoLocator lido locator contract address /// @param wstETH wstETH contract address /// @param communityStakingModule community staking module contract address - /// @param _blockedBondRetentionPeriod retention period for blocked bond in seconds - /// @param _blockedBondManagementPeriod management period for blocked bond in seconds + /// @param bondLockRetentionPeriod retention period for locked bond in seconds + /// @param bondLockManagementPeriod management period for locked bond in seconds constructor( - uint256 commonBondSize, + uint256[] memory bondCurve, address admin, address lidoLocator, address wstETH, address communityStakingModule, - uint256 _blockedBondRetentionPeriod, - uint256 _blockedBondManagementPeriod - ) { + uint256 bondLockRetentionPeriod, + uint256 bondLockManagementPeriod + ) + CSBondCurve(bondCurve) + CSBondLock(bondLockRetentionPeriod, bondLockManagementPeriod) + { // check zero addresses require(admin != address(0), "admin is zero address"); require(lidoLocator != address(0), "lido locator is zero address"); @@ -151,20 +131,11 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { "community staking module is zero address" ); require(wstETH != address(0), "wstETH is zero address"); - _validateBlockedBondPeriods( - _blockedBondRetentionPeriod, - _blockedBondManagementPeriod - ); _setupRole(DEFAULT_ADMIN_ROLE, admin); LIDO_LOCATOR = ILidoLocator(lidoLocator); CSM = ICSModule(communityStakingModule); WSTETH = IWstETH(wstETH); - - COMMON_BOND_SIZE = commonBondSize; - - blockedBondRetentionPeriod = _blockedBondRetentionPeriod; - blockedBondManagementPeriod = _blockedBondManagementPeriod; } /// @notice Sets fee distributor contract address. @@ -175,30 +146,33 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { FEE_DISTRIBUTOR = fdAddress; } - /// @notice Sets blocked bond periods. + /// @notice Sets bond lock periods. /// @param retention period in seconds to retain bond lock /// @param management period in seconds to manage bond lock by node operator - function setBlockedBondPeriods( + function setLockedBondPeriods( uint256 retention, uint256 management ) external onlyRole(DEFAULT_ADMIN_ROLE) { - _validateBlockedBondPeriods(retention, management); - blockedBondRetentionPeriod = retention; - blockedBondManagementPeriod = management; + // todo: is it admin role? + _setBondLockPeriods(retention, management); } - function _validateBlockedBondPeriods( - uint256 retention, - uint256 management - ) internal pure { - if ( - retention < MIN_BLOCKED_BOND_RETENTION_PERIOD || - retention > MAX_BLOCKED_BOND_RETENTION_PERIOD || - management < MIN_BLOCKED_BOND_MANAGEMENT_PERIOD || - management > MAX_BLOCKED_BOND_MANAGEMENT_PERIOD - ) { - revert InvalidBlockedBondRetentionPeriod(); - } + /// @notice Sets bond curve. + /// @param bondCurve bond curve to set. + function setBondCurve( + uint256[] memory bondCurve + ) external onlyRole(SET_BOND_CURVE_ROLE) { + _setBondCurve(bondCurve); + } + + /// @notice Sets basis points of the bond multiplier for the given node operator. + /// @param nodeOperatorId id of the node operator to set bond multiplier for. + /// @param basisPoints basis points of the bond multiplier. + function setBondMultiplier( + uint256 nodeOperatorId, + uint256 basisPoints + ) external onlyRole(SET_BOND_MULTIPLIER_ROLE) { + _setBondMultiplier(nodeOperatorId, basisPoints); } /// @notice Pauses accounting by DAO decision. @@ -220,27 +194,6 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { return _bondShares[nodeOperatorId]; } - /// @notice Returns basis points of the bond multiplier for the given node operator. - /// @param nodeOperatorId id of the node operator to get bond multiplier for. - /// @return bond multiplier basis points. - function getBondMultiplier( - uint256 nodeOperatorId - ) public view returns (uint256) { - uint256 basisPoints = _bondMultiplierBasisPoints[nodeOperatorId]; - return basisPoints > 0 ? basisPoints : TOTAL_BASIS_POINTS; - } - - /// @notice Sets basis points of the bond multiplier for the given node operator. - /// @param nodeOperatorId id of the node operator to set bond multiplier for. - /// @param basisPoints bond multiplier basis points. - function setBondMultiplier( - uint256 nodeOperatorId, - uint256 basisPoints - ) external onlyRole(SET_BOND_MULTIPLIER_ROLE) { - if (basisPoints > TOTAL_BASIS_POINTS) revert InvalidMultiplier(); - _bondMultiplierBasisPoints[nodeOperatorId] = basisPoints; - } - // todo: describe `rewardsProof` /// @notice Returns total rewards (bond + fees) in ETH for the given node operator. @@ -254,7 +207,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 cumulativeFeeShares ) public view returns (uint256) { (uint256 current, uint256 required) = _bondSharesSummary( - _getNodeOperatorActiveKeys(nodeOperatorId) + nodeOperatorId ); current += _feeDistributor().getFeesToDistribute( rewardsProof, @@ -359,18 +312,22 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { return WSTETH.getWstETHByStETH(getMissingBondStETH(nodeOperatorId)); } - /// @notice Returns the amount of ETH blocked by the given node operator. - /// @param nodeOperatorId id of the node operator to get blocked bond for. - /// @return blocked bond in ETH. - function getBlockedBondETH( + /// @notice Returns information about the locked bond for the given node operator. + /// @param nodeOperatorId id of the node operator to get locked bond info for. + /// @return locked bond info. + function getLockedBondInfo( + uint256 nodeOperatorId + ) public view returns (CSBondLock.BondLock memory) { + return CSBondLock._get(nodeOperatorId); + } + + /// @notice Returns the amount of locked bond in ETH by the given node operator. + /// @param nodeOperatorId id of the node operator to get locked bond amount. + /// @return amount of locked bond in ETH. + function getActualLockedBondETH( uint256 nodeOperatorId ) public view returns (uint256) { - if ( - _blockedBondEther[nodeOperatorId].retentionUntil >= block.timestamp - ) { - return _blockedBondEther[nodeOperatorId].ETHAmount; - } - return 0; + return CSBondLock._getActualAmount(nodeOperatorId); } /// @notice Returns the required bond in ETH (inc. missed and excess) for the given node operator to upload new keys. @@ -381,22 +338,26 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 additionalKeysCount ) public view returns (uint256) { + // todo: can be optimized. get active keys once (uint256 current, uint256 required) = _bondETHSummary(nodeOperatorId); - uint256 requiredForKeys = (getRequiredBondETHForKeys( - additionalKeysCount - ) * getBondMultiplier(nodeOperatorId)) / TOTAL_BASIS_POINTS; + uint256 currentKeysCount = _getNodeOperatorActiveKeys(nodeOperatorId); + uint256 multiplier = getBondMultiplier(nodeOperatorId); + uint256 requiredForNextKeys = _getBondAmountByKeysCount( + currentKeysCount + additionalKeysCount, + multiplier + ) - _getBondAmountByKeysCount(currentKeysCount, multiplier); uint256 missing = required > current ? required - current : 0; if (missing > 0) { - return missing + requiredForKeys; + return missing + requiredForNextKeys; } uint256 excess = current - required; - if (excess >= requiredForKeys) { + if (excess >= requiredForNextKeys) { return 0; } - return requiredForKeys - excess; + return requiredForNextKeys - excess; } /// @notice Returns the required bond in stETH (inc. missed and excess) for the given node operator to upload new keys. @@ -424,15 +385,19 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { } /// @notice Returns the required bond in ETH for the given number of keys. + /// @dev To calculate the amount for the new keys 2 calls are required: + /// getRequiredBondETHForKeys(newTotal) - getRequiredBondETHForKeys(currentTotal) /// @param keysCount number of keys to get required bond for. /// @return required in ETH. function getRequiredBondETHForKeys( uint256 keysCount ) public view returns (uint256) { - return keysCount * COMMON_BOND_SIZE; + return _getBondAmountByKeysCount(keysCount); } /// @notice Returns the required bond in stETH for the given number of keys. + /// @dev To calculate the amount for the new keys 2 calls are required: + /// getRequiredBondStETHForKeys(newTotal) - getRequiredBondStETHForKeys(currentTotal) /// @param keysCount number of keys to get required bond for. /// @return required in stETH. function getRequiredBondStETHForKeys( @@ -442,18 +407,14 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { } /// @notice Returns the required bond in wstETH for the given number of keys. + /// @dev To calculate the amount for the new keys 2 calls are required: + /// getRequiredBondWstETHForKeys(newTotal) - getRequiredBondWstETHForKeys(currentTotal) /// @param keysCount number of keys to get required bond for. /// @return required in wstETH. function getRequiredBondWstETHForKeys( uint256 keysCount ) public view returns (uint256) { - return _getRequiredBondSharesForKeys(keysCount); - } - - function _getRequiredBondSharesForKeys( - uint256 keysCount - ) internal view returns (uint256) { - return _sharesByEth(getRequiredBondETHForKeys(keysCount)); + return WSTETH.getWstETHByStETH(getRequiredBondStETHForKeys(keysCount)); } /// @dev unbonded meaning amount of keys with no bond at all @@ -463,9 +424,24 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function getUnbondedKeysCount( uint256 nodeOperatorId ) public view returns (uint256) { - return - getRequiredBondETH(nodeOperatorId, 0) / - getRequiredBondETHForKeys(1); + uint256 activeKeys = _getNodeOperatorActiveKeys(nodeOperatorId); + uint256 currentBond = _ethByShares(_bondShares[nodeOperatorId]); + uint256 lockedBond = getActualLockedBondETH(nodeOperatorId); + if (currentBond > lockedBond) { + uint256 multiplier = getBondMultiplier(nodeOperatorId); + currentBond -= lockedBond; + uint256 bondedKeys = _getKeysCountByBondAmount( + currentBond, + multiplier + ); + if ( + currentBond > _getBondAmountByKeysCount(bondedKeys, multiplier) + ) { + bondedKeys += 1; + } + return activeKeys > bondedKeys ? activeKeys - bondedKeys : 0; + } + return activeKeys; } /// @notice Returns the number of keys by the given bond ETH amount @@ -473,7 +449,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function getKeysCountByBondETH( uint256 ETHAmount ) public view returns (uint256) { - return ETHAmount / getRequiredBondETHForKeys(1); + return _getKeysCountByBondAmount(ETHAmount); } /// @notice Returns the number of keys by the given bond stETH amount @@ -481,7 +457,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function getKeysCountByBondStETH( uint256 stETHAmount ) public view returns (uint256) { - return stETHAmount / getRequiredBondStETHForKeys(1); + return getKeysCountByBondETH(stETHAmount); } /// @notice Returns the number of keys by the given bond wstETH amount @@ -489,7 +465,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function getKeysCountByBondWstETH( uint256 wstETHAmount ) public view returns (uint256) { - return wstETHAmount / getRequiredBondWstETHForKeys(1); + return getKeysCountByBondETH(WSTETH.getStETHByWstETH(wstETHAmount)); } /// @notice Stake user's ETH to Lido and make deposit in stETH to the bond @@ -817,112 +793,46 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { onlyRole(EL_REWARDS_STEALING_PENALTY_INIT_ROLE) onlyExistingNodeOperator(nodeOperatorId) { - if (amount == 0) { - revert InvalidStolenAmount(); - } emit ELRewardsStealingPenaltyInitiated( nodeOperatorId, blockNumber, amount ); - _changeBlockedBondState({ - nodeOperatorId: nodeOperatorId, - ETHAmount: _blockedBondEther[nodeOperatorId].ETHAmount + amount, - retentionUntil: block.timestamp + blockedBondRetentionPeriod - }); + CSBondLock._lock(nodeOperatorId, amount); } - /// @notice Releases blocked bond in ETH for the given node operator. - /// @param nodeOperatorId id of the node operator to release blocked bond for. + /// @notice Releases locked bond in ETH for the given node operator. + /// @param nodeOperatorId id of the node operator to release locked bond for. /// @param amount amount of ETH to release. - function releaseBlockedBondETH( + function releaseLockedBondETH( uint256 nodeOperatorId, uint256 amount ) external - onlyRole(EL_REWARDS_STEALING_PENALTY_INIT_ROLE) + onlyRole(EL_REWARDS_STEALING_PENALTY_RELEASE_ROLE) onlyExistingNodeOperator(nodeOperatorId) { - emit BlockedBondReleased(nodeOperatorId, amount); - _reduceBlockedBondETH(nodeOperatorId, amount); + CSBondLock._reduceAmount(nodeOperatorId, amount); + emit BondLockReleased(nodeOperatorId, amount); } - /// @notice Compensates blocked bond ETH for the given node operator. - /// @param nodeOperatorId id of the node operator to compensate blocked bond for. - function compensateBlockedBondETH( + /// @notice Compensates locked bond ETH for the given node operator. + /// @param nodeOperatorId id of the node operator to compensate locked bond for. + function compensateLockedBondETH( uint256 nodeOperatorId ) external payable onlyExistingNodeOperator(nodeOperatorId) { - require(msg.value > 0, "value should be greater than zero"); payable(LIDO_LOCATOR.elRewardsVault()).transfer(msg.value); - emit BlockedBondCompensated(nodeOperatorId, msg.value); - _reduceBlockedBondETH(nodeOperatorId, msg.value); + CSBondLock._reduceAmount(nodeOperatorId, msg.value); + emit BondLockCompensated(nodeOperatorId, msg.value); } - function _reduceBlockedBondETH( - uint256 nodeOperatorId, - uint256 amount - ) internal { - uint256 blocked = getBlockedBondETH(nodeOperatorId); - require(blocked > 0, "no blocked bond to release"); - require( - _blockedBondEther[nodeOperatorId].ETHAmount >= amount, - "blocked bond is less than amount to release" - ); - _changeBlockedBondState( - nodeOperatorId, - _blockedBondEther[nodeOperatorId].ETHAmount - amount, - _blockedBondEther[nodeOperatorId].retentionUntil - ); - } - - /// @notice Settles blocked bond for the given node operators. - /// @dev Should be called by the committee. Doesn't settle blocked bond if it is in the safe frame (1 day) - /// @param nodeOperatorIds ids of the node operators to settle blocked bond for. - function settleBlockedBondETH( + /// @dev Should be called by the committee. Doesn't settle locked bond if it is in the safe frame (1 day) + /// @notice Settles locked bond for the given node operators. + /// @param nodeOperatorIds ids of the node operators to settle locked bond for. + function settleLockedBondETH( uint256[] memory nodeOperatorIds ) external onlyRole(EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE) { - for (uint256 i; i < nodeOperatorIds.length; ++i) { - uint256 nodeOperatorId = nodeOperatorIds[i]; - BlockedBond storage blockedBond = _blockedBondEther[nodeOperatorId]; - if ( - block.timestamp + - blockedBondRetentionPeriod - - blockedBond.retentionUntil < - blockedBondManagementPeriod - ) { - // blocked bond in safe frame to manage it by committee or node operator - continue; - } - uint256 uncovered; - if ( - blockedBond.ETHAmount > 0 && - blockedBond.retentionUntil >= block.timestamp - ) { - uncovered = _penalize(nodeOperatorId, blockedBond.ETHAmount); - } - _changeBlockedBondState({ - nodeOperatorId: nodeOperatorId, - ETHAmount: uncovered, - retentionUntil: blockedBond.retentionUntil - }); - } - } - - function _changeBlockedBondState( - uint256 nodeOperatorId, - uint256 ETHAmount, - uint256 retentionUntil - ) internal { - if (ETHAmount == 0) { - delete _blockedBondEther[nodeOperatorId]; - emit BlockedBondChanged(nodeOperatorId, 0, 0); - return; - } - _blockedBondEther[nodeOperatorId] = BlockedBond({ - ETHAmount: ETHAmount, - retentionUntil: retentionUntil - }); - emit BlockedBondChanged(nodeOperatorId, ETHAmount, retentionUntil); + CSBondLock._settle(nodeOperatorIds); } /// @notice Burn all bond and request exits for all node operators' validators. @@ -953,9 +863,14 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function _penalize( uint256 nodeOperatorId, - uint256 ETHAmount - ) internal onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { - uint256 penaltyShares = _sharesByEth(ETHAmount); + uint256 amount + ) + internal + override + onlyExistingNodeOperator(nodeOperatorId) + returns (uint256) + { + uint256 penaltyShares = _sharesByEth(amount); uint256 currentShares = getBondShares(nodeOperatorId); uint256 sharesToBurn = penaltyShares < currentShares ? penaltyShares @@ -1034,10 +949,11 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { ) internal view returns (uint256 current, uint256 required) { current = _ethByShares(getBondShares(nodeOperatorId)); required = - ((getRequiredBondETHForKeys( - _getNodeOperatorActiveKeys(nodeOperatorId) - ) * getBondMultiplier(nodeOperatorId)) / TOTAL_BASIS_POINTS) + - getBlockedBondETH(nodeOperatorId); + _getBondAmountByKeysCount( + _getNodeOperatorActiveKeys(nodeOperatorId), + getBondMultiplier(nodeOperatorId) + ) + + getActualLockedBondETH(nodeOperatorId); } function _bondSharesSummary( @@ -1045,10 +961,13 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { ) internal view returns (uint256 current, uint256 required) { current = getBondShares(nodeOperatorId); required = - ((_getRequiredBondSharesForKeys( - _getNodeOperatorActiveKeys(nodeOperatorId) - ) * getBondMultiplier(nodeOperatorId)) / TOTAL_BASIS_POINTS) + - _sharesByEth(getBlockedBondETH(nodeOperatorId)); + _sharesByEth( + _getBondAmountByKeysCount( + _getNodeOperatorActiveKeys(nodeOperatorId), + getBondMultiplier(nodeOperatorId) + ) + ) + + _sharesByEth(getActualLockedBondETH(nodeOperatorId)); } function _sharesByEth(uint256 ethAmount) internal view returns (uint256) { diff --git a/src/CSBondCurve.sol b/src/CSBondCurve.sol new file mode 100644 index 00000000..8d89b97a --- /dev/null +++ b/src/CSBondCurve.sol @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.21; + +abstract contract CSBondCurveBase { + event BondCurveChanged(uint256[] bondCurve); + event BondMultiplierChanged( + uint256 indexed nodeOperatorId, + uint256 basisPoints + ); +} + +abstract contract CSBondCurve is CSBondCurveBase { + /// @dev Array of bond amounts for particular keys count. + /// + /// For example: + /// Array Index |> 0 1 2 i + /// Bond Amount |> [ 2 ETH ] [ 3.9 ETH ] [ 5.7 ETH ] [ ... ] + /// Keys Count |> 1 2 3 i + 1 + /// + /// Bond Amount (ETH) + /// ^ + /// | + /// 6 - + /// | ------------------- 5.9 ETH -->.. + /// 5.5 - . ^ + /// | . | + /// 5 - . | + /// | . | + /// 4.5 - . | + /// | . | + /// 4 - .. | + /// | -------- 3.9 ETH -->.. | + /// 3.5 - .^ | + /// | .. | | + /// 3 - .. | | + /// | . | | + /// 2.5 - . | | + /// | .. | | + /// 2 - -------->.. | | + /// | ^ | | + /// |----------|----------|----------|----------|----> Keys Count + /// | 1 2 3 i + /// + uint256[] public bondCurve; + + /// @dev This mapping contains bond multiplier points (in basis points) for Node Operator's bond. + /// By default, all Node Operators have x1 multiplier (10000 basis points). + /// + /// For example: + /// There is a bond curve as above ^ + /// Some Node Operator has x0.90 bond multiplier (9000 basis points) + /// Bond Curve with multiplier for this Node Operator will be: + /// + /// Bond Amount (ETH) + /// ^ + /// | + /// 4 - + /// | ------------------- 3.6 ETH -->. + /// 3.5 - .. ^ + /// | .. | + /// 3 - .. | + /// | -------- 2.7 ETH -->... | + /// 2.5 - .. | | + /// | .. | | + /// 2 - .. | | + /// | 1.8 ETH->... | | + /// 1.5 - ^ | | + /// | | | | + /// 1 - | | | + /// |----------|----------|----------|----------|----> Keys Count + /// | 1 2 3 i + /// + mapping(uint256 => uint256) public bondMultiplierBP; + + // todo: might be redefined in the future + uint256 internal constant MAX_CURVE_LENGTH = 20; + uint256 internal constant MIN_CURVE_LENGTH = 1; + + uint256 internal constant BASIS_POINTS = 10000; + uint256 internal constant MAX_BOND_MULTIPLIER = BASIS_POINTS; // x1 + uint256 internal constant MIN_BOND_MULTIPLIER = MAX_BOND_MULTIPLIER / 2; // x0.5 + + uint256 internal _bondCurveTrend; + + constructor(uint256[] memory _bondCurve) { + _setBondCurve(_bondCurve); + } + + function _setBondCurve(uint256[] memory _bondCurve) internal { + if ( + _bondCurve.length < MIN_CURVE_LENGTH || + _bondCurve.length > MAX_CURVE_LENGTH + ) revert InvalidBondCurveLength(); + // todo: check curve values (not worse than previous and makes sense) + if (_bondCurve[0] == 0) revert InvalidBondCurveValues(); + for (uint256 i = 1; i < _bondCurve.length; i++) { + if (_bondCurve[i] <= _bondCurve[i - 1]) + revert InvalidBondCurveValues(); + } + bondCurve = _bondCurve; + _bondCurveTrend = + _bondCurve[_bondCurve.length - 1] - + // if the curve length is 1, then 0 is used as the previous value to calculate the trend + (_bondCurve.length > 1 ? _bondCurve[_bondCurve.length - 2] : 0); + emit BondCurveChanged(_bondCurve); + } + + function _setBondMultiplier( + uint256 nodeOperatorId, + uint256 basisPoints + ) internal { + if ( + basisPoints < MIN_BOND_MULTIPLIER || + basisPoints > MAX_BOND_MULTIPLIER + ) revert InvalidMultiplier(); + // todo: check curve values (not worse than previous) + bondMultiplierBP[nodeOperatorId] = basisPoints; + emit BondMultiplierChanged(nodeOperatorId, basisPoints); + } + + /// @notice Returns basis points of the bond multiplier for the given node operator. + /// if it isn't set, the multiplier is x1 (MAX_BOND_MULTIPLIER) + function getBondMultiplier( + uint256 nodeOperatorId + ) public view returns (uint256) { + uint256 basisPoints = bondMultiplierBP[nodeOperatorId]; + return basisPoints > 0 ? basisPoints : MAX_BOND_MULTIPLIER; + } + + /// @notice Returns keys count for the given bond amount. + function _getKeysCountByBondAmount( + uint256 amount + ) internal view returns (uint256) { + return _getKeysCountByBondAmount(amount, MAX_BOND_MULTIPLIER); + } + + /// @notice Returns keys count for the given bond amount for particular node operator. + function _getKeysCountByBondAmount( + uint256 amount, + uint256 multiplier + ) internal view returns (uint256) { + if (amount < (bondCurve[0] * multiplier) / BASIS_POINTS) return 0; + uint256 maxCurveAmount = (bondCurve[bondCurve.length - 1] * + multiplier) / BASIS_POINTS; + if (amount >= maxCurveAmount) { + return + bondCurve.length + + ((amount - maxCurveAmount) / + ((_bondCurveTrend * multiplier) / BASIS_POINTS)); + } + return _searchKeysCount(amount, multiplier); + } + + function _searchKeysCount( + uint256 amount, + uint256 multiplier + ) internal view returns (uint256) { + uint256 low; + uint256 high = bondCurve.length - 1; + while (low <= high) { + uint256 mid = (low + high) / 2; + uint256 midAmount = (bondCurve[mid] * multiplier) / BASIS_POINTS; + if (amount == midAmount) { + return mid + 1; + } + if (amount < midAmount) { + // zero mid is avoided above + high = mid - 1; + } else if (amount > midAmount) { + low = mid + 1; + } + } + return low; + } + + function _getBondAmountByKeysCount( + uint256 keys + ) internal view returns (uint256) { + return _getBondAmountByKeysCount(keys, MAX_BOND_MULTIPLIER); + } + + function _getBondAmountByKeysCount( + uint256 keys, + uint256 multiplier + ) internal view returns (uint256) { + if (keys == 0) return 0; + if (keys <= bondCurve.length) { + return (bondCurve[keys - 1] * multiplier) / BASIS_POINTS; + } + return + ((bondCurve[bondCurve.length - 1] * multiplier) / BASIS_POINTS) + + (keys - bondCurve.length) * + ((_bondCurveTrend * multiplier) / BASIS_POINTS); + } + + error InvalidBondCurveLength(); + error InvalidBondCurveValues(); + error InvalidMultiplier(); +} diff --git a/src/CSBondLock.sol b/src/CSBondLock.sol new file mode 100644 index 00000000..14d5076a --- /dev/null +++ b/src/CSBondLock.sol @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.21; + +abstract contract CSBondLockBase { + event BondLockChanged( + uint256 indexed nodeOperatorId, + uint256 newAmount, + uint256 retentionUntil + ); + event BondLockPeriodsChanged( + uint256 retentionPeriod, + uint256 managementPeriod + ); + + error InvalidBondLockPeriods(); + error InvalidBondLockAmount(); +} + +abstract contract CSBondLock is CSBondLockBase { + struct BondLock { + uint256 amount; + uint256 retentionUntil; + } + + // todo: should be reconsidered + uint256 public constant MIN_BOND_LOCK_RETENTION_PERIOD = 4 weeks; + uint256 public constant MAX_BOND_LOCK_RETENTION_PERIOD = 365 days; + uint256 public constant MIN_BOND_LOCK_MANAGEMENT_PERIOD = 1 days; + uint256 public constant MAX_BOND_LOCK_MANAGEMENT_PERIOD = 7 days; + + uint256 internal _bondLockRetentionPeriod; + uint256 internal _bondLockManagementPeriod; + + mapping(uint256 => BondLock) internal _bondLock; + + constructor(uint256 retentionPeriod, uint256 managementPeriod) { + _setBondLockPeriods(retentionPeriod, managementPeriod); + } + + function _setBondLockPeriods( + uint256 retention, + uint256 management + ) internal { + _validateBondLockPeriods(retention, management); + _bondLockRetentionPeriod = retention; + _bondLockManagementPeriod = management; + emit BondLockPeriodsChanged(retention, management); + } + + function getBondLockPeriods() + external + view + returns (uint256 retention, uint256 management) + { + return (_bondLockRetentionPeriod, _bondLockManagementPeriod); + } + + function _validateBondLockPeriods( + uint256 retention, + uint256 management + ) internal pure { + if ( + retention < MIN_BOND_LOCK_RETENTION_PERIOD || + retention > MAX_BOND_LOCK_RETENTION_PERIOD || + management < MIN_BOND_LOCK_MANAGEMENT_PERIOD || + management > MAX_BOND_LOCK_MANAGEMENT_PERIOD + ) { + revert InvalidBondLockPeriods(); + } + } + + /// @notice Returns the amount and retention time of locked bond by the given node operator. + function _get( + uint256 nodeOperatorId + ) internal view returns (BondLock memory) { + return _bondLock[nodeOperatorId]; + } + + /// @notice Returns the amount of locked bond by the given node operator. + function _getActualAmount( + uint256 nodeOperatorId + ) internal view returns (uint256) { + if (_bondLock[nodeOperatorId].retentionUntil >= block.timestamp) { + return _bondLock[nodeOperatorId].amount; + } + return 0; + } + + /// @notice Reports EL rewards stealing for the given node operator. + /// @param nodeOperatorId id of the node operator to lock bond for. + /// @param amount amount to lock. + function _lock(uint256 nodeOperatorId, uint256 amount) internal { + if (amount == 0) { + revert InvalidBondLockAmount(); + } + _changeBondLock({ + nodeOperatorId: nodeOperatorId, + amount: _bondLock[nodeOperatorId].amount + amount, + retentionUntil: block.timestamp + _bondLockRetentionPeriod + }); + } + + /// @dev Should be called by the committee. Doesn't settle blocked bond if it is in the safe frame (1 day) + /// @notice Settles blocked bond for the given node operators. + /// @param nodeOperatorIds ids of the node operators to settle blocked bond for. + function _settle(uint256[] memory nodeOperatorIds) internal { + for (uint256 i; i < nodeOperatorIds.length; ++i) { + uint256 nodeOperatorId = nodeOperatorIds[i]; + BondLock storage bondLock = _bondLock[nodeOperatorId]; + if ( + block.timestamp + + _bondLockRetentionPeriod - + bondLock.retentionUntil < + _bondLockManagementPeriod + ) { + // blocked bond in safe frame to manage it by committee or node operator + continue; + } + uint256 uncovered; + if ( + bondLock.amount > 0 && + bondLock.retentionUntil >= block.timestamp + ) { + uncovered = _penalize(nodeOperatorId, bondLock.amount); + } + _changeBondLock({ + nodeOperatorId: nodeOperatorId, + amount: uncovered, + retentionUntil: bondLock.retentionUntil + }); + } + } + + function _reduceAmount(uint256 nodeOperatorId, uint256 amount) internal { + uint256 blocked = _getActualAmount(nodeOperatorId); + if (amount == 0) { + revert InvalidBondLockAmount(); + } + if (blocked < amount) { + revert InvalidBondLockAmount(); + } + _changeBondLock( + nodeOperatorId, + _bondLock[nodeOperatorId].amount - amount, + _bondLock[nodeOperatorId].retentionUntil + ); + } + + function _changeBondLock( + uint256 nodeOperatorId, + uint256 amount, + uint256 retentionUntil + ) private { + if (amount == 0) { + delete _bondLock[nodeOperatorId]; + emit BondLockChanged(nodeOperatorId, 0, 0); + return; + } + _bondLock[nodeOperatorId] = BondLock({ + amount: amount, + retentionUntil: retentionUntil + }); + emit BondLockChanged(nodeOperatorId, amount, retentionUntil); + } + + function _penalize( + uint256 nodeOperatorId, + uint256 amount + ) internal virtual returns (uint256); +} diff --git a/src/CSModule.sol b/src/CSModule.sol index b73e831c..52f08cdd 100644 --- a/src/CSModule.sol +++ b/src/CSModule.sol @@ -7,7 +7,7 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; import { ICSAccounting } from "./interfaces/ICSAccounting.sol"; -import { IStakingModule } from "./interfaces/IStakingModule.sol"; +import { ICSModule } from "./interfaces/ICSModule.sol"; import { ILidoLocator } from "./interfaces/ILidoLocator.sol"; import { ILido } from "./interfaces/ILido.sol"; @@ -36,17 +36,6 @@ struct NodeOperator { uint256 queueNonce; } -struct NodeOperatorInfo { - bool active; - address managerAddress; - address rewardAddress; - uint256 totalVettedValidators; - uint256 totalExitedValidators; - uint256 totalWithdrawnValidators; - uint256 totalAddedValidators; - uint256 totalDepositedValidators; -} - contract CSModuleBase { event NodeOperatorAdded(uint256 indexed nodeOperatorId, address from); event NodeOperatorManagerAddressChangeProposed( @@ -129,7 +118,7 @@ contract CSModuleBase { error SigningKeysInvalidOffset(); } -contract CSModule is IStakingModule, CSModuleBase { +contract CSModule is ICSModule, CSModuleBase { using QueueLib for QueueLib.Queue; // @dev max number of node operators is limited by uint64 due to Batch serialization in 32 bytes diff --git a/test/CSAccounting.blockedBond.t.sol b/test/CSAccounting.blockedBond.t.sol deleted file mode 100644 index b6c5121e..00000000 --- a/test/CSAccounting.blockedBond.t.sol +++ /dev/null @@ -1,735 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Lido -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.21; - -import "forge-std/Test.sol"; - -import { CSAccountingBase, CSAccounting } from "../src/CSAccounting.sol"; -import { PermitTokenBase } from "./helpers/Permit.sol"; -import { Stub } from "./helpers/mocks/Stub.sol"; -import { LidoMock } from "./helpers/mocks/LidoMock.sol"; -import { WstETHMock } from "./helpers/mocks/WstETHMock.sol"; -import { LidoLocatorMock } from "./helpers/mocks/LidoLocatorMock.sol"; -import { CommunityStakingModuleMock } from "./helpers/mocks/CommunityStakingModuleMock.sol"; -import { CommunityStakingFeeDistributorMock } from "./helpers/mocks/CommunityStakingFeeDistributorMock.sol"; -import { WithdrawalQueueMockBase, WithdrawalQueueMock } from "./helpers/mocks/WithdrawalQueueMock.sol"; - -import { Fixtures } from "./helpers/Fixtures.sol"; - -contract CSAccounting_revealed is CSAccounting { - constructor( - uint256 commonBondSize, - address admin, - address lidoLocator, - address wstETH, - address communityStakingModule, - uint256 blockedBondRetentionPeriod, - uint256 blockedBondManagementPeriod - ) - CSAccounting( - commonBondSize, - admin, - lidoLocator, - wstETH, - communityStakingModule, - blockedBondRetentionPeriod, - blockedBondManagementPeriod - ) - {} - - function _bondShares_set_value( - uint256 nodeOperatorId, - uint256 value - ) public { - _bondShares[nodeOperatorId] = value; - } - - function _blockedBondEther_get_value( - uint256 nodeOperatorId - ) public view returns (BlockedBond memory) { - return _blockedBondEther[nodeOperatorId]; - } - - function _blockedBondEther_set_value( - uint256 nodeOperatorId, - BlockedBond memory value - ) public { - _blockedBondEther[nodeOperatorId] = value; - } - - function _changeBlockedBondState_revealed( - uint256 nodeOperatorId, - uint256 ETHAmount, - uint256 retentionUntil - ) public { - _changeBlockedBondState(nodeOperatorId, ETHAmount, retentionUntil); - } - - function _reduceBlockedBondETH_revealed( - uint256 nodeOperatorId, - uint256 ETHAmount - ) public { - _reduceBlockedBondETH(nodeOperatorId, ETHAmount); - } -} - -contract CSAccountingTest is Test, Fixtures, CSAccountingBase { - using stdStorage for StdStorage; - - LidoLocatorMock internal locator; - WstETHMock internal wstETH; - LidoMock internal stETH; - - Stub internal burner; - - CSAccounting_revealed public accounting; - CommunityStakingModuleMock public stakingModule; - CommunityStakingFeeDistributorMock public feeDistributor; - - address internal admin; - address internal user; - address internal stranger; - - function setUp() public { - admin = address(1); - - user = address(2); - stranger = address(777); - - (locator, wstETH, stETH, burner) = initLido(); - - stakingModule = new CommunityStakingModuleMock(); - accounting = new CSAccounting_revealed( - 2 ether, - admin, - address(locator), - address(wstETH), - address(stakingModule), - 8 weeks, - 1 days - ); - feeDistributor = new CommunityStakingFeeDistributorMock( - address(locator), - address(accounting) - ); - vm.startPrank(admin); - accounting.setFeeDistributor(address(feeDistributor)); - accounting.grantRole(accounting.INSTANT_PENALIZE_BOND_ROLE(), admin); - accounting.grantRole( - accounting.EL_REWARDS_STEALING_PENALTY_INIT_ROLE(), - admin - ); - accounting.grantRole( - accounting.EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE(), - admin - ); - vm.stopPrank(); - } - - function test_getBlockedBondETH() public { - uint256 noId = 0; - uint256 amount = 1 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - assertEq(accounting.getBlockedBondETH(noId), amount); - - // retentionUntil is not passed yet - vm.warp(retentionUntil); - assertEq(accounting.getBlockedBondETH(noId), amount); - - // the next block after retentionUntil - vm.warp(retentionUntil + 12); - assertEq(accounting.getBlockedBondETH(noId), 0); - } - - function test_getRequiredBondETH_withBlockedBond() public { - uint256 noId = 0; - uint256 amount = 100500 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - assertEq(accounting.getRequiredBondETH(noId, 0), amount); - - // the next block after retentionUntil - vm.warp(retentionUntil + 12); - assertEq(accounting.getRequiredBondETH(noId, 0), 0); - } - - function test_getExcessBondETH_withBlockedBond() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 amount = 100500 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - vm.deal(user, 12 ether); - vm.startPrank(user); - stETH.submit{ value: 12 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 12 ether); - vm.stopPrank(); - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - assertEq(accounting.getExcessBondETH(noId), 0); - - // the next block after retentionUntil - vm.warp(retentionUntil + 12); - assertApproxEqAbs(accounting.getExcessBondETH(0), 10 ether, 1); - } - - function test_claimRewardStETH_withBlockedBond() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 amount = 100500 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - vm.deal(user, 12 ether); - vm.startPrank(user); - stETH.submit{ value: 12 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 12 ether); - vm.stopPrank(); - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed(0, user, 0); - - vm.prank(user); - accounting.claimRewardsStETH(new bytes32[](0), noId, 0, UINT256_MAX); - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: 1 ether, - retentionUntil: retentionUntil - }) - ); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed(0, user, 9 ether + 1 wei); - - vm.prank(user); - accounting.claimRewardsStETH(new bytes32[](0), noId, 0, UINT256_MAX); - } - - function test_claimRewardsWstETH_withBlockedBond() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 amount = 100500 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - vm.deal(user, 12 ether); - vm.startPrank(user); - stETH.submit{ value: 12 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 12 ether); - vm.stopPrank(); - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHRewardsClaimed(0, user, 0); - - vm.prank(user); - accounting.claimRewardsWstETH(new bytes32[](0), noId, 0, UINT256_MAX); - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: 1 ether, - retentionUntil: retentionUntil - }) - ); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHRewardsClaimed( - 0, - user, - stETH.getSharesByPooledEth(9 ether + 1 wei) - ); - - vm.prank(user); - accounting.claimRewardsWstETH(new bytes32[](0), noId, 0, UINT256_MAX); - } - - function test_requestRewardsETH_withBlockedBond() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 amount = 100500 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._bondShares_set_value(0, 100 ether); - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit ETHRewardsRequested(0, user, 0); - - vm.prank(user); - accounting.requestRewardsETH(new bytes32[](0), noId, 0, UINT256_MAX); - } - - function test_private_changeBlockedBondState() public { - uint256 noId = 0; - uint256 amount = 1 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(noId, amount, retentionUntil); - accounting._changeBlockedBondState_revealed({ - nodeOperatorId: noId, - ETHAmount: amount, - retentionUntil: retentionUntil - }); - - CSAccounting.BlockedBond memory value = accounting - ._blockedBondEther_get_value(noId); - - assertEq(value.ETHAmount, amount); - assertEq(value.retentionUntil, retentionUntil); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(noId, 0, 0); - - accounting._changeBlockedBondState_revealed({ - nodeOperatorId: noId, - ETHAmount: 0, - retentionUntil: 0 - }); - - value = accounting._blockedBondEther_get_value(noId); - - assertEq(value.ETHAmount, 0); - assertEq(value.retentionUntil, 0); - } - - function test_initELRewardsStealingPenalty() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 proposedBlockNumber = 100500; - uint256 firstStolenAmount = 1 ether; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit ELRewardsStealingPenaltyInitiated( - noId, - proposedBlockNumber, - firstStolenAmount - ); - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged( - noId, - firstStolenAmount, - block.timestamp + 8 weeks - ); - - vm.prank(admin); - accounting.initELRewardsStealingPenalty({ - nodeOperatorId: noId, - blockNumber: proposedBlockNumber, - amount: firstStolenAmount - }); - - assertEq( - accounting._blockedBondEther_get_value(noId).ETHAmount, - firstStolenAmount - ); - assertEq( - accounting._blockedBondEther_get_value(noId).retentionUntil, - block.timestamp + 8 weeks - ); - - // new block and new stealing - vm.warp(block.timestamp + 12 seconds); - - uint256 secondStolenAmount = 2 ether; - proposedBlockNumber = 100501; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit ELRewardsStealingPenaltyInitiated( - noId, - proposedBlockNumber, - secondStolenAmount - ); - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged( - noId, - firstStolenAmount + secondStolenAmount, - block.timestamp + 8 weeks - ); - - vm.prank(admin); - accounting.initELRewardsStealingPenalty({ - nodeOperatorId: noId, - blockNumber: proposedBlockNumber, - amount: secondStolenAmount - }); - - assertEq( - accounting._blockedBondEther_get_value(noId).ETHAmount, - firstStolenAmount + secondStolenAmount - ); - assertEq( - accounting._blockedBondEther_get_value(noId).retentionUntil, - block.timestamp + 8 weeks - ); - } - - function test_initELRewardsStealingPenalty_revertWhenNonExistingOperator() - public - { - vm.expectRevert("node operator does not exist"); - - vm.prank(admin); - accounting.initELRewardsStealingPenalty({ - nodeOperatorId: 0, - blockNumber: 100500, - amount: 100 ether - }); - } - - function test_initELRewardsStealingPenalty_revertWhenZero() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - vm.expectRevert(InvalidStolenAmount.selector); - - vm.prank(admin); - accounting.initELRewardsStealingPenalty({ - nodeOperatorId: 0, - blockNumber: 100500, - amount: 0 - }); - } - - function test_initELRewardsStealingPenalty_revertWhenNoRole() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f" - ); - - accounting.initELRewardsStealingPenalty({ - nodeOperatorId: 0, - blockNumber: 100500, - amount: 100 ether - }); - } - - function test_settleBlockedBondETH() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - vm.deal(user, 12 ether); - vm.startPrank(user); - stETH.submit{ value: 12 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 12 ether); - vm.stopPrank(); - - uint256[] memory nosToPenalize = new uint256[](2); - nosToPenalize[0] = 0; - // non-existing node operator should be skipped in the loop - nosToPenalize[1] = 100500; - - uint256 retentionUntil = block.timestamp + 8 weeks; - - accounting._blockedBondEther_set_value( - 0, - CSAccounting.BlockedBond({ - ETHAmount: 1 ether, - retentionUntil: retentionUntil - }) - ); - - // less than 1 day after penalty init - vm.warp(block.timestamp + 20 hours); - - vm.prank(admin); - accounting.settleBlockedBondETH(nosToPenalize); - - CSAccounting.BlockedBond memory value = accounting - ._blockedBondEther_get_value(0); - - assertEq(value.ETHAmount, 1 ether); - assertEq(value.retentionUntil, retentionUntil); - - // penalty amount is less than the bond - vm.warp(block.timestamp + 2 days); - - uint256 penalty = stETH.getPooledEthByShares( - stETH.getSharesByPooledEth(1 ether) - ); - uint256 covering = penalty; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BondPenalized(0, penalty, covering); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(0, 0, 0); - - vm.prank(admin); - accounting.settleBlockedBondETH(nosToPenalize); - - value = accounting._blockedBondEther_get_value(0); - assertEq(value.ETHAmount, 0); - assertEq(value.retentionUntil, 0); - - // penalty amount is greater than the bond - accounting._blockedBondEther_set_value( - 0, - CSAccounting.BlockedBond({ - ETHAmount: 100 ether, - retentionUntil: retentionUntil - }) - ); - - penalty = stETH.getPooledEthByShares( - stETH.getSharesByPooledEth(100 ether) - ); - covering = 11 ether; - uint256 uncovered = penalty - covering; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BondPenalized(0, penalty, covering); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(0, uncovered, retentionUntil); - - vm.prank(admin); - accounting.settleBlockedBondETH(nosToPenalize); - - value = accounting._blockedBondEther_get_value(0); - assertEq(value.ETHAmount, uncovered); - assertEq(value.retentionUntil, retentionUntil); - - // retention period expired - accounting._blockedBondEther_set_value( - 0, - CSAccounting.BlockedBond({ - ETHAmount: 100 ether, - retentionUntil: retentionUntil - }) - ); - vm.warp(retentionUntil + 12); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(0, 0, 0); - - vm.prank(admin); - accounting.settleBlockedBondETH(nosToPenalize); - - value = accounting._blockedBondEther_get_value(0); - assertEq(value.ETHAmount, 0); - assertEq(value.retentionUntil, 0); - } - - function test_private_reduceBlockedBondETH() public { - uint256 noId = 0; - uint256 amount = 100 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - // part of blocked bond is released - uint256 toReduce = 10 ether; - uint256 rest = amount - toReduce; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(noId, rest, retentionUntil); - - accounting._reduceBlockedBondETH_revealed(noId, toReduce); - - CSAccounting.BlockedBond memory value = accounting - ._blockedBondEther_get_value(noId); - - assertEq(value.ETHAmount, rest); - assertEq(value.retentionUntil, retentionUntil); - - // all blocked bond is released - toReduce = rest; - rest = 0; - retentionUntil = 0; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(noId, rest, retentionUntil); - - accounting._reduceBlockedBondETH_revealed(noId, toReduce); - - value = accounting._blockedBondEther_get_value(noId); - - assertEq(value.ETHAmount, rest); - assertEq(value.retentionUntil, retentionUntil); - } - - function test_private_reduceBlockedBondETH_revertWhenNoBlocked() public { - vm.expectRevert("no blocked bond to release"); - accounting._reduceBlockedBondETH_revealed(0, 1 ether); - } - - function test_private_reduceBlockedBondETH_revertWhenAmountGreaterThanBlocked() - public - { - uint256 noId = 0; - uint256 amount = 100 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - vm.expectRevert("blocked bond is less than amount to release"); - accounting._reduceBlockedBondETH_revealed(0, 101 ether); - } - - function test_releaseBlockedBondETH() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 amount = 100 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - uint256 toRelease = 10 ether; - uint256 rest = amount - toRelease; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondReleased(noId, toRelease); - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(noId, rest, retentionUntil); - - vm.prank(admin); - accounting.releaseBlockedBondETH(noId, toRelease); - } - - function test_releaseBlockedBondETH_revertWhenNonExistingOperator() public { - vm.expectRevert("node operator does not exist"); - - vm.prank(admin); - accounting.releaseBlockedBondETH(0, 1 ether); - } - - function test_releaseBlockedBondETH_revertWhenNoRole() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f" - ); - accounting.releaseBlockedBondETH(0, 1 ether); - } - - function test_compensateBlockedBondETH() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - uint256 noId = 0; - uint256 amount = 100 ether; - uint256 retentionUntil = block.timestamp + 1 weeks; - - accounting._blockedBondEther_set_value( - noId, - CSAccounting.BlockedBond({ - ETHAmount: amount, - retentionUntil: retentionUntil - }) - ); - - uint256 toCompensate = 10 ether; - uint256 rest = amount - toCompensate; - - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondCompensated(noId, toCompensate); - vm.expectEmit(true, true, true, true, address(accounting)); - emit BlockedBondChanged(noId, rest, retentionUntil); - - vm.deal(user, toCompensate); - vm.prank(user); - accounting.compensateBlockedBondETH{ value: toCompensate }(noId); - - assertEq(address(locator.elRewardsVault()).balance, toCompensate); - } - - function test_compensateBlockedBondETH_revertWhenZero() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - - vm.expectRevert("value should be greater than zero"); - accounting.compensateBlockedBondETH{ value: 0 }(0); - } - - function test_compensateBlockedBondETH_revertWhenNonExistingOperator() - public - { - vm.expectRevert("node operator does not exist"); - accounting.compensateBlockedBondETH{ value: 1 ether }(0); - } - - function _createNodeOperator( - uint64 ongoingVals, - uint64 withdrawnVals - ) internal { - stakingModule.setNodeOperator({ - _nodeOperatorId: 0, - _active: true, - _rewardAddress: user, - _totalVettedValidators: ongoingVals, - _totalExitedValidators: 0, - _totalWithdrawnValidators: withdrawnVals, - _totalAddedValidators: ongoingVals, - _totalDepositedValidators: ongoingVals - }); - } -} diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index b320313d..06189268 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -5,21 +5,65 @@ pragma solidity 0.8.21; import "forge-std/Test.sol"; +import { ICSModule } from "../src/interfaces/ICSModule.sol"; +import { IStakingModule } from "../src/interfaces/IStakingModule.sol"; + import { CSAccountingBase, CSAccounting } from "../src/CSAccounting.sol"; +import { CSBondLock } from "../src/CSBondLock.sol"; +import { CSBondCurve } from "../src/CSBondCurve.sol"; import { PermitTokenBase } from "./helpers/Permit.sol"; import { Stub } from "./helpers/mocks/Stub.sol"; import { LidoMock } from "./helpers/mocks/LidoMock.sol"; import { WstETHMock } from "./helpers/mocks/WstETHMock.sol"; import { LidoLocatorMock } from "./helpers/mocks/LidoLocatorMock.sol"; -import { CommunityStakingModuleMock } from "./helpers/mocks/CommunityStakingModuleMock.sol"; import { CommunityStakingFeeDistributorMock } from "./helpers/mocks/CommunityStakingFeeDistributorMock.sol"; import { WithdrawalQueueMockBase, WithdrawalQueueMock } from "./helpers/mocks/WithdrawalQueueMock.sol"; +import { Utilities } from "./helpers/Utilities.sol"; import { Fixtures } from "./helpers/Fixtures.sol"; -contract CSAccountingTest is +// todo: non-existing node operator tests +// todo: bond lock permission tests +// todo: bond lock emit event tests + +contract CSAccountingForTests is CSAccounting { + constructor( + uint256[] memory bondCurve, + address admin, + address lidoLocator, + address wstETH, + address communityStakingModule, + uint256 lockedBondRetentionPeriod, + uint256 lockedBondManagementPeriod + ) + CSAccounting( + bondCurve, + admin, + lidoLocator, + wstETH, + communityStakingModule, + lockedBondRetentionPeriod, + lockedBondManagementPeriod + ) + {} + + function setBondCurve_ForTest(uint256[] memory curve) public { + _setBondCurve(curve); + } + + function setBondMultiplier_ForTest(uint256 id, uint256 multiplier) public { + _setBondMultiplier(id, multiplier); + } + + function setBondLock_ForTest(uint256 id, uint256 amount) public { + CSBondLock._lock(id, amount); + } +} + +contract CSAccountingBaseTest is Test, Fixtures, + Utilities, PermitTokenBase, CSAccountingBase, WithdrawalQueueMockBase @@ -31,15 +75,15 @@ contract CSAccountingTest is Stub internal burner; - CSAccounting public accounting; - CommunityStakingModuleMock public stakingModule; + CSAccountingForTests public accounting; + Stub public stakingModule; CommunityStakingFeeDistributorMock public feeDistributor; address internal admin; address internal user; address internal stranger; - function setUp() public { + function setUp() public virtual { admin = address(1); user = address(2); @@ -47,9 +91,11 @@ contract CSAccountingTest is (locator, wstETH, stETH, burner) = initLido(); - stakingModule = new CommunityStakingModuleMock(); - accounting = new CSAccounting( - 2 ether, + stakingModule = new Stub(); + uint256[] memory curve = new uint256[](1); + curve[0] = 2 ether; + accounting = new CSAccountingForTests( + curve, admin, address(locator), address(wstETH), @@ -72,1167 +118,4651 @@ contract CSAccountingTest is accounting.EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE(), admin ); + accounting.grantRole(accounting.SET_BOND_CURVE_ROLE(), admin); + accounting.grantRole(accounting.SET_BOND_MULTIPLIER_ROLE(), admin); vm.stopPrank(); } - function test_totalBondShares() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - accounting.depositETH{ value: 32 ether }(user, 0); - uint256 sharesToDeposit = stETH.getSharesByPooledEth(32 ether); - assertEq(accounting.totalBondShares(), sharesToDeposit); + function mock_getNodeOperatorsCount(uint256 returnValue) internal { + vm.mockCall( + address(stakingModule), + abi.encodeWithSelector( + IStakingModule.getNodeOperatorsCount.selector + ), + abi.encode(returnValue) + ); } - function test_getRequiredBondETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - assertEq(accounting.getRequiredBondETH(0, 0), 32 ether); + function mock_getNodeOperator( + ICSModule.NodeOperatorInfo memory returnValue + ) internal { + vm.mockCall( + address(stakingModule), + abi.encodeWithSelector(ICSModule.getNodeOperator.selector, 0), + abi.encode(returnValue) + ); } - function test_getRequiredBondStETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - assertEq(accounting.getRequiredBondStETH(0, 0), 32 ether); + function mock_getNodeOperator() internal { + ICSModule.NodeOperatorInfo memory n; + n.active = true; + n.managerAddress = address(user); + n.rewardAddress = address(user); + n.totalVettedValidators = 16; + n.totalExitedValidators = 0; + n.totalWithdrawnValidators = 0; + n.totalAddedValidators = 16; + n.totalDepositedValidators = 16; + mock_getNodeOperator(n); } - function test_getRequiredBondWstETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - assertEq( - accounting.getRequiredBondWstETH(0, 0), - stETH.getSharesByPooledEth(32 ether) - ); + function mock_getNodeOperatorsCount() internal { + mock_getNodeOperatorsCount(1); } +} - function test_getRequiredBondETH_OneWithdrawnValidator() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 1 }); - assertEq(accounting.getRequiredBondETH(0, 0), 30 ether); +abstract contract BondAmountModifiersTest { + // 1 key -> 2 ether + // 2 keys -> 4 ether + // n keys -> 2 + (n - 1) * 2 ether + function test_default() public virtual; + + // 1 key -> 2 ether + // 2 keys -> 3 ether + // n keys -> 2 + (n - 1) * 1 ether + function test_WithCurve() public virtual; + + // 1 key -> 1.8 ether + // 2 keys -> 3.6 ether + // n keys -> 1.8 + (n - 1) * 1.8 ether + function test_WithMultiplier() public virtual; + + // 1 key -> 2 ether + 1 ether + // 2 keys -> 4 ether + 1 ether + // n keys -> 2 + (n - 1) * 2 ether + 1 ether + function test_WithLocked() public virtual; + + // 1 key -> 1.8 ether + // 2 keys -> 2.7 ether + // n keys -> 1.8 + (n - 1) * 0.9 ether + function test_WithCurveAndMultiplier() public virtual; + + // 1 key -> 2 ether + 1 ether + // 2 keys -> 3 ether + 1 ether + // n keys -> 2 + (n - 1) * 1 ether + 1 ether + function test_WithCurveAndLocked() public virtual; + + // 1 key -> 1.8 ether + 1 ether + // 2 keys -> 3.6 ether + 1 ether + // n keys -> 1.8 + (n - 1) * 1.8 ether + 1 ether + function test_WithMultiplierAndLocked() public virtual; + + // 1 key -> 1.8 ether + 1 ether + // 2 keys -> 2.7 ether + 1 ether + // n keys -> 1.8 + (n - 1) * 0.9 ether + 1 ether + function test_WithCurveAndMultiplierAndLocked() public virtual; +} + +abstract contract CSAccountingBondStateBaseTest is + BondAmountModifiersTest, + CSAccountingBaseTest +{ + function _operator(uint256 ongoing, uint256 withdrawn) internal virtual { + ICSModule.NodeOperatorInfo memory n; + n.active = true; + n.managerAddress = address(user); + n.rewardAddress = address(user); + n.totalVettedValidators = ongoing; + n.totalExitedValidators = 0; + n.totalWithdrawnValidators = withdrawn; + n.totalAddedValidators = ongoing; + n.totalDepositedValidators = ongoing; + mock_getNodeOperator(n); + mock_getNodeOperatorsCount(1); } - function test_getRequiredBondStETH_OneWithdrawnValidator() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 1 }); - assertEq(accounting.getRequiredBondStETH(0, 0), 30 ether); + function _deposit(uint256 bond) internal virtual { + vm.deal(user, bond); + vm.prank(user); + accounting.depositETH{ value: bond }(user, 0); } - function test_getRequiredBondWstETH_OneWithdrawnValidator() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 1 }); - assertEq( - accounting.getRequiredBondWstETH(0, 0), - stETH.getSharesByPooledEth(30 ether) - ); + uint256[] public defaultCurve = [2 ether, 3 ether]; + + function _curve(uint256[] memory curve) internal virtual { + accounting.setBondCurve_ForTest(curve); } - function test_getRequiredBondETH_OneWithdrawnOneAddedValidator() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 1 }); - assertEq(accounting.getRequiredBondETH(0, 1), 32 ether); + function _multiplier(uint256 id, uint256 multiplier) internal virtual { + accounting.setBondMultiplier_ForTest(id, multiplier); } - function test_getRequiredBondStETH_OneWithdrawnOneAddedValidator() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 1 }); - assertEq(accounting.getRequiredBondStETH(0, 1), 32 ether); + function _lock(uint256 id, uint256 amount) internal virtual { + accounting.setBondLock_ForTest(id, amount); } - function test_getRequiredBondWstETH_OneWithdrawnOneAddedValidator() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 1 }); - assertEq( - accounting.getRequiredBondWstETH(0, 1), - stETH.getSharesByPooledEth(32 ether) - ); + function test_WithOneWithdrawnValidator() public virtual; + + function test_WithBond() public virtual; + + function test_WithBondAndOneWithdrawnValidator() public virtual; + + function test_WithExcessBond() public virtual; + + function test_WithExcessBondAndOneWithdrawnValidator() public virtual; + + function test_WithMissingBond() public virtual; + + function test_WithMissingBondAndOneWithdrawnValidator() public virtual; +} + +contract CSAccountingGetExcessBondETHTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 1 ether, 1 wei); } - function test_getRequiredBondETH_WithExcessBond() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 64 ether); - vm.startPrank(user); - accounting.depositETH{ value: 63 ether }(user, 0); - assertApproxEqAbs( - accounting.getRequiredBondETH(0, 16), - 1 ether, - 1, // max accuracy error - "required ETH should be ~1 ether for the next 16 validators to deposit" - ); + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + assertApproxEqAbs(accounting.getExcessBondETH(0), 16 ether, 1 wei); } - function test_getRequiredBondStETH_WithExcessBond() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 64 ether); - vm.startPrank(user); - stETH.submit{ value: 64 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 63 ether); - assertApproxEqAbs( - accounting.getRequiredBondStETH(0, 16), - 1 ether, - 1, // max accuracy error - "required stETH should be ~1 ether for the next 16 validators to deposit" - ); + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 4.2 ether, 1 wei); } - function test_getRequiredBondWstETH_WithExcessBond() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 64 ether); - vm.startPrank(user); - stETH.submit{ value: 64 ether }({ _referal: address(0) }); - uint256 amount = wstETH.wrap(63 ether); - accounting.depositWstETH(user, 0, amount); - assertApproxEqAbs( - accounting.getRequiredBondWstETH(0, 16), - stETH.getSharesByPooledEth(1 ether), - 2, // max accuracy error - "required wstETH should be ~1 ether for the next 16 validators to deposit" - ); + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 0 ether, 1 wei); } - function test_getRequiredBondETHForKeys() public { - assertEq(accounting.getRequiredBondETHForKeys(1), 2 ether); + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 17.7 ether, 1 wei); } - function test_getRequiredBondStETHForKeys() public { - assertEq(accounting.getRequiredBondStETHForKeys(1), 2 ether); + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 15 ether, 1 wei); } - function test_getRequiredBondWstETHForKeys() public { - assertEq( - accounting.getRequiredBondWstETHForKeys(1), - stETH.getSharesByPooledEth(2 ether) - ); + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 3.2 ether, 1 wei); } - function test_depositETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - uint256 sharesToDeposit = stETH.getSharesByPooledEth(32 ether); + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 16.7 ether, 1 wei); + } - vm.expectEmit(true, true, true, true, address(accounting)); - emit ETHBondDeposited(0, user, 32 ether); + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 2 ether, 1 wei); + } - vm.prank(user); - accounting.depositETH{ value: 32 ether }(user, 0); + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getExcessBondETH(0), 0); + } - assertEq( - address(user).balance, - 0, - "user balance should be 0 after deposit" - ); - assertEq( - accounting.getBondShares(0), - sharesToDeposit, - "bond shares should be equal to deposited shares" - ); - assertEq( - stETH.sharesOf(address(accounting)), - sharesToDeposit, - "bond manager shares should be equal to deposited shares" - ); + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 2 ether, 1 wei); } - function test_depositETH_CoverSeveralValidators() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - vm.deal(user, 32 ether); + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 64 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 32 ether, 1 wei); + } - uint256 required = accounting.getRequiredBondETHForKeys(1); - vm.startPrank(user); - accounting.depositETH{ value: required }(user, 0); + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 64 ether }); + assertApproxEqAbs(accounting.getExcessBondETH(0), 34 ether, 1 wei); + } - assertApproxEqAbs( - accounting.getRequiredBondETH(0, 0), - 0, - 1, // max accuracy error - "required ETH should be ~0 for 1 deposited validator" - ); + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertEq(accounting.getExcessBondETH(0), 0); + } - required = accounting.getRequiredBondETH(0, 1); - accounting.depositETH{ value: required }(user, 0); - stakingModule.addValidator(0, 1); + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertEq(accounting.getExcessBondETH(0), 0); + } +} - assertApproxEqAbs( - accounting.getRequiredBondETH(0, 0), - 0, - 1, // max accuracy error - "required ETH should be ~0 for 2 deposited validators" - ); +contract CSAccountingGetExcessBondStETHTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 1 ether, 1 wei); } - function test_depositStETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - uint256 sharesToDeposit = stETH.submit{ value: 32 ether }({ - _referal: address(0) - }); + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 16 ether, 1 wei); + } - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHBondDeposited(0, user, 32 ether); + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 4.2 ether, 1 wei); + } - accounting.depositStETH(user, 0, 32 ether); + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 0 ether, 1 wei); + } - assertEq( - stETH.balanceOf(user), - 0, - "user balance should be 0 after deposit" - ); - assertEq( - accounting.getBondShares(0), - sharesToDeposit, - "bond shares should be equal to deposited shares" - ); - assertEq( - stETH.sharesOf(address(accounting)), - sharesToDeposit, - "bond manager shares should be equal to deposited shares" - ); + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 17.7 ether, 1 wei); } - function test_depositStETH_CoverSeveralValidators() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 15 ether, 1 wei); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 3.2 ether, 1 wei); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 16.7 ether, 1 wei); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 2 ether, 1 wei); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getExcessBondStETH(0), 0); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 2 ether, 1 wei); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 64 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 32 ether, 1 wei); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 64 ether }); + assertApproxEqAbs(accounting.getExcessBondStETH(0), 34 ether, 1 wei); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertEq(accounting.getExcessBondStETH(0), 0); + } - uint256 required = accounting.getRequiredBondStETHForKeys(1); - accounting.depositStETH(user, 0, required); + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertEq(accounting.getExcessBondStETH(0), 0); + } +} +contract CSAccountingGetExcessBondWstETHTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); assertApproxEqAbs( - accounting.getRequiredBondStETH(0, 0), - 0, - 1, // max accuracy error - "required stETH should be ~0 for 1 deposited validator" + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(1 ether), + 1 wei ); + } - required = accounting.getRequiredBondStETH(0, 1); - accounting.depositStETH(user, 0, required); - stakingModule.addValidator(0, 1); + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); assertApproxEqAbs( - accounting.getRequiredBondStETH(0, 0), - 0, - 1, // max accuracy error - "required stETH should be ~0 for 2 deposited validators" + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(16 ether), + 1 wei ); } - function test_depositWstETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - uint256 wstETHAmount = wstETH.wrap(32 ether); - uint256 sharesToDeposit = stETH.getSharesByPooledEth( - wstETH.getStETHByWstETH(wstETHAmount) + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(4.2 ether), + 1 wei ); + } - vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHBondDeposited(0, user, wstETHAmount); + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(0 ether), + 1 wei + ); + } - accounting.depositWstETH(user, 0, wstETHAmount); + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(17.7 ether), + 1 wei + ); + } - assertEq( - wstETH.balanceOf(user), - 0, - "user balance should be 0 after deposit" + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(15 ether), + 1 wei ); - assertEq( - accounting.getBondShares(0), - sharesToDeposit, - "bond shares should be equal to deposited shares" + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(3.2 ether), + 1 wei ); - assertEq( - stETH.sharesOf(address(accounting)), - sharesToDeposit, - "bond manager shares should be equal to deposited shares" + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(16.7 ether), + 1 wei ); } - function test_depositWstETH_CoverSeveralValidators() public { - _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - vm.startPrank(user); - vm.deal(user, 32 ether); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - wstETH.wrap(32 ether); + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(2 ether), + 1 wei + ); + } - uint256 required = accounting.getRequiredBondWstETHForKeys(1); - accounting.depositWstETH(user, 0, required); + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getExcessBondWstETH(0), 0); + } + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); assertApproxEqAbs( - accounting.getRequiredBondWstETH(0, 0), - 0, - 1, // max accuracy error - "required wstETH should be ~0 for 1 deposited validator" + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(2 ether), + 1 wei ); + } - required = accounting.getRequiredBondStETH(0, 1); - accounting.depositWstETH(user, 0, required); - stakingModule.addValidator(0, 1); + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 64 ether }); + assertApproxEqAbs( + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(32 ether), + 1 wei + ); + } + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 64 ether }); assertApproxEqAbs( - accounting.getRequiredBondWstETH(0, 0), - 0, - 1, // max accuracy error - "required wstETH should be ~0 for 2 deposited validators" + accounting.getExcessBondWstETH(0), + wstETH.getWstETHByStETH(34 ether), + 1 wei ); } - function test_depositStETHWithPermit() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.prank(user); - uint256 sharesToDeposit = stETH.submit{ value: 32 ether }({ - _referal: address(0) - }); + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertEq(accounting.getExcessBondWstETH(0), 0); + } - vm.expectEmit(true, true, true, true, address(stETH)); - emit Approval(user, address(accounting), 32 ether); - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHBondDeposited(0, user, 32 ether); + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertEq(accounting.getExcessBondWstETH(0), 0); + } +} - vm.prank(user); - accounting.depositStETHWithPermit( - user, - 0, - 32 ether, - CSAccounting.PermitInput({ - value: 32 ether, - deadline: type(uint256).max, - // mock permit signature - v: 0, - r: 0, - s: 0 - }) +contract CSAccountingGetMissingBondETHTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 16 ether, 1 wei); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + assertApproxEqAbs(accounting.getMissingBondETH(0), 1 ether, 1 wei); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 12.8 ether, 1 wei); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 17 ether, 1 wei); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getMissingBondETH(0), 0); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 2 ether, 1 wei); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 13.8 ether, 1 wei); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 0.3 ether, 1 wei); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 14 ether, 1 wei); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 0, 1 wei); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getMissingBondETH(0), 0 ether); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 64 ether }); + assertEq(accounting.getMissingBondETH(0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 64 ether }); + assertEq(accounting.getMissingBondETH(0), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 8 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 24 ether, 2 wei); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 8 ether }); + assertApproxEqAbs(accounting.getMissingBondETH(0), 22 ether, 2 wei); + } +} + +contract CSAccountingGetMissingBondStETHTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 16 ether, 1 wei); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 1 ether, 1 wei); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 12.8 ether, 1 wei); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 17 ether, 1 wei); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getMissingBondStETH(0), 0); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 2 ether, 1 wei); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 13.8 ether, 1 wei); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 0.3 ether, 1 wei); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 14 ether, 1 wei); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 0, 1 wei); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getMissingBondStETH(0), 0 ether); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 64 ether }); + assertEq(accounting.getMissingBondStETH(0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 64 ether }); + assertEq(accounting.getMissingBondStETH(0), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 8 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 24 ether, 2 wei); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 8 ether }); + assertApproxEqAbs(accounting.getMissingBondStETH(0), 22 ether, 2 wei); + } +} + +contract CSAccountingGetMissingBondWstETHTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(16 ether), + 1 wei + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(1 ether), + 1 wei + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(12.8 ether), + 1 wei + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(17 ether), + 1 wei + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(0) + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(2 ether), + 1 wei + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(13.8 ether), + 1 wei + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(0.3 ether), + 1 wei + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertEq( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(14 ether) + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getMissingBondWstETH(0), 0); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertEq( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(0 ether) + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 64 ether }); + assertEq(accounting.getMissingBondWstETH(0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 64 ether }); + assertEq(accounting.getMissingBondWstETH(0), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 8 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(24 ether), + 2 wei + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 8 ether }); + assertApproxEqAbs( + accounting.getMissingBondWstETH(0), + wstETH.getWstETHByStETH(22 ether), + 1 wei + ); + } +} + +contract CSAccountingGetUnbondedKeysCountTest is CSAccountingBondStateBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 10); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _curve(defaultCurve); + assertEq(accounting.getUnbondedKeysCount(0), 5); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getUnbondedKeysCount(0), 9); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 10); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getUnbondedKeysCount(0), 4); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 6); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 10); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 5); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 11.5 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 9); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 11.5 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 10); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 11.5 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 9); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 5.75 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 13); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 5.75 ether }); + assertEq(accounting.getUnbondedKeysCount(0), 12); + } +} + +abstract contract CSAccountingGetRequiredBondBaseTest is + CSAccountingBondStateBaseTest +{ + function test_OneWithdrawnOneAddedValidator() public virtual; + + function test_WithBondAndOneWithdrawnAndOneAddedValidator() public virtual; + + function test_WithExcessBondAndOneWithdrawnAndOneAddedValidator() + public + virtual; + + function test_WithMissingBondAndOneWithdrawnAndOneAddedValidator() + public + virtual; +} + +contract CSAccountingGetRequiredETHBondTest is + CSAccountingGetRequiredBondBaseTest +{ + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + assertEq(accounting.getRequiredBondETH(0, 0), 32 ether); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + assertEq(accounting.getRequiredBondETH(0, 0), 17 ether); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getRequiredBondETH(0, 0), 28.8 ether); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 33 ether); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getRequiredBondETH(0, 0), 15.3 ether); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 18 ether); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 29.8 ether); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 16.3 ether); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + assertEq(accounting.getRequiredBondETH(0, 0), 30 ether); + } + + function test_OneWithdrawnOneAddedValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + assertEq(accounting.getRequiredBondETH(0, 1), 32 ether); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getRequiredBondETH(0, 0), 0, 1 wei); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 0); + } + + function test_WithBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getRequiredBondETH(0, 1), 0, 1); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondETH(0, 0), 0); + } + + function test_WithExcessBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondETH(0, 1), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getRequiredBondETH(0, 0), 16 ether, 1 wei); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getRequiredBondETH(0, 0), 14 ether, 1 wei); + } + + function test_WithMissingBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs(accounting.getRequiredBondETH(0, 1), 16 ether, 1 wei); + } +} + +contract CSAccountingGetRequiredStETHBondTest is + CSAccountingGetRequiredBondBaseTest +{ + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + assertEq(accounting.getRequiredBondStETH(0, 0), 32 ether); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + assertEq(accounting.getRequiredBondStETH(0, 0), 17 ether); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getRequiredBondStETH(0, 0), 28.8 ether); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 33 ether); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq(accounting.getRequiredBondStETH(0, 0), 15.3 ether); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 18 ether); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 29.8 ether); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 16.3 ether); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + assertEq(accounting.getRequiredBondStETH(0, 0), 30 ether); + } + + function test_OneWithdrawnOneAddedValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + assertEq(accounting.getRequiredBondStETH(0, 1), 32 ether); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getRequiredBondStETH(0, 0), 0, 1 wei); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 0); + } + + function test_WithBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getRequiredBondStETH(0, 1), 0, 1); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondStETH(0, 0), 0); + } + + function test_WithExcessBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondStETH(0, 1), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs( + accounting.getRequiredBondStETH(0, 0), + 16 ether, + 1 wei + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs( + accounting.getRequiredBondStETH(0, 0), + 14 ether, + 1 wei + ); + } + + function test_WithMissingBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs( + accounting.getRequiredBondStETH(0, 1), + 16 ether, + 1 wei + ); + } +} + +contract CSAccountingGetRequiredWstETHBondTest is + CSAccountingGetRequiredBondBaseTest +{ + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(32 ether) + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(17 ether) + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(28.8 ether) + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(33 ether) + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(15.3 ether) + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(18 ether) + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(29.8 ether) + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(16.3 ether) + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(30 ether) + ); + } + + function test_OneWithdrawnOneAddedValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + assertEq( + accounting.getRequiredBondWstETH(0, 1), + stETH.getSharesByPooledEth(32 ether) + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getRequiredBondWstETH(0, 0), 0, 1 wei); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertEq(accounting.getRequiredBondWstETH(0, 0), 0); + } + + function test_WithBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + assertApproxEqAbs(accounting.getRequiredBondWstETH(0, 1), 0, 1); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondWstETH(0, 0), 0); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondWstETH(0, 0), 0); + } + + function test_WithExcessBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + assertEq(accounting.getRequiredBondWstETH(0, 1), 0); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(16 ether), + 1 wei + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertEq( + accounting.getRequiredBondWstETH(0, 0), + stETH.getSharesByPooledEth(14 ether) + ); + } + + function test_WithMissingBondAndOneWithdrawnAndOneAddedValidator() + public + override + { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + assertApproxEqAbs( + accounting.getRequiredBondWstETH(0, 1), + stETH.getSharesByPooledEth(16 ether), + 1 wei + ); + } +} + +abstract contract CSAccountingGetRequiredBondForKeysBaseTest is + CSAccountingBaseTest +{ + uint256[] public defaultCurve = [2 ether, 3 ether]; + + function _curve(uint256[] memory curve) internal virtual { + accounting.setBondCurve_ForTest(curve); + } + + function test_default() public virtual; + + function test_WithCurve() public virtual; +} + +contract CSAccountingGetRequiredBondETHForKeysTest is + CSAccountingGetRequiredBondForKeysBaseTest +{ + function test_default() public override { + assertEq(accounting.getRequiredBondETHForKeys(0), 0); + assertEq(accounting.getRequiredBondETHForKeys(1), 2 ether); + assertEq(accounting.getRequiredBondETHForKeys(2), 4 ether); + } + + function test_WithCurve() public override { + _curve(defaultCurve); + assertEq(accounting.getRequiredBondETHForKeys(0), 0); + assertEq(accounting.getRequiredBondETHForKeys(1), 2 ether); + assertEq(accounting.getRequiredBondETHForKeys(2), 3 ether); + } +} + +contract CSAccountingGetRequiredBondStETHForKeysTest is + CSAccountingGetRequiredBondForKeysBaseTest +{ + function test_default() public override { + assertEq(accounting.getRequiredBondStETHForKeys(0), 0); + assertEq(accounting.getRequiredBondStETHForKeys(1), 2 ether); + assertEq(accounting.getRequiredBondStETHForKeys(2), 4 ether); + } + + function test_WithCurve() public override { + _curve(defaultCurve); + assertEq(accounting.getRequiredBondETHForKeys(0), 0); + assertEq(accounting.getRequiredBondStETHForKeys(1), 2 ether); + assertEq(accounting.getRequiredBondStETHForKeys(2), 3 ether); + } +} + +contract CSAccountingGetRequiredBondWstETHForKeysTest is + CSAccountingGetRequiredBondForKeysBaseTest +{ + function test_default() public override { + assertEq(accounting.getRequiredBondWstETHForKeys(0), 0); + assertEq( + accounting.getRequiredBondWstETHForKeys(1), + stETH.getSharesByPooledEth(2 ether) + ); + assertEq( + accounting.getRequiredBondWstETHForKeys(2), + stETH.getSharesByPooledEth(4 ether) + ); + } + + function test_WithCurve() public override { + _curve(defaultCurve); + assertEq(accounting.getRequiredBondWstETHForKeys(0), 0); + assertEq( + accounting.getRequiredBondWstETHForKeys(1), + stETH.getSharesByPooledEth(2 ether) + ); + assertEq( + accounting.getRequiredBondWstETHForKeys(2), + stETH.getSharesByPooledEth(3 ether) + ); + } +} + +contract CSAccountingGetKeysCountByBondETHTest is + CSAccountingGetRequiredBondForKeysBaseTest +{ + function test_default() public override { + assertEq(accounting.getKeysCountByBondETH(0), 0); + assertEq(accounting.getKeysCountByBondETH(1.99 ether), 0); + assertEq(accounting.getKeysCountByBondETH(2 ether), 1); + assertEq(accounting.getKeysCountByBondETH(4 ether), 2); + assertEq(accounting.getKeysCountByBondETH(16 ether), 8); + } + + function test_WithCurve() public override { + _curve(defaultCurve); + assertEq(accounting.getKeysCountByBondETH(0), 0); + assertEq(accounting.getKeysCountByBondETH(1.99 ether), 0); + assertEq(accounting.getKeysCountByBondETH(2 ether), 1); + assertEq(accounting.getKeysCountByBondETH(3 ether), 2); + assertEq(accounting.getKeysCountByBondETH(16 ether), 15); + } +} + +contract CSAccountingGetKeysCountByBondStETHTest is + CSAccountingGetRequiredBondForKeysBaseTest +{ + function test_default() public override { + assertEq(accounting.getKeysCountByBondStETH(0), 0); + assertEq(accounting.getKeysCountByBondStETH(1.99 ether), 0); + assertEq(accounting.getKeysCountByBondStETH(2 ether), 1); + assertEq(accounting.getKeysCountByBondStETH(4 ether), 2); + assertEq(accounting.getKeysCountByBondETH(16 ether), 8); + } + + function test_WithCurve() public override { + _curve(defaultCurve); + assertEq(accounting.getKeysCountByBondStETH(0), 0); + assertEq(accounting.getKeysCountByBondStETH(1.99 ether), 0); + assertEq(accounting.getKeysCountByBondStETH(2 ether), 1); + assertEq(accounting.getKeysCountByBondStETH(3 ether), 2); + assertEq(accounting.getKeysCountByBondETH(16 ether), 15); + } +} + +contract CSAccountingGetKeysCountByBondWstETHTest is + CSAccountingGetRequiredBondForKeysBaseTest +{ + function test_default() public override { + assertEq(accounting.getKeysCountByBondWstETH(0), 0); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(1.99 ether) + ), + 0 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(2 ether + 1 wei) + ), + 1 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(4 ether + 1 wei) + ), + 2 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(16 ether + 1 wei) + ), + 8 + ); + } + + function test_WithCurve() public override { + _curve(defaultCurve); + assertEq(accounting.getKeysCountByBondWstETH(0), 0); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(1.99 ether) + ), + 0 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(2 ether + 1 wei) + ), + 1 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(4 ether) + ), + 2 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(16 ether + 1 wei) + ), + 15 + ); + } +} + +abstract contract CSAccountingRewardsBaseTest is CSAccountingBondStateBaseTest { + struct RewardsLeaf { + bytes32[] proof; + uint256 nodeOperatorId; + uint256 shares; + } + + RewardsLeaf leaf; + + uint256 sharesAsFee; + uint256 stETHAsFee; + uint256 wstETHAsFee; + uint256 unstETHAsFee; + uint256 unstETHSharesAsFee; + + function setUp() public override { + super.setUp(); + mock_getNodeOperator(); + mock_getNodeOperatorsCount(); + } + + function _deposit(uint256 bond, uint256 fee) internal { + // Deposit bond for node operator + vm.deal(user, bond); + vm.prank(user); + accounting.depositETH{ value: bond }(user, 0); + // Set validator fee rewards + vm.deal(address(feeDistributor), fee); + vm.prank(address(feeDistributor)); + sharesAsFee = stETH.submit{ value: fee }(address(0)); + stETHAsFee = stETH.getPooledEthByShares(sharesAsFee); + wstETHAsFee = wstETH.getWstETHByStETH(stETHAsFee); + unstETHAsFee = stETH.getPooledEthByShares(sharesAsFee); + unstETHSharesAsFee = stETH.getSharesByPooledEth(unstETHAsFee); + leaf = RewardsLeaf({ + proof: new bytes32[](1), + nodeOperatorId: 0, + shares: sharesAsFee + }); + } +} + +contract CSAccountingGetTotalRewardsETHTest is CSAccountingRewardsBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 0 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 15 ether + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 3.2 ether + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 16.7 ether + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 14 ether, + 1 wei + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 2.2 ether, + 1 wei + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 15.7 ether, + 1 wei + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 0 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 ether + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + ); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 2 ether + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 1 ether + ); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 3 ether, + 1 wei + ); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } +} + +contract CSAccountingGetTotalRewardsStETHTest is CSAccountingRewardsBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 0 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 15 ether + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 3.2 ether + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 16.7 ether + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 14 ether, + 1 wei + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 2.2 ether, + 1 wei + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 15.7 ether, + 1 wei + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 0 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 ether + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + ); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 2 ether + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 1 ether + ); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + stETHAsFee + 3 ether, + 1 wei + ); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } +} + +contract CSAccountingGetTotalRewardsWstETHTest is CSAccountingRewardsBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 0 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 15 ether) + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 3.2 ether) + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 16.7 ether) + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 14 ether), + 1 wei + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 2.2 ether), + 1 wei + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 15.7 ether), + 1 wei + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 0 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 ether + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee) + ); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 2 ether) + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 1 ether) + ); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + assertApproxEqAbs( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + wstETH.getWstETHByStETH(stETHAsFee + 3 ether), + 1 wei + ); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + assertEq( + accounting.getTotalRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares + ), + 0 + ); + } +} + +abstract contract CSAccountingClaimRewardsBaseTest is + CSAccountingRewardsBaseTest +{ + function test_EventEmitted() public virtual; + + function test_WithDesirableValue() public virtual; + + function test_RevertWhen_NotOwner() public virtual; +} + +contract CSAccountingClaimStETHRewardsTest is CSAccountingClaimRewardsBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee, + "user balance should be equal to fee reward" + ); + assertEq( + bondSharesAfter, + bondSharesBefore, + "bond shares after claim should be equal to before" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesBefore, + "bond manager after claim should be equal to before" + ); + assertEq( + accounting.totalBondShares(), + bondSharesBefore, + "total bond shares after claim should be equal to before" + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee + 15 ether, + "user balance should be equal to fee reward plus excess bond after curve" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(15 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after curve" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee + 3.2 ether, + "user balance should be equal to fee reward plus excess bond after multiplier" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(3.2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after multiplier" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + 0, + "user balance should be equal to zero" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore + sharesAsFee, + 1 wei, + "bond shares after claim should be equal to before plus fee shares" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee + 16.7 ether, + "user balance should be equal to fee reward plus excess bond after curve and multiplier" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(16.7 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after curve and multiplier" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertApproxEqAbs( + stETH.balanceOf(address(user)), + stETHAsFee + 14 ether, + 1 wei, + "user balance should be equal to fee reward plus excess bond after curve minus locked" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(14 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after curve minus locked" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertApproxEqAbs( + stETH.balanceOf(address(user)), + stETHAsFee + 2.2 ether, + 1 wei, + "user balance should be equal to fee reward plus excess bond after multiplier minus locked" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2.2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after multiplier minus locked" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertApproxEqAbs( + stETH.balanceOf(address(user)), + stETHAsFee + 15.7 ether, + 1 wei, + "user balance should be equal to fee reward plus excess bond after curve and multiplier minus locked" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(15.7 ether), + 2 wei, + "bond shares after claim should be equal to before minus excess bond after curve and multiplier minus locked" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertApproxEqAbs( + stETH.balanceOf(address(user)), + stETHAsFee + 2 ether, + 1 wei, + "user balance should be equal to fee reward plus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee, + "user balance should be equal to fee reward" + ); + assertEq( + bondSharesAfter, + bondSharesBefore, + "bond shares after claim should be equal to before" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee + 2 ether, + "user balance should be equal to fee reward plus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHAsFee + 1 ether, + "user balance should be equal to fee reward plus excess bond" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(1 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertApproxEqAbs( + stETH.balanceOf(address(user)), + stETHAsFee + 3 ether, + 1 wei, + "user balance should be equal to fee reward plus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(3 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + 0, + "user balance should be equal to zero" + ); + assertEq( + bondSharesAfter, + bondSharesBefore + sharesAsFee, + "bond shares after claim should be equal to before plus fee shares" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + 0, + "user balance should be equal to zero" + ); + assertEq( + bondSharesAfter, + bondSharesBefore + sharesAsFee, + "bond shares after claim should be equal to before plus fee shares" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_EventEmitted() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + vm.expectEmit(true, true, true, true, address(accounting)); + emit StETHRewardsClaimed( + leaf.nodeOperatorId, + user, + stETH.getPooledEthByShares(sharesAsFee) + ); + + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + } + + function test_WithDesirableValue() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 sharesToClaim = stETH.getSharesByPooledEth(0.05 ether); + uint256 stETHToClaim = stETH.getPooledEthByShares(sharesToClaim); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + 0.05 ether + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + stETH.balanceOf(address(user)), + stETHToClaim, + "user balance should be equal to claimed" + ); + assertEq( + bondSharesAfter, + (bondSharesBefore + sharesAsFee) - sharesToClaim, + "bond shares after should be equal to before and fee minus claimed shares" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after should be equal to before and fee minus claimed shares" + ); + assertEq(accounting.totalBondShares(), bondSharesAfter); + } + + function test_RevertWhen_NotOwner() public override { + vm.expectRevert( + abi.encodeWithSelector(NotOwnerToClaim.selector, stranger, user) + ); + vm.prank(stranger); + accounting.claimRewardsStETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + } +} + +contract CSAccountingClaimWstETHRewardsTest is + CSAccountingClaimRewardsBaseTest +{ + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertEq( + wstETH.balanceOf(address(user)), + wstETHAsFee, + "user balance should be equal to fee reward" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore, + 1 wei, + "bond shares after claim should be equal to before" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to after" + ); + } + + function test_WithCurve() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertEq( + wstETH.balanceOf(address(user)), + wstETH.getWstETHByStETH(stETHAsFee + 15 ether), + "user balance should be equal to fee reward plus excess bond after curve" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(15 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after curve" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertEq( + wstETH.balanceOf(address(user)), + wstETH.getWstETHByStETH(stETHAsFee + 3.2 ether), + "user balance should be equal to fee reward plus excess bond after multiplier" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(3.2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after multiplier" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertEq( + wstETH.balanceOf(address(user)), + 0, + "user balance should be equal to zero" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore + sharesAsFee, + 1 wei, + "bond shares after claim should be equal to before plus fee shares" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertEq( + wstETH.balanceOf(address(user)), + wstETH.getWstETHByStETH(stETHAsFee + 16.7 ether), + "user balance should be equal to fee reward plus excess bond after curve and multiplier" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(16.7 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after curve and multiplier" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETH.getWstETHByStETH(stETHAsFee + 14 ether), + 1 wei, + "user balance should be equal to fee reward plus excess bond after curve minus locked" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(14 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after curve minus locked" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETH.getWstETHByStETH(stETHAsFee + 2.2 ether), + 1 wei, + "user balance should be equal to fee reward plus excess bond after multiplier minus locked" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2.2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after multiplier minus locked" + ); assertEq( - stETH.balanceOf(user), + wstETH.balanceOf(address(accounting)), 0, - "user balance should be 0 after deposit" + "bond manager wstETH balance should be 0" ); assertEq( - accounting.getBondShares(0), - sharesToDeposit, - "bond shares should be equal to deposited shares" + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETH.getWstETHByStETH(stETHAsFee + 15.7 ether), + 1 wei, + "user balance should be equal to fee reward plus excess bond after curve and multiplier minus locked" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(15.7 ether), + 2 wei, + "bond shares after claim should be equal to before minus excess bond after curve and multiplier minus locked" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" ); assertEq( stETH.sharesOf(address(accounting)), - sharesToDeposit, - "bond manager shares should be equal to deposited shares" + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" ); } - function test_depositStETHWithPermit_alreadyPermitted() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHBondDeposited(0, user, 32 ether); + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETHAsFee + stETH.getSharesByPooledEth(2 ether), + 1 wei, + "user balance should be equal to fee reward plus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } - vm.mockCall( - address(stETH), - abi.encodeWithSelector( - stETH.allowance.selector, - user, - address(accounting) - ), - abi.encode(32 ether) + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); - vm.recordLogs(); + assertEq( + wstETH.balanceOf(address(user)), + wstETHAsFee, + "user balance should be equal to fee reward" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore, + 1 wei, + "bond shares after claim should be equal to before" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(user); - accounting.depositStETHWithPermit( - user, + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETHAsFee + stETH.getSharesByPooledEth(2 ether), + 1 wei, + "user balance should be equal to fee reward" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + wstETH.balanceOf(address(accounting)), 0, - 32 ether, - CSAccounting.PermitInput({ - value: 32 ether, - deadline: type(uint256).max, - // mock permit signature - v: 0, - r: 0, - s: 0 - }) + "bond manager wstETH balance should be 0" ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETHAsFee + stETH.getSharesByPooledEth(1 ether), + 1 wei, + "user balance should be equal to fee reward plus excess bond" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(1 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond" + ); assertEq( - vm.getRecordedLogs().length, - 1, - "should emit only one event about deposit" + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" ); } - function test_depositWstETHWithPermit() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - uint256 wstETHAmount = wstETH.wrap(32 ether); - uint256 sharesToDeposit = stETH.getSharesByPooledEth( - wstETH.getStETHByWstETH(wstETHAmount) + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); + + assertApproxEqAbs( + wstETH.balanceOf(address(user)), + wstETHAsFee + stETH.getSharesByPooledEth(3 ether), + 1 wei, + "user balance should be equal to fee reward plus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(3 ether), + 1 wei, + "bond shares after claim should be equal to before minus excess bond after one validator withdrawn" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq( + wstETH.balanceOf(address(user)), + 0, + "user balance should be equal to zero" + ); + assertEq( + bondSharesAfter, + bondSharesBefore + sharesAsFee, + "bond shares after claim should be equal to before plus fee shares" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" ); - vm.stopPrank(); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" + ); + } - vm.expectEmit(true, true, true, true, address(wstETH)); - emit Approval(user, address(accounting), 32 ether); - vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHBondDeposited(0, user, wstETHAmount); + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); + uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(user); - accounting.depositWstETHWithPermit( - user, - 0, - wstETHAmount, - CSAccounting.PermitInput({ - value: 32 ether, - deadline: type(uint256).max, - // mock permit signature - v: 0, - r: 0, - s: 0 - }) + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); assertEq( - wstETH.balanceOf(user), + wstETH.balanceOf(address(user)), 0, - "user balance should be 0 after deposit" + "user balance should be equal to zero" ); assertEq( - accounting.getBondShares(0), - sharesToDeposit, - "bond shares should be equal to deposited shares" + bondSharesAfter, + bondSharesBefore + sharesAsFee, + "bond shares after claim should be equal to before plus fee shares" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after claim should be equal to bond shares after" ); assertEq( accounting.totalBondShares(), - sharesToDeposit, - "bond manager shares should be equal to deposited shares" + bondSharesAfter, + "total bond shares after claim should be equal to bond shares after" ); } - function test_depositWstETHWithPermit_alreadyPermitted() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - uint256 wstETHAmount = wstETH.wrap(32 ether); - vm.stopPrank(); + function test_EventEmitted() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHBondDeposited(0, user, wstETHAmount); + emit WstETHRewardsClaimed(0, user, wstETHAsFee); - vm.mockCall( - address(wstETH), - abi.encodeWithSelector( - wstETH.allowance.selector, - user, - address(accounting) - ), - abi.encode(32 ether) + vm.prank(user); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + } - vm.recordLogs(); + function test_WithDesirableValue() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 sharesToClaim = stETH.getSharesByPooledEth(0.05 ether); + uint256 wstETHToClaim = wstETH.getWstETHByStETH( + stETH.getPooledEthByShares(sharesToClaim) + ); + uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(user); - accounting.depositWstETHWithPermit( - user, - 0, - wstETHAmount, - CSAccounting.PermitInput({ - value: 32 ether, - deadline: type(uint256).max, - // mock permit signature - v: 0, - r: 0, - s: 0 - }) + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + sharesToClaim ); + uint256 bondSharesAfter = accounting.getBondShares(0); + vm.stopPrank(); assertEq( - vm.getRecordedLogs().length, - 1, - "should emit only one event about deposit" + wstETH.balanceOf(address(user)), + wstETHToClaim, + "user balance should be equal to claimed" + ); + assertApproxEqAbs( + bondSharesAfter, + (bondSharesBefore + sharesAsFee) - sharesToClaim, + 1 wei, + "bond shares after should be equal to before and fee minus claimed shares" + ); + assertEq( + wstETH.balanceOf(address(accounting)), + 0, + "bond manager wstETH balance should be 0" + ); + assertEq( + stETH.sharesOf(address(accounting)), + bondSharesAfter, + "bond manager after should be equal to before and fee minus claimed shares" + ); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares after should be equal to before and fee minus claimed shares" ); } - function test_depositETH_RevertIfNotExistedOperator() public { - vm.expectRevert("node operator does not exist"); - vm.prank(user); - accounting.depositETH{ value: 0 }(user, 0); + function test_RevertWhen_NotOwner() public override { + vm.expectRevert( + abi.encodeWithSelector(NotOwnerToClaim.selector, stranger, user) + ); + vm.prank(stranger); + accounting.claimRewardsWstETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); } +} - function test_depositStETH_RevertIfNotExistedOperator() public { - vm.expectRevert("node operator does not exist"); +contract CSAccountingRequestRewardsETHRewardsTest is + CSAccountingClaimRewardsBaseTest +{ + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(user); - accounting.depositStETH(user, 0, 32 ether); - } + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); - function test_depositETH_RevertIfInvalidSender() public { - vm.expectRevert(InvalidSender.selector); - vm.prank(stranger); - accounting.depositETH{ value: 0 }(user, 0); + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertEq( + bondSharesAfter, + bondSharesBefore, + "bond shares should not change after request" + ); + assertEq( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee, + "shares of withdrawal queue should be equal to requested shares" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should not change" + ); } - function test_depositStETH_RevertIfInvalidSender() public { - vm.expectRevert(InvalidSender.selector); - vm.prank(stranger); - accounting.depositStETH(user, 0, 32 ether); - } + function test_WithCurve() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); - function test_depositStETHWithPermit_RevertIfInvalidSender() public { - vm.expectRevert(InvalidSender.selector); - vm.prank(stranger); - accounting.depositStETHWithPermit( - user, - 0, - 32 ether, - CSAccounting.PermitInput({ - value: 32 ether, - deadline: type(uint256).max, - // mock permit signature - v: 0, - r: 0, - s: 0 - }) + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); - } + uint256 bondSharesAfter = accounting.getBondShares(0); - function test_depositWstETH_RevertIfInvalidSender() public { - vm.expectRevert(InvalidSender.selector); - vm.prank(stranger); - accounting.depositWstETH(user, 0, 32 ether); + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(15 ether), + 1 wei, + "bond shares should be changed after request minus excess bond after curve" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(15 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after curve" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" + ); } - function test_depositWstETHWithPermit_RevertIfInvalidSender() public { - vm.expectRevert(InvalidSender.selector); - vm.prank(stranger); - accounting.depositWstETHWithPermit( - user, - 0, - 32 ether, - CSAccounting.PermitInput({ - value: 32 ether, - deadline: type(uint256).max, - // mock permit signature - v: 0, - r: 0, - s: 0 - }) + function test_WithMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertEq( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(3.2 ether), + "bond shares should be changed after request minus excess bond after multiplier" + ); + assertEq( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(3.2 ether), + "shares of withdrawal queue should be equal to requested shares and excess bond after multiplier" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" ); } - function test_getTotalRewardsETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 ETHAsFee = stETH.getPooledEthByShares(sharesAsFee); - vm.deal(user, 32 ether); - vm.startPrank(user); - accounting.depositETH{ value: 32 ether }(user, 0); + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); - // todo: should we think about simulate rebase? - uint256 totalRewards = accounting.getTotalRewardsETH( - new bytes32[](1), - 0, - sharesAsFee + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq(totalRewards, ETHAsFee); + assertEq(requestIds.length, 0, "request ids length should be 1"); + assertEq( + bondSharesAfter, + bondSharesBefore + sharesAsFee, + "bond shares should be equal to before plus fee shares" + ); + assertEq( + stETH.sharesOf(address(locator.withdrawalQueue())), + 0, + "shares of withdrawal queue should be equal to zero" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" + ); } - function test_getTotalRewardsStETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 stETHAsFee = stETH.getPooledEthByShares(sharesAsFee); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); + function test_WithCurveAndMultiplier() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); - // todo: should we think about simulate rebase? - uint256 totalRewards = accounting.getTotalRewardsStETH( - new bytes32[](1), - 0, - sharesAsFee + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq(totalRewards, stETHAsFee); + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(16.7 ether), + 1 wei, + "bond shares should be equal to before minus excess bond after curve and multiplier" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(16.7 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after curve and multiplier" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" + ); } - function test_getTotalRewardsWstETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 wstETHAsFee = wstETH.getWstETHByStETH( - stETH.getPooledEthByShares(sharesAsFee) - ); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); - // todo: should we think about simulate rebase? - uint256 totalRewards = accounting.getTotalRewardsWstETH( - new bytes32[](1), - 0, - sharesAsFee + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq(totalRewards, wstETHAsFee); + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(14 ether), + 1 wei, + "bond shares should be equal to before minus excess bond after curve and locked" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(14 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" + ); } - function test_getExcessBondETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 64 ether); - vm.startPrank(user); - accounting.depositETH{ value: 64 ether }(user, 0); - - assertApproxEqAbs(accounting.getExcessBondETH(0), 32 ether, 1); - } + function test_WithMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); - function test_getExcessBondStETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 64 ether); - vm.startPrank(user); - accounting.depositETH{ value: 64 ether }(user, 0); + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertApproxEqAbs(accounting.getExcessBondStETH(0), 32 ether, 1); + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2.2 ether), + 1 wei, + "bond shares should be equal to before minus excess bond after multiplier and locked" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(2.2 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after multiplier and locked" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" + ); } - function test_getExcessBondWstETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 64 ether); - vm.startPrank(user); - accounting.depositETH{ value: 64 ether }(user, 0); + function test_WithCurveAndMultiplierAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); + _curve(defaultCurve); + _multiplier({ id: 0, multiplier: 9000 }); + _lock({ id: 0, amount: 1 ether }); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + uint256 bondSharesAfter = accounting.getBondShares(0); + assertEq(requestIds.length, 1, "request ids length should be 1"); assertApproxEqAbs( - accounting.getExcessBondWstETH(0), - wstETH.getWstETHByStETH(32 ether), - 1 + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(15.7 ether), + 2 wei, + "bond shares should be equal to before minus excess bond after curve and multiplier and locked" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(15.7 ether), + 2 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after curve and multiplier and locked" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" ); } - function test_getMissingBondETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 16 ether); - vm.startPrank(user); - accounting.depositETH{ value: 16 ether }(user, 0); - - assertApproxEqAbs(accounting.getMissingBondETH(0), 16 ether, 1); - } + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); - function test_getMissingBondStETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 16 ether); - vm.startPrank(user); - accounting.depositETH{ value: 16 ether }(user, 0); + uint256 bondSharesBefore = accounting.getBondShares(0); - assertApproxEqAbs(accounting.getMissingBondStETH(0), 16 ether, 1); - } + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); - function test_getMissingBondWstETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 16 ether); - vm.startPrank(user); - accounting.depositETH{ value: 16 ether }(user, 0); + uint256 bondSharesAfter = accounting.getBondShares(0); + assertEq(requestIds.length, 1, "request ids length should be 1"); assertApproxEqAbs( - accounting.getMissingBondWstETH(0), - wstETH.getWstETHByStETH(16 ether), - 1 + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(2 ether), + 1 wei, + "bond shares should be equal to before minus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(2 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after one validator withdrawn" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" ); } - function test_getUnbondedKeysCount() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 17.57 ether); - vm.startPrank(user); - accounting.depositETH{ value: 17.57 ether }(user, 0); + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); - assertEq(accounting.getUnbondedKeysCount(0), 7); - } + uint256 bondSharesBefore = accounting.getBondShares(0); - function test_getKeysCountByBondETH() public { - assertEq(accounting.getKeysCountByBondETH(0), 0); - assertEq(accounting.getKeysCountByBondETH(1.99 ether), 0); - assertEq(accounting.getKeysCountByBondETH(2 ether), 1); - assertEq(accounting.getKeysCountByBondETH(4 ether), 2); - } + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); - function test_getKeysCountByBondStETH() public { - assertEq(accounting.getKeysCountByBondStETH(0), 0); - assertEq(accounting.getKeysCountByBondStETH(1.99 ether), 0); - assertEq(accounting.getKeysCountByBondStETH(2 ether), 1); - assertEq(accounting.getKeysCountByBondStETH(4 ether), 2); - } + uint256 bondSharesAfter = accounting.getBondShares(0); - function test_getKeysCountByBondWstETH() public { - assertEq(accounting.getKeysCountByBondWstETH(0), 0); + assertEq(requestIds.length, 1, "request ids length should be 1"); assertEq( - accounting.getKeysCountByBondWstETH( - wstETH.getWstETHByStETH(1.99 ether) - ), - 0 + bondSharesAfter, + bondSharesBefore, + "bond shares should not change after request" ); assertEq( - accounting.getKeysCountByBondWstETH( - wstETH.getWstETHByStETH(2 ether) - ), - 1 + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee, + "shares of withdrawal queue should be equal to requested shares" ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); assertEq( - accounting.getKeysCountByBondWstETH( - wstETH.getWstETHByStETH(4 ether) - ), - 2 + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" ); } - function test_claimRewardsStETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 stETHAsFee = stETH.getPooledEthByShares(sharesAsFee); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed( - 0, - user, - stETH.getPooledEthByShares(sharesAsFee) - ); + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether, fee: 0.1 ether }); uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsStETH( - new bytes32[](1), - 0, - sharesAsFee, + + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq( - stETH.balanceOf(address(user)), - stETHAsFee, - "user balance should be equal to fee reward" - ); - assertEq( + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( bondSharesAfter, - bondSharesBefore, - "bond shares after claim should be equal to before" + bondSharesBefore - stETH.getSharesByPooledEth(2 ether), + 1 wei, + "bond shares should be equal to before minus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(2 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after one validator withdrawn" ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); assertEq( - stETH.sharesOf(address(accounting)), + accounting.totalBondShares(), bondSharesAfter, - "bond manager after claim should be equal to before" + "total bond shares should be equal to after" ); } - function test_claimRewardsStETH_WithDesirableValue() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 sharesToClaim = stETH.getSharesByPooledEth(0.05 ether); - uint256 stETHToClaim = stETH.getPooledEthByShares(sharesToClaim); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed(0, user, stETHToClaim); + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsStETH( - new bytes32[](1), - 0, - sharesAsFee, - 0.05 ether + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq( - stETH.balanceOf(address(user)), - stETHToClaim, - "user balance should be equal to claimed" - ); - assertEq( + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( bondSharesAfter, - (bondSharesBefore + sharesAsFee) - sharesToClaim, - "bond shares after should be equal to before and fee minus claimed shares" + bondSharesBefore - stETH.getSharesByPooledEth(1 ether), + 1 wei, + "bond shares should be equal to before minus excess bond" ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(1 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); assertEq( - stETH.sharesOf(address(accounting)), + accounting.totalBondShares(), bondSharesAfter, - "bond manager after should be equal to before and fee minus claimed shares" + "total bond shares should be equal to after" ); } - function test_claimRewardsStETH_WhenAmountToClaimIsHigherThanRewards() - public - { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 stETHAsFee = stETH.getPooledEthByShares(sharesAsFee); + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether, fee: 0.1 ether }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); + uint256 bondSharesBefore = accounting.getBondShares(0); + + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); + + uint256 bondSharesAfter = accounting.getBondShares(0); + + assertEq(requestIds.length, 1, "request ids length should be 1"); + assertApproxEqAbs( + bondSharesAfter, + bondSharesBefore - stETH.getSharesByPooledEth(3 ether), + 1 wei, + "bond shares should be equal to before minus excess bond after one validator withdrawn" + ); + assertApproxEqAbs( + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesAsFee + stETH.getSharesByPooledEth(3 ether), + 1 wei, + "shares of withdrawal queue should be equal to requested shares and excess bond after one validator withdrawn" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), + bondSharesAfter, + "total bond shares should be equal to after" + ); + } - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed(0, user, stETHAsFee); + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsStETH( - new bytes32[](1), - 0, - sharesAsFee, - 100 * 1e18 + + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); + assertEq(requestIds.length, 0, "request ids length should be 0"); assertEq( - stETH.balanceOf(address(user)), - stETHAsFee, - "user balance should be equal to fee reward" + bondSharesAfter, + bondSharesBefore + sharesAsFee, + "bond shares should be equal to before plus fee shares" ); assertEq( - bondSharesAfter, - bondSharesBefore, - "bond shares after should be equal to before" + stETH.sharesOf(address(locator.withdrawalQueue())), + 0, + "shares of withdrawal queue should be equal to zero" ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); assertEq( - stETH.sharesOf(address(accounting)), + accounting.totalBondShares(), bondSharesAfter, - "bond manager after should be equal to before" + "total bond shares should be equal to after" ); } - function test_claimRewardsStETH_WhenRequiredBondIsEqualActual() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 1 ether }(address(0)); - - vm.deal(user, 31 ether); - vm.startPrank(user); - stETH.submit{ value: 31 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 31 ether); - - vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed(0, user, 0); + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether, fee: 0.1 ether }); uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsStETH( - new bytes32[](1), - 0, - sharesAsFee, + + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, UINT256_MAX ); + uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq(stETH.balanceOf(address(user)), 0, "user balance should be 0"); + assertEq(requestIds.length, 0, "request ids length should be 0"); assertEq( bondSharesAfter, bondSharesBefore + sharesAsFee, - "bond shares should be increased by fee" + "bond shares should be equal to before plus fee shares" ); assertEq( - stETH.sharesOf(address(accounting)), + stETH.sharesOf(address(locator.withdrawalQueue())), + 0, + "shares of withdrawal queue should be equal to zero" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), bondSharesAfter, - "bond manager shares should be increased by fee" + "total bond shares should be equal to after" ); } - function test_claimRewardsStETH_WhenRequiredBondIsHigherActual() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.5 ether }(address(0)); - - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 31 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 31 ether); + function test_EventEmitted() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + vm.expectEmit( + true, + true, + true, + true, + address(locator.withdrawalQueue()) + ); + emit WithdrawalRequested( + 1, + address(accounting), + user, + unstETHAsFee, + unstETHSharesAsFee + ); vm.expectEmit(true, true, true, true, address(accounting)); - emit StETHRewardsClaimed(0, user, 0); + emit ETHRewardsRequested(0, user, unstETHAsFee); - uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsStETH( - new bytes32[](1), - 0, - sharesAsFee, + vm.prank(user); + accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, UINT256_MAX ); + } + + function test_WithDesirableValue() public override { + _deposit({ bond: 32 ether, fee: 0.1 ether }); + + uint256 sharesToRequest = stETH.getSharesByPooledEth(0.05 ether); + uint256 unstETHToRequest = stETH.getPooledEthByShares(sharesToRequest); + uint256 unstETHSharesToRequest = stETH.getSharesByPooledEth( + unstETHToRequest + ); + + uint256 bondSharesBefore = accounting.getBondShares(0); + vm.prank(user); + uint256[] memory requestIds = accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + 0.05 ether + ); uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq(stETH.balanceOf(address(user)), 0, "user balance should be 0"); + assertEq(requestIds.length, 1, "request ids length should be 1"); assertEq( bondSharesAfter, - bondSharesBefore + sharesAsFee, - "bond shares should be increased by fee" + bondSharesBefore + sharesAsFee - sharesToRequest, + "bond shares should be equal to before plus fee shares minus requested shares" ); assertEq( - stETH.sharesOf(address(accounting)), + stETH.sharesOf(address(locator.withdrawalQueue())), + unstETHSharesToRequest, + "shares of withdrawal queue should be equal to requested shares" + ); + assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq( + accounting.totalBondShares(), bondSharesAfter, - "bond manager shares should be increased by fee" + "total bond shares should be equal to after" ); } - function test_claimRewardsStETH_RevertWhenCallerIsNotRewardAddress() - public - { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - + function test_RevertWhen_NotOwner() public override { vm.expectRevert( abi.encodeWithSelector(NotOwnerToClaim.selector, stranger, user) ); vm.prank(stranger); - accounting.claimRewardsStETH(new bytes32[](1), 0, 1, 1 ether); + accounting.requestRewardsETH( + leaf.proof, + leaf.nodeOperatorId, + leaf.shares, + UINT256_MAX + ); } +} - function test_claimRewardsWstETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 wstETHAsFee = wstETH.getWstETHByStETH( - stETH.getPooledEthByShares(sharesAsFee) - ); +contract CSAccountingDepositsTest is CSAccountingBaseTest { + function setUp() public override { + super.setUp(); + mock_getNodeOperator(); + mock_getNodeOperatorsCount(); + } + + function test_depositETH() public { vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); + uint256 sharesToDeposit = stETH.getSharesByPooledEth(32 ether); vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHRewardsClaimed(0, user, wstETHAsFee); + emit ETHBondDeposited(0, user, 32 ether); - uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsWstETH( - new bytes32[](1), + vm.prank(user); + accounting.depositETH{ value: 32 ether }(user, 0); + + assertEq( + address(user).balance, 0, - sharesAsFee, - UINT256_MAX + "user balance should be 0 after deposit" ); - uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq( - wstETH.balanceOf(address(user)), - wstETHAsFee, - "user balance should be equal to fee reward" + accounting.getBondShares(0), + sharesToDeposit, + "bond shares should be equal to deposited shares" ); assertEq( - bondSharesAfter, - bondSharesBefore + 1 wei, - "bond shares after claim should contain wrapped fee accuracy error" + stETH.sharesOf(address(accounting)), + sharesToDeposit, + "bond manager shares should be equal to deposited shares" ); + assertEq(accounting.totalBondShares(), sharesToDeposit); + } + + function test_depositStETH() public { + vm.deal(user, 32 ether); + vm.prank(user); + uint256 sharesToDeposit = stETH.submit{ value: 32 ether }({ + _referal: address(0) + }); + + vm.expectEmit(true, true, true, true, address(accounting)); + emit StETHBondDeposited(0, user, 32 ether); + + vm.prank(user); + accounting.depositStETH(user, 0, 32 ether); + assertEq( - wstETH.balanceOf(address(accounting)), + stETH.balanceOf(user), 0, - "bond manager wstETH balance should be 0" + "user balance should be 0 after deposit" + ); + assertEq( + accounting.getBondShares(0), + sharesToDeposit, + "bond shares should be equal to deposited shares" ); assertEq( stETH.sharesOf(address(accounting)), - bondSharesBefore + 1 wei, - "bond manager after claim should contain wrapped fee accuracy error" + sharesToDeposit, + "bond manager shares should be equal to deposited shares" ); + assertEq(accounting.totalBondShares(), sharesToDeposit); } - function test_claimRewardsWstETH_WithDesirableValue() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - uint256 sharesToClaim = stETH.getSharesByPooledEth(0.05 ether); - uint256 wstETHToClaim = wstETH.getWstETHByStETH( - stETH.getPooledEthByShares(sharesToClaim) - ); + function test_depositWstETH() public { vm.deal(user, 32 ether); vm.startPrank(user); stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); + uint256 wstETHAmount = wstETH.wrap(32 ether); + uint256 sharesToDeposit = stETH.getSharesByPooledEth( + wstETH.getStETHByWstETH(wstETHAmount) + ); + vm.stopPrank(); vm.expectEmit(true, true, true, true, address(accounting)); - emit WstETHRewardsClaimed(0, user, wstETHToClaim); + emit WstETHBondDeposited(0, user, wstETHAmount); - uint256 bondSharesBefore = accounting.getBondShares(0); - accounting.claimRewardsWstETH( - new bytes32[](1), + vm.prank(user); + accounting.depositWstETH(user, 0, wstETHAmount); + + assertEq( + wstETH.balanceOf(user), 0, - sharesAsFee, - stETH.getSharesByPooledEth(0.05 ether) + "user balance should be 0 after deposit" ); - uint256 bondSharesAfter = accounting.getBondShares(0); - assertEq( - wstETH.balanceOf(address(user)), - wstETHToClaim, - "user balance should be equal to fee reward" + accounting.getBondShares(0), + sharesToDeposit, + "bond shares should be equal to deposited shares" ); assertEq( - bondSharesAfter, - (bondSharesBefore + sharesAsFee) - wstETHToClaim, - "bond shares after should be equal to before and fee minus claimed shares" + stETH.sharesOf(address(accounting)), + sharesToDeposit, + "bond manager shares should be equal to deposited shares" + ); + assertEq(accounting.totalBondShares(), sharesToDeposit); + } + + function test_depositStETHWithPermit() public { + vm.deal(user, 32 ether); + vm.prank(user); + uint256 sharesToDeposit = stETH.submit{ value: 32 ether }({ + _referal: address(0) + }); + + vm.expectEmit(true, true, true, true, address(stETH)); + emit Approval(user, address(accounting), 32 ether); + vm.expectEmit(true, true, true, true, address(accounting)); + emit StETHBondDeposited(0, user, 32 ether); + + vm.prank(user); + accounting.depositStETHWithPermit( + user, + 0, + 32 ether, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) ); + assertEq( - wstETH.balanceOf(address(accounting)), + stETH.balanceOf(user), 0, - "bond manager wstETH balance should be 0" + "user balance should be 0 after deposit" + ); + assertEq( + accounting.getBondShares(0), + sharesToDeposit, + "bond shares should be equal to deposited shares" ); assertEq( stETH.sharesOf(address(accounting)), - (bondSharesBefore + sharesAsFee) - wstETHToClaim, - "bond shares after should be equal to before and fee minus claimed shares" + sharesToDeposit, + "bond manager shares should be equal to deposited shares" ); + assertEq(accounting.totalBondShares(), sharesToDeposit); } - function test_claimRewardsWstETH_RevertWhenCallerIsNotRewardAddress() - public - { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); + function test_depositStETHWithPermit_AlreadyPermitted() public { + vm.deal(user, 32 ether); + vm.prank(user); + stETH.submit{ value: 32 ether }({ _referal: address(0) }); + + vm.expectEmit(true, true, true, true, address(accounting)); + emit StETHBondDeposited(0, user, 32 ether); + + vm.mockCall( + address(stETH), + abi.encodeWithSelector( + stETH.allowance.selector, + user, + address(accounting) + ), + abi.encode(32 ether) + ); + + vm.recordLogs(); + + vm.prank(user); + accounting.depositStETHWithPermit( + user, + 0, + 32 ether, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) + ); - vm.expectRevert( - abi.encodeWithSelector(NotOwnerToClaim.selector, stranger, user) + assertEq( + vm.getRecordedLogs().length, + 1, + "should emit only one event about deposit" ); - vm.prank(stranger); - accounting.claimRewardsWstETH(new bytes32[](1), 0, 1, 1 ether); } - function test_requestRewardsETH() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - + function test_depositWstETHWithPermit() public { vm.deal(user, 32 ether); vm.startPrank(user); stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); - - uint256 requestedAsUnstETH = stETH.getPooledEthByShares(sharesAsFee); - uint256 requestedAsUnstETHAsShares = stETH.getSharesByPooledEth( - requestedAsUnstETH + uint256 wstETHAmount = wstETH.wrap(32 ether); + uint256 sharesToDeposit = stETH.getSharesByPooledEth( + wstETH.getStETHByWstETH(wstETHAmount) ); + vm.stopPrank(); - vm.expectEmit( - true, - true, - true, - true, - address(locator.withdrawalQueue()) - ); - emit WithdrawalRequested( - 1, - address(accounting), - user, - requestedAsUnstETH, - requestedAsUnstETHAsShares - ); + vm.expectEmit(true, true, true, true, address(wstETH)); + emit Approval(user, address(accounting), 32 ether); vm.expectEmit(true, true, true, true, address(accounting)); - emit ETHRewardsRequested( - 0, + emit WstETHBondDeposited(0, user, wstETHAmount); + + vm.prank(user); + accounting.depositWstETHWithPermit( user, - stETH.getPooledEthByShares(sharesAsFee) + 0, + wstETHAmount, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) ); - uint256 bondSharesBefore = accounting.getBondShares(0); - uint256[] memory requestIds = accounting.requestRewardsETH( - new bytes32[](1), + assertEq( + wstETH.balanceOf(user), 0, - sharesAsFee, - UINT256_MAX + "user balance should be 0 after deposit" ); - uint256 bondSharesAfter = accounting.getBondShares(0); - - assertEq(requestIds.length, 1, "request ids length should be 1"); assertEq( - bondSharesAfter, - bondSharesBefore, - "bond shares should not change after request" + accounting.getBondShares(0), + sharesToDeposit, + "bond shares should be equal to deposited shares" ); assertEq( - stETH.sharesOf(address(locator.withdrawalQueue())), - requestedAsUnstETHAsShares, - "shares of withdrawal queue should be equal to requested shares" + accounting.totalBondShares(), + sharesToDeposit, + "bond manager shares should be equal to deposited shares" ); - assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); + assertEq(accounting.totalBondShares(), sharesToDeposit); } - function test_requestRewardsETH_WithDesirableValue() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(address(feeDistributor), 0.1 ether); - vm.prank(address(feeDistributor)); - uint256 sharesAsFee = stETH.submit{ value: 0.1 ether }(address(0)); - + function test_depositWstETHWithPermit_AlreadyPermitted() public { vm.deal(user, 32 ether); vm.startPrank(user); stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); + uint256 wstETHAmount = wstETH.wrap(32 ether); + vm.stopPrank(); - uint256 requestedAsShares = stETH.getSharesByPooledEth(0.05 ether); - uint256 requestedAsUnstETH = stETH.getPooledEthByShares( - requestedAsShares - ); - uint256 requestedAsUnstETHAsShares = stETH.getSharesByPooledEth( - requestedAsUnstETH + vm.expectEmit(true, true, true, true, address(accounting)); + emit WstETHBondDeposited(0, user, wstETHAmount); + + vm.mockCall( + address(wstETH), + abi.encodeWithSelector( + wstETH.allowance.selector, + user, + address(accounting) + ), + abi.encode(32 ether) ); - vm.expectEmit( - true, - true, - true, - true, - address(locator.withdrawalQueue()) + vm.recordLogs(); + + vm.prank(user); + accounting.depositWstETHWithPermit( + user, + 0, + wstETHAmount, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) ); - emit WithdrawalRequested( + + assertEq( + vm.getRecordedLogs().length, 1, - address(accounting), - user, - requestedAsUnstETH, - requestedAsUnstETHAsShares + "should emit only one event about deposit" ); - vm.expectEmit(true, true, true, true, address(accounting)); - emit ETHRewardsRequested(0, user, requestedAsUnstETH); + } - uint256 bondSharesBefore = accounting.getBondShares(0); - uint256[] memory requestIds = accounting.requestRewardsETH( - new bytes32[](1), + function test_depositETH_RevertIfNotExistedOperator() public { + vm.expectRevert("node operator does not exist"); + vm.prank(user); + accounting.depositETH{ value: 0 }(user, 1); + } + + function test_depositStETH_RevertIfNotExistedOperator() public { + vm.expectRevert("node operator does not exist"); + vm.prank(user); + accounting.depositStETH(user, 1, 0 ether); + } + + function test_depositWstETH_RevertIfNotExistedOperator() public { + vm.expectRevert("node operator does not exist"); + vm.prank(user); + accounting.depositWstETH(user, 1, 0 ether); + } + + function test_depositETH_RevertIfInvalidSender() public { + vm.expectRevert(InvalidSender.selector); + vm.prank(stranger); + accounting.depositETH{ value: 0 }(user, 0); + } + + function test_depositStETH_RevertIfInvalidSender() public { + vm.expectRevert(InvalidSender.selector); + vm.prank(stranger); + accounting.depositStETH(user, 0, 32 ether); + } + + function test_depositStETHWithPermit_RevertIfInvalidSender() public { + vm.expectRevert(InvalidSender.selector); + vm.prank(stranger); + accounting.depositStETHWithPermit( + user, 0, - sharesAsFee, - 0.05 ether + 32 ether, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) ); - uint256 bondSharesAfter = accounting.getBondShares(0); + } - assertEq(requestIds.length, 1, "request ids length should be 1"); - assertEq( - bondSharesAfter, - (bondSharesBefore + sharesAsFee) - requestedAsShares, - "bond shares after should be equal to before and fee minus requested shares" - ); - assertEq( - stETH.sharesOf(address(locator.withdrawalQueue())), - requestedAsUnstETHAsShares, - "shares of withdrawal queue should be equal to requested shares" + function test_depositWstETH_RevertIfInvalidSender() public { + vm.expectRevert(InvalidSender.selector); + vm.prank(stranger); + accounting.depositWstETH(user, 0, 32 ether); + } + + function test_depositWstETHWithPermit_RevertIfInvalidSender() public { + vm.expectRevert(InvalidSender.selector); + vm.prank(stranger); + accounting.depositWstETHWithPermit( + user, + 0, + 32 ether, + CSAccounting.PermitInput({ + value: 32 ether, + deadline: type(uint256).max, + // mock permit signature + v: 0, + r: 0, + s: 0 + }) ); - assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); } +} - function test_penalize_LessThanDeposit() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); +contract CSAccountingPenalizeTest is CSAccountingBaseTest { + function setUp() public override { + super.setUp(); + mock_getNodeOperator(); + mock_getNodeOperatorsCount(); vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); - vm.stopPrank(); + vm.prank(user); + accounting.depositETH{ value: 32 ether }(user, 0); + } + function test_penalize_LessThanDeposit() public { uint256 shares = stETH.getSharesByPooledEth(1 ether); uint256 penalized = stETH.getPooledEthByShares(shares); vm.expectEmit(true, true, true, true, address(accounting)); @@ -1241,15 +4771,16 @@ contract CSAccountingTest is uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(admin); accounting.penalize(0, 1 ether); + uint256 bondSharesAfter = accounting.getBondShares(0); assertEq( - accounting.getBondShares(0), + bondSharesAfter, bondSharesBefore - shares, "bond shares should be decreased by penalty" ); assertEq( stETH.sharesOf(address(accounting)), - bondSharesBefore - shares, + bondSharesAfter, "bond manager shares should be decreased by penalty" ); assertEq( @@ -1257,16 +4788,10 @@ contract CSAccountingTest is shares, "burner shares should be equal to penalty" ); + assertEq(accounting.totalBondShares(), bondSharesAfter); } function test_penalize_MoreThanDeposit() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); - vm.stopPrank(); - uint256 bondSharesBefore = accounting.getBondShares(0); uint256 penaltyShares = stETH.getSharesByPooledEth(33 ether); vm.expectEmit(true, true, true, true, address(accounting)); @@ -1294,16 +4819,10 @@ contract CSAccountingTest is bondSharesBefore, "burner shares should be equal to bond shares" ); + assertEq(accounting.totalBondShares(), 0); } function test_penalize_EqualToDeposit() public { - _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); - vm.deal(user, 32 ether); - vm.startPrank(user); - stETH.submit{ value: 32 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 32 ether); - vm.stopPrank(); - uint256 shares = stETH.getSharesByPooledEth(32 ether); uint256 penalized = stETH.getPooledEthByShares(shares); vm.expectEmit(true, true, true, true, address(accounting)); @@ -1327,29 +4846,105 @@ contract CSAccountingTest is shares, "burner shares should be equal to penalty" ); + assertEq(accounting.totalBondShares(), 0); } function test_penalize_RevertWhenCallerHasNoRole() public { vm.expectRevert( - "AccessControl: account 0x0000000000000000000000000000000000000309 is missing role 0x9909cf24c2d3bafa8c229558d86a1b726ba57c3ef6350848dcf434a4181b56c7" + bytes( + Utilities.accessErrorString( + stranger, + accounting.INSTANT_PENALIZE_BOND_ROLE() + ) + ) ); + vm.prank(stranger); accounting.penalize(0, 20); } +} - function _createNodeOperator( - uint64 ongoingVals, - uint64 withdrawnVals - ) internal { - stakingModule.setNodeOperator({ - _nodeOperatorId: 0, - _active: true, - _rewardAddress: user, - _totalVettedValidators: ongoingVals, - _totalExitedValidators: 0, - _totalWithdrawnValidators: withdrawnVals, - _totalAddedValidators: ongoingVals, - _totalDepositedValidators: ongoingVals - }); +contract CSAccountingMiscTest is CSAccountingBaseTest { + function test_totalBondShares() public { + mock_getNodeOperatorsCount(2); + vm.deal(user, 64 ether); + vm.startPrank(user); + accounting.depositETH{ value: 32 ether }(user, 0); + accounting.depositETH{ value: 32 ether }(user, 1); + vm.stopPrank(); + uint256 totalDepositedShares = stETH.getSharesByPooledEth(32 ether) + + stETH.getSharesByPooledEth(32 ether); + assertEq(accounting.totalBondShares(), totalDepositedShares); + } + + function test_setFeeDistributor() public { + vm.prank(admin); + accounting.setFeeDistributor(address(1337)); + assertEq(accounting.FEE_DISTRIBUTOR(), address(1337)); + } + + function test_setFeeDistributor_RevertWhen_DoesNotHaveRole() public { + vm.expectRevert( + bytes( + Utilities.accessErrorString( + stranger, + accounting.DEFAULT_ADMIN_ROLE() + ) + ) + ); + + vm.prank(stranger); + accounting.setFeeDistributor(address(1337)); + } + + function test_setBondCurve() public { + uint256[] memory _bondCurve = new uint256[](2); + _bondCurve[0] = 2 ether; + _bondCurve[1] = 4 ether; + + vm.prank(admin); + accounting.setBondCurve(_bondCurve); + + assertEq(accounting.bondCurve(0), 2 ether); + assertEq(accounting.bondCurve(1), 4 ether); + } + + function test_setBondCurve_RevertWhen_DoesNotHaveRole() public { + uint256[] memory _bondCurve = new uint256[](2); + _bondCurve[0] = 2 ether; + _bondCurve[1] = 4 ether; + + vm.expectRevert( + bytes( + Utilities.accessErrorString( + stranger, + accounting.SET_BOND_CURVE_ROLE() + ) + ) + ); + + vm.prank(stranger); + accounting.setBondCurve(_bondCurve); + } + + function test_setBondMultiplier() public { + vm.prank(admin); + accounting.setBondMultiplier(0, 9500); + + assertEq(accounting.getBondMultiplier(0), 9500); + } + + function test_setBondMultiplier_RevertWhen_DoesNotHaveRole() public { + vm.expectRevert( + bytes( + Utilities.accessErrorString( + stranger, + accounting.SET_BOND_MULTIPLIER_ROLE() + ) + ) + ); + + vm.prank(stranger); + accounting.setBondMultiplier(0, 9500); } } diff --git a/test/CSBondCurve.t.sol b/test/CSBondCurve.t.sol new file mode 100644 index 00000000..a5fe66f6 --- /dev/null +++ b/test/CSBondCurve.t.sol @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.21; + +import "forge-std/Test.sol"; + +import { CSBondCurve, CSBondCurveBase } from "../src/CSBondCurve.sol"; + +contract CSBondCurveTestable is CSBondCurve { + constructor(uint256[] memory bondCurve) CSBondCurve(bondCurve) {} + + function getBondCurveTrend() external view returns (uint256) { + return _bondCurveTrend; + } + + function setBondCurve(uint256[] memory bondCurve) external { + _setBondCurve(bondCurve); + } + + function setBondMultiplier( + uint256 nodeOperatorId, + uint256 basisPoints + ) external { + _setBondMultiplier(nodeOperatorId, basisPoints); + } + + function getKeysCountByBondAmount( + uint256 amount + ) external view returns (uint256) { + return _getKeysCountByBondAmount(amount); + } + + function getBondAmountByKeysCount( + uint256 keys + ) external view returns (uint256) { + return _getBondAmountByKeysCount(keys); + } + + function getKeysCountByBondAmount( + uint256 amount, + uint256 multiplier + ) external view returns (uint256) { + return _getKeysCountByBondAmount(amount, multiplier); + } + + function getBondAmountByKeysCount( + uint256 keys, + uint256 multiplier + ) external view returns (uint256) { + return _getBondAmountByKeysCount(keys, multiplier); + } +} + +contract CSBondCurveTest is Test, CSBondCurveBase { + // todo: add gas-cost test for _searchKeysCount + + CSBondCurveTestable public bondCurve; + + function setUp() public { + uint256[] memory _bondCurve = new uint256[](3); + _bondCurve[0] = 2 ether; + _bondCurve[1] = 4 ether; + _bondCurve[2] = 5 ether; + bondCurve = new CSBondCurveTestable(_bondCurve); + } + + function test_setBondCurve() public { + uint256[] memory _bondCurve = new uint256[](2); + _bondCurve[0] = 16 ether; + _bondCurve[1] = 32 ether; + + vm.expectEmit(true, true, true, true, address(bondCurve)); + emit BondCurveChanged(_bondCurve); + + bondCurve.setBondCurve(_bondCurve); + + assertEq(bondCurve.bondCurve(0), 16 ether); + assertEq(bondCurve.bondCurve(1), 32 ether); + assertEq(bondCurve.getBondCurveTrend(), 16 ether); + } + + function test_setBondCurve_RevertWhen_LessThanMinBondCurveLength() public { + vm.expectRevert(CSBondCurve.InvalidBondCurveLength.selector); + bondCurve.setBondCurve(new uint256[](0)); + } + + function test_setBondCurve_RevertWhen_MoreThanMaxBondCurveLength() public { + vm.expectRevert(CSBondCurve.InvalidBondCurveLength.selector); + bondCurve.setBondCurve(new uint256[](21)); + } + + function test_setBondCurve_RevertWhen_ZeroValue() public { + uint256[] memory _bondCurve = new uint256[](1); + _bondCurve[0] = 0 ether; + + vm.expectRevert(CSBondCurve.InvalidBondCurveValues.selector); + bondCurve.setBondCurve(_bondCurve); + } + + function test_getKeysCountByBondAmount() public { + assertEq(bondCurve.getKeysCountByBondAmount(0), 0); + assertEq(bondCurve.getKeysCountByBondAmount(1.9 ether), 0); + assertEq(bondCurve.getKeysCountByBondAmount(2 ether), 1); + assertEq(bondCurve.getKeysCountByBondAmount(2.1 ether), 1); + assertEq(bondCurve.getKeysCountByBondAmount(4 ether), 2); + assertEq(bondCurve.getKeysCountByBondAmount(5 ether), 3); + assertEq(bondCurve.getKeysCountByBondAmount(5.1 ether), 3); + assertEq(bondCurve.getKeysCountByBondAmount(6 ether), 4); + } + + function test_getBondAmountByKeysCount() public { + assertEq(bondCurve.getBondAmountByKeysCount(0), 0); + assertEq(bondCurve.getBondAmountByKeysCount(1), 2 ether); + assertEq(bondCurve.getBondAmountByKeysCount(2), 4 ether); + assertEq(bondCurve.getBondAmountByKeysCount(3), 5 ether); + assertEq(bondCurve.getBondAmountByKeysCount(4), 6 ether); + } +} + +contract CSBondCurveWithMultiplierTest is Test, CSBondCurveBase { + CSBondCurveTestable public bondCurve; + + function setUp() public { + uint256[] memory simple = new uint256[](1); + simple[0] = 2 ether; + bondCurve = new CSBondCurveTestable(simple); + } + + function test_setBondMultiplier() public { + assertEq(bondCurve.getBondMultiplier(0), 10000); + + vm.expectEmit(true, true, true, true, address(bondCurve)); + emit BondMultiplierChanged(0, 5000); + + bondCurve.setBondMultiplier(0, 5000); + assertEq(bondCurve.getBondMultiplier(0), 5000); + + bondCurve.setBondMultiplier(0, 10000); + assertEq(bondCurve.getBondMultiplier(0), 10000); + } + + function test_setBondMultiplier_RevertWhen_LessThanMin() public { + vm.expectRevert(CSBondCurve.InvalidMultiplier.selector); + bondCurve.setBondMultiplier(0, 4999); + } + + function test_setBondMultiplier_RevertWhen_MoreThanMax() public { + vm.expectRevert(CSBondCurve.InvalidMultiplier.selector); + bondCurve.setBondMultiplier(0, 10001); + } + + function test_getKeysCountByCurveValue() public { + assertEq(bondCurve.getKeysCountByBondAmount(0 ether, 5000), 0); + assertEq(bondCurve.getKeysCountByBondAmount(1 ether, 5000), 1); + assertEq(bondCurve.getKeysCountByBondAmount(2 ether, 5000), 2); + + assertEq(bondCurve.getKeysCountByBondAmount(0 ether, 9000), 0); + assertEq(bondCurve.getKeysCountByBondAmount(1.8 ether, 9000), 1); + assertEq(bondCurve.getKeysCountByBondAmount(5.39 ether, 9000), 2); + } + + function test_getBondAmountByKeysCount() public { + assertEq(bondCurve.getBondAmountByKeysCount(0, 5000), 0); + assertEq(bondCurve.getBondAmountByKeysCount(1, 5000), 1 ether); + assertEq(bondCurve.getBondAmountByKeysCount(2, 5000), 2 ether); + + assertEq(bondCurve.getBondAmountByKeysCount(0, 9000), 0); + assertEq(bondCurve.getBondAmountByKeysCount(1, 9000), 1.8 ether); + assertEq(bondCurve.getBondAmountByKeysCount(2, 9000), 3.6 ether); + } +} diff --git a/test/CSBondLock.t.sol b/test/CSBondLock.t.sol new file mode 100644 index 00000000..8fe2e8b7 --- /dev/null +++ b/test/CSBondLock.t.sol @@ -0,0 +1,380 @@ +// SPDX-FileCopyrightText: 2023 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.21; + +import "forge-std/Test.sol"; + +import { CSBondLockBase, CSBondLock } from "../src/CSBondLock.sol"; +import { PermitTokenBase } from "./helpers/Permit.sol"; +import { Stub } from "./helpers/mocks/Stub.sol"; +import { LidoMock } from "./helpers/mocks/LidoMock.sol"; +import { WstETHMock } from "./helpers/mocks/WstETHMock.sol"; +import { LidoLocatorMock } from "./helpers/mocks/LidoLocatorMock.sol"; +import { CommunityStakingModuleMock } from "./helpers/mocks/CommunityStakingModuleMock.sol"; +import { CommunityStakingFeeDistributorMock } from "./helpers/mocks/CommunityStakingFeeDistributorMock.sol"; +import { WithdrawalQueueMockBase, WithdrawalQueueMock } from "./helpers/mocks/WithdrawalQueueMock.sol"; + +import { Utilities } from "./helpers/Utilities.sol"; +import { Fixtures } from "./helpers/Fixtures.sol"; + +contract CSBondLockTestable is CSBondLock { + constructor( + uint256 retentionPeriod, + uint256 managementPeriod + ) CSBondLock(retentionPeriod, managementPeriod) {} + + function setBondLockPeriods( + uint256 retention, + uint256 management + ) external { + _setBondLockPeriods(retention, management); + } + + function get( + uint256 nodeOperatorId + ) external view returns (CSBondLock.BondLock memory) { + return _get(nodeOperatorId); + } + + function getActualAmount(uint256 amount) external view returns (uint256) { + return _getActualAmount(amount); + } + + function lock(uint256 nodeOperatorId, uint256 amount) external { + _lock(nodeOperatorId, amount); + } + + function settle(uint256[] memory nodeOperatorIds) external { + _settle(nodeOperatorIds); + } + + function reduceAmount(uint256 nodeOperatorId, uint256 amount) external { + _reduceAmount(nodeOperatorId, amount); + } + + uint256 internal _mockedUncoveredPenalty; + mapping(uint256 => bool) public penalized; + + function _penalize( + uint256 nodeOperatorId, + uint256 amount + ) internal override returns (uint256) { + penalized[nodeOperatorId] = true; + return _mockedUncoveredPenalty; + } + + function mock_uncoveredPenalty(uint256 amount) external { + _mockedUncoveredPenalty = amount; + } +} + +contract CSBondLockTest is Test, CSBondLockBase { + CSBondLockTestable public bondLock; + + function setUp() public { + bondLock = new CSBondLockTestable(8 weeks, 1 days); + } + + function test_setBondLockPeriods() public { + uint256 retention = 4 weeks; + uint256 management = 2 days; + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockPeriodsChanged(retention, management); + + bondLock.setBondLockPeriods(retention, management); + + (uint256 _retention, uint256 _management) = bondLock + .getBondLockPeriods(); + assertEq(_retention, retention); + assertEq(_management, management); + } + + function test_setBondLockPeriods_RevertWhen_RetentionLessThanMin() public { + uint256 minRetention = bondLock.MIN_BOND_LOCK_RETENTION_PERIOD(); + uint256 minManagement = bondLock.MIN_BOND_LOCK_MANAGEMENT_PERIOD(); + vm.expectRevert(InvalidBondLockPeriods.selector); + bondLock.setBondLockPeriods(minRetention - 1 seconds, minManagement); + } + + function test_setBondLockPeriods_RevertWhen_RetentionGreaterThanMax() + public + { + uint256 maxRetention = bondLock.MAX_BOND_LOCK_RETENTION_PERIOD(); + uint256 minManagement = bondLock.MIN_BOND_LOCK_MANAGEMENT_PERIOD(); + vm.expectRevert(InvalidBondLockPeriods.selector); + bondLock.setBondLockPeriods(maxRetention + 1 seconds, minManagement); + } + + function test_setBondLockPeriods_RevertWhen_ManagementLessThanMin() public { + uint256 minRetention = bondLock.MIN_BOND_LOCK_RETENTION_PERIOD(); + uint256 minManagement = bondLock.MIN_BOND_LOCK_MANAGEMENT_PERIOD(); + vm.expectRevert(InvalidBondLockPeriods.selector); + bondLock.setBondLockPeriods(minRetention, minManagement - 1 seconds); + } + + function test_setBondLockPeriods_RevertWhen_ManagementGreaterThanMax() + public + { + uint256 minRetention = bondLock.MIN_BOND_LOCK_RETENTION_PERIOD(); + uint256 maxManagement = bondLock.MAX_BOND_LOCK_MANAGEMENT_PERIOD(); + vm.expectRevert(InvalidBondLockPeriods.selector); + bondLock.setBondLockPeriods(minRetention, maxManagement + 1 seconds); + } + + function test_getActualAmount() public { + uint256 noId = 0; + uint256 amount = 1 ether; + bondLock.lock(noId, amount); + + uint256 value = bondLock.getActualAmount(noId); + assertEq(value, amount); + } + + function test_getActualAmount_WhenRetentionPeriodIsPassed() public { + (uint256 retentionPeriod, ) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256 amount = 1 ether; + bondLock.lock(noId, amount); + + vm.warp(block.timestamp + retentionPeriod + 1 seconds); + + uint256 value = bondLock.getActualAmount(noId); + assertEq(value, 0); + } + + function test_lock() public { + (uint256 retentionPeriod, ) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256 amount = 1 ether; + uint256 retentionUntil = block.timestamp + retentionPeriod; + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, amount, retentionUntil); + + bondLock.lock(noId, amount); + + CSBondLock.BondLock memory lock = bondLock.get(noId); + assertEq(lock.amount, amount); + assertEq(lock.retentionUntil, retentionUntil); + } + + function test_lock_WhenSecondTime() public { + (uint256 retentionPeriod, ) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256 amount = 1 ether; + bondLock.lock(noId, amount); + + uint256 newBlockTimestamp = block.timestamp + 1 seconds; + vm.warp(newBlockTimestamp); + uint256 newRetentionUntil = newBlockTimestamp + retentionPeriod; + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, amount + 1.5 ether, newRetentionUntil); + bondLock.lock(noId, 1.5 ether); + + CSBondLock.BondLock memory lock = bondLock.get(noId); + assertEq(lock.amount, amount + 1.5 ether); + assertEq(lock.retentionUntil, newRetentionUntil); + } + + function test_lock_RevertWhen_ZeroAmount() public { + vm.expectRevert(InvalidBondLockAmount.selector); + bondLock.lock(0, 0); + } + + function test_settle() public { + (, uint256 managementPeriod) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256[] memory idsToSettle = new uint256[](1); + idsToSettle[0] = noId; + bondLock.lock(0, 1 ether); + bondLock.mock_uncoveredPenalty(0 ether); + + // more than management period after penalty init + // eligible to settle + vm.warp(block.timestamp + managementPeriod + 1 seconds); + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, 0, 0); + + bondLock.settle(idsToSettle); + + CSBondLock.BondLock memory lock = bondLock.get(noId); + assertEq(lock.amount, 0 ether); + assertEq(lock.retentionUntil, 0); + assertEq(bondLock.penalized(noId), true); + } + + function test_settle_WhenUncovered() public { + (, uint256 managementPeriod) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256[] memory idsToSettle = new uint256[](1); + idsToSettle[0] = noId; + bondLock.lock(noId, 1 ether); + uint256 retentionPeriodWhenLock = block.timestamp + 8 weeks; + bondLock.mock_uncoveredPenalty(0.3 ether); + + // more than management period after penalty init + // eligible to settle + vm.warp(block.timestamp + managementPeriod + 1 seconds); + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, 0.3 ether, retentionPeriodWhenLock); + + bondLock.settle(idsToSettle); + + CSBondLock.BondLock memory lock = bondLock.get(noId); + assertEq(lock.amount, 0.3 ether); + assertEq(lock.retentionUntil, retentionPeriodWhenLock); + assertEq(bondLock.penalized(noId), true); + } + + function test_settle_WhenRetentionPeriodIsExpired() public { + (uint256 retentionPeriod, ) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256[] memory idsToSettle = new uint256[](1); + idsToSettle[0] = 0; + bondLock.lock(0, 1 ether); + + // more than retention period after penalty init + // not eligible already + vm.warp(block.timestamp + retentionPeriod + 1 seconds); + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, 0, 0); + + bondLock.settle(idsToSettle); + + CSBondLock.BondLock memory lock = bondLock.get(noId); + assertEq(lock.amount, 0 ether); + assertEq(lock.retentionUntil, 0); + } + + function test_settle_WhenInManagementPeriod() public { + (uint256 retentionPeriod, uint256 managementPeriod) = bondLock + .getBondLockPeriods(); + uint256 noId = 0; + uint256[] memory idsToSettle = new uint256[](1); + idsToSettle[0] = noId; + bondLock.lock(noId, 1 ether); + uint256 retentionPeriodWhenLock = block.timestamp + retentionPeriod; + + // less than management period after penalty init + // not eligible to settle yet + vm.warp(block.timestamp + managementPeriod - 1 hours); + + vm.recordLogs(); + + bondLock.settle(idsToSettle); + + CSBondLock.BondLock memory lock = bondLock.get(noId); + assertEq(lock.amount, 1 ether); + assertEq(lock.retentionUntil, retentionPeriodWhenLock); + + assertEq(vm.getRecordedLogs().length, 0, "should not emit any events"); + } + + function test_settle_WhenDifferentStates() public { + (uint256 retentionPeriod, uint256 managementPeriod) = bondLock + .getBondLockPeriods(); + // one eligible, one expired, one in management period + uint256[] memory idsToSettle = new uint256[](3); + idsToSettle[0] = 0; + idsToSettle[1] = 1; + idsToSettle[2] = 2; + + // more than retention period after penalty init + // not eligible already + bondLock.lock(0, 1 ether); + vm.warp(block.timestamp + retentionPeriod + 1 seconds); + + // more than management period after penalty init + // eligible to settle + bondLock.lock(1, 1 ether); + bondLock.mock_uncoveredPenalty(0 ether); + vm.warp(block.timestamp + managementPeriod + 1 seconds); + + // less than management period after penalty init + // not eligible to settle yet + bondLock.lock(2, 1 ether); + uint256 retentionPeriodWhenLockTheLast = block.timestamp + + retentionPeriod; + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(0, 0, 0); + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(1, 0, 0); + + bondLock.settle(idsToSettle); + + CSBondLock.BondLock memory lock = bondLock.get(0); + assertEq(lock.amount, 0 ether); + assertEq(lock.retentionUntil, 0); + + lock = bondLock.get(1); + assertEq(lock.amount, 0 ether); + assertEq(lock.retentionUntil, 0); + assertEq(bondLock.penalized(1), true); + + lock = bondLock.get(2); + assertEq(lock.amount, 1 ether); + assertEq(lock.retentionUntil, retentionPeriodWhenLockTheLast); + } + + function test_reduceAmount_WhenFull() public { + uint256 noId = 0; + uint256 amount = 100 ether; + + bondLock.lock(noId, amount); + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, 0, 0); + + bondLock.reduceAmount(noId, amount); + + CSBondLock.BondLock memory lock = bondLock.get(0); + assertEq(lock.amount, 0); + assertEq(lock.retentionUntil, 0); + } + + function test_reduceAmount_WhenPartial() public { + (uint256 retentionPeriod, ) = bondLock.getBondLockPeriods(); + uint256 noId = 0; + uint256 amount = 100 ether; + + bondLock.lock(noId, amount); + uint256 retentionPeriodWhenLock = block.timestamp + retentionPeriod; + + uint256 toRelease = 10 ether; + uint256 rest = amount - toRelease; + + vm.warp(block.timestamp + 1 seconds); + + vm.expectEmit(true, true, true, true, address(bondLock)); + emit BondLockChanged(noId, rest, retentionPeriodWhenLock); + + bondLock.reduceAmount(noId, toRelease); + + CSBondLock.BondLock memory lock = bondLock.get(0); + assertEq(lock.amount, rest); + assertEq(lock.retentionUntil, retentionPeriodWhenLock); + } + + function test_reduceAmount_RevertWhen_ZeroAmount() public { + vm.expectRevert(InvalidBondLockAmount.selector); + bondLock.reduceAmount(0, 0); + } + + function test_reduceAmount_RevertWhen_GreaterThanLock() public { + uint256 noId = 0; + uint256 amount = 100 ether; + + bondLock.lock(noId, amount); + + vm.expectRevert(InvalidBondLockAmount.selector); + bondLock.reduceAmount(noId, amount + 1 ether); + } +} diff --git a/test/CSModule.t.sol b/test/CSModule.t.sol index e6d7bb29..9f270088 100644 --- a/test/CSModule.t.sol +++ b/test/CSModule.t.sol @@ -68,8 +68,11 @@ contract CSMCommon is Test, Fixtures, Utilities, CSModuleBase { address(accounting) ); csm = new CSModule("community-staking-module", address(locator)); + uint256[] memory curve = new uint256[](2); + curve[0] = 2 ether; + curve[1] = 4 ether; accounting = new CSAccounting( - 2 ether, + curve, admin, address(locator), address(wstETH), @@ -499,7 +502,7 @@ contract CSMObtainDepositData is CSMCommon { contract CsmProposeNodeOperatorManagerAddressChange is CSMCommon { function test_proposeNodeOperatorManagerAddressChange() public { uint256 noId = createNodeOperator(); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.managerAddress, nodeOperator); assertEq(no.rewardAddress, nodeOperator); @@ -551,7 +554,7 @@ contract CsmProposeNodeOperatorManagerAddressChange is CSMCommon { contract CsmConfirmNodeOperatorManagerAddressChange is CSMCommon { function test_confirmNodeOperatorManagerAddressChange() public { uint256 noId = createNodeOperator(); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.managerAddress, nodeOperator); assertEq(no.rewardAddress, nodeOperator); @@ -600,7 +603,7 @@ contract CsmConfirmNodeOperatorManagerAddressChange is CSMCommon { contract CsmProposeNodeOperatorRewardAddressChange is CSMCommon { function test_proposeNodeOperatorRewardAddressChange() public { uint256 noId = createNodeOperator(); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.managerAddress, nodeOperator); assertEq(no.rewardAddress, nodeOperator); @@ -652,7 +655,7 @@ contract CsmProposeNodeOperatorRewardAddressChange is CSMCommon { contract CsmConfirmNodeOperatorRewardAddressChange is CSMCommon { function test_confirmNodeOperatorRewardAddressChange() public { uint256 noId = createNodeOperator(); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.managerAddress, nodeOperator); assertEq(no.rewardAddress, nodeOperator); @@ -712,7 +715,7 @@ contract CsmResetNodeOperatorManagerAddress is CSMCommon { vm.prank(stranger); csm.resetNodeOperatorManagerAddress(noId); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.managerAddress, stranger); assertEq(no.rewardAddress, stranger); } @@ -751,7 +754,7 @@ contract CsmVetKeys is CSMCommon { emit VettedSigningKeysCountChanged(noId, 1); csm.vetKeys(noId, 1); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 1); BatchInfo[] memory exp = new BatchInfo[](1); @@ -774,7 +777,7 @@ contract CsmVetKeys is CSMCommon { emit VettedSigningKeysCountChanged(noId, 2); csm.vetKeys(noId, 2); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 2); BatchInfo[] memory exp = new BatchInfo[](2); @@ -1098,7 +1101,7 @@ contract CsmRemoveKeys is CSMCommon { }); assertEq(obtainedKeys, bytes.concat(key4, key3), "unexpected keys"); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalAddedValidators, 2); } @@ -1136,7 +1139,7 @@ contract CsmRemoveKeys is CSMCommon { "unexpected keys" ); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalAddedValidators, 3); } @@ -1174,7 +1177,7 @@ contract CsmRemoveKeys is CSMCommon { "unexpected keys" ); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalAddedValidators, 3); } @@ -1212,7 +1215,7 @@ contract CsmRemoveKeys is CSMCommon { "unexpected keys" ); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalAddedValidators, 3); } @@ -1231,7 +1234,7 @@ contract CsmRemoveKeys is CSMCommon { csm.removeKeys({ nodeOperatorId: noId, startIndex: 0, keysCount: 5 }); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalAddedValidators, 0); } @@ -1260,7 +1263,7 @@ contract CsmRemoveKeys is CSMCommon { } csm.removeKeys({ nodeOperatorId: noId, startIndex: 1, keysCount: 2 }); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 1); } @@ -1282,7 +1285,7 @@ contract CsmRemoveKeys is CSMCommon { */ csm.removeKeys({ nodeOperatorId: noId, startIndex: 3, keysCount: 2 }); - NodeOperatorInfo memory no = csm.getNodeOperator(0); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(0); assertEq(no.totalVettedValidators, 3); } @@ -1413,7 +1416,7 @@ contract CsmGetNodeOperatorSummary is CSMCommon { assertTrue(summary.isTargetLimitActive); assertEq(summary.targetValidatorsCount, 2); // should be unvetted - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 0); } @@ -1471,7 +1474,7 @@ contract CsmUpdateTargetValidatorsLimits is CSMCommon { csm.vetKeys(noId, 1); csm.updateTargetValidatorsLimits(noId, true, 1); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 0); } @@ -1484,7 +1487,7 @@ contract CsmUpdateTargetValidatorsLimits is CSMCommon { csm.vetKeys(noId, 1); csm.updateTargetValidatorsLimits(noId, true, 2); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 1); } @@ -1497,7 +1500,7 @@ contract CsmUpdateTargetValidatorsLimits is CSMCommon { csm.vetKeys(noId, 1); csm.updateTargetValidatorsLimits(noId, false, 1); - NodeOperatorInfo memory no = csm.getNodeOperator(noId); + CSModule.NodeOperatorInfo memory no = csm.getNodeOperator(noId); assertEq(no.totalVettedValidators, 1); } diff --git a/test/helpers/Utilities.sol b/test/helpers/Utilities.sol index 2dab7a6f..1d443ca8 100644 --- a/test/helpers/Utilities.sol +++ b/test/helpers/Utilities.sol @@ -71,4 +71,15 @@ contract Utilities is CommonBase { revert("wrong chain id"); } } + + function accessErrorString( + address account, + bytes32 role + ) internal pure returns (string memory) { + string memory errorString = "AccessControl: account "; + errorString = string.concat(errorString, vm.toString(account)); + errorString = string.concat(errorString, " is missing role "); + errorString = string.concat(errorString, vm.toString(role)); + return errorString; + } } diff --git a/test/integration/DepositInTokens.t.sol b/test/integration/DepositInTokens.t.sol index 2b4854f3..f190be8e 100644 --- a/test/integration/DepositInTokens.t.sol +++ b/test/integration/DepositInTokens.t.sol @@ -51,8 +51,11 @@ contract DepositIntegrationTest is strangerPrivateKey = 0x517a4637; stranger = vm.addr(strangerPrivateKey); + uint256[] memory curve = new uint256[](2); + curve[0] = 2 ether; + curve[1] = 4 ether; accounting = new CSAccounting( - 2 ether, + curve, user, address(locator), address(wstETH), diff --git a/test/integration/StakingRouter.t.sol b/test/integration/StakingRouter.t.sol index 990249b0..8a1b0960 100644 --- a/test/integration/StakingRouter.t.sol +++ b/test/integration/StakingRouter.t.sol @@ -40,8 +40,11 @@ contract StakingRouterIntegrationTest is Test, Utilities, IntegrationFixtures { vm.label(address(stakingRouter), "stakingRouter"); csm = new CSModule("community-staking-module", address(locator)); + uint256[] memory curve = new uint256[](2); + curve[0] = 2 ether; + curve[1] = 4 ether; CSAccounting accounting = new CSAccounting( - 2 ether, + curve, address(csm), address(locator), address(wstETH),