From 126c5ccc44841e9b8cacfdc73d652aa2df32b630 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 31 Oct 2023 11:49:34 +0400 Subject: [PATCH 1/8] feat: add solhint to makefile --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 777823ec..a2cceab7 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,8 @@ artifacts: clean: forge clean rm -rf cache broadcast out +lint-solhint: + yarn lint:solhint lint-check: yarn lint:check lint-fix: @@ -48,6 +50,7 @@ endif # aliases a: artifacts +ls: lint-solhint lc: lint-check lf: lint-fix t: test From 4cf442e5a3585d74f32b6fc4bd050dc01e8237e0 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 26 Oct 2023 17:29:16 +0400 Subject: [PATCH 2/8] feat: block funds by mev committee --- script/Deploy.s.sol | 14 +- src/CSAccounting.sol | 292 +++++++++++++++++++------ test/CSAccounting.t.sol | 41 ++-- test/CSMAddValidator.t.sol | 2 +- test/CSMInit.t.sol | 2 +- test/integration/DepositInTokens.t.sol | 4 +- test/integration/StakingRouter.t.sol | 4 +- 7 files changed, 266 insertions(+), 93 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index a7359aa8..300125fc 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -29,8 +29,6 @@ contract Deploy is Script { uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); address deployerAddress = vm.addr(deployerPrivateKey); - address[] memory penalizers = new address[](1); - penalizers[0] = deployerAddress; // TODO: temporary vm.startBroadcast(deployerPrivateKey); locator = ILidoLocator(LIDO_LOCATOR_ADDRESS); @@ -45,7 +43,7 @@ contract Deploy is Script { lidoLocator: address(locator), communityStakingModule: address(csm), wstETH: address(wstETH), - penalizeRoleMembers: penalizers + blockedBondRetentionPeriod: 8 weeks }); CSFeeOracle feeOracle = new CSFeeOracle({ secondsPerBlock: 12, @@ -65,6 +63,16 @@ contract Deploy is Script { admin: deployerAddress }); accounting.setFeeDistributor(address(feeDistributor)); + // TODO: temporary + accounting.grantRole(accounting.PENALIZE_BOND_ROLE(), deployerAddress); + accounting.grantRole( + accounting.EL_REWARDS_STEALING_PENALTY_ROLE(), + deployerAddress + ); + accounting.grantRole( + accounting.EASY_TRACK_MOTION_AGENT_ROLE(), + deployerAddress + ); // TODO: csm.setBondManager(address(accounting)); vm.stopBroadcast(); diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index bf229d58..34f174a2 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -28,11 +28,6 @@ contract CSAccountingBase { address from, uint256 amount ); - event BondPenalized( - uint256 indexed nodeOperatorId, - uint256 penaltyShares, - uint256 burnedShares - ); event StETHRewardsClaimed( uint256 indexed nodeOperatorId, address to, @@ -48,6 +43,25 @@ contract CSAccountingBase { address to, uint256 amount ); + event ELRewardsStealingReported( + uint256 indexed nodeOperatorId, + uint256 proposedBlockNumber, + uint256 stolenAmount + ); + event BlockedBondChanged( + uint256 indexed nodeOperatorId, + uint256 newETHAmount, + uint256 retentionUntil + ); + event BlockedBondCompensated( + uint256 indexed nodeOperatorId, + uint256 ETHAmount + ); + event BondPenalized( + uint256 indexed nodeOperatorId, + uint256 penaltyETH, + uint256 coveringETH + ); } contract CSAccounting is CSAccountingBase, AccessControlEnumerable { @@ -58,19 +72,30 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { bytes32 r; bytes32 s; } + struct BlockedBondEther { + uint256 ETHAmount; + uint256 retentionUntil; + } bytes32 public constant PENALIZE_BOND_ROLE = keccak256("PENALIZE_BOND_ROLE"); + bytes32 public constant EL_REWARDS_STEALING_PENALTY_ROLE = + keccak256("EL_REWARDS_STEALING_PENALTY_ROLE"); + bytes32 public constant EASY_TRACK_MOTION_AGENT_ROLE = + keccak256("EASY_TRACK_MOTION_AGENT_ROLE"); + + uint256 public immutable COMMON_BOND_SIZE; + uint256 public immutable BLOCKED_BOND_RETENTION_PERIOD; ILidoLocator private immutable LIDO_LOCATOR; ICSModule private immutable CSM; IWstETH private immutable WSTETH; - uint256 private immutable COMMON_BOND_SIZE; address public FEE_DISTRIBUTOR; uint256 public totalBondShares; mapping(uint256 => uint256) private _bondShares; + mapping(uint256 => BlockedBondEther) private _blockedBondEther; error NotOwnerToClaim(address msgSender, address owner); @@ -79,14 +104,14 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { /// @param lidoLocator lido locator contract address /// @param wstETH wstETH contract address /// @param communityStakingModule community staking module contract address - /// @param penalizeRoleMembers list of addresses with PENALIZE_BOND_ROLE + /// @param blockedBondRetentionPeriod retention period for blocked bond in seconds constructor( uint256 commonBondSize, address admin, address lidoLocator, address wstETH, address communityStakingModule, - address[] memory penalizeRoleMembers + uint256 blockedBondRetentionPeriod ) { // check zero addresses require(admin != address(0), "admin is zero address"); @@ -97,24 +122,18 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { ); require(wstETH != address(0), "wstETH is zero address"); require( - penalizeRoleMembers.length > 0, - "penalize role members is empty" + blockedBondRetentionPeriod > 7 days, + "blocked bond retention period should be greater than 7 days" ); _setupRole(DEFAULT_ADMIN_ROLE, admin); - for (uint256 i; i < penalizeRoleMembers.length; ++i) { - require( - penalizeRoleMembers[i] != address(0), - "penalize role member is zero address" - ); - _setupRole(PENALIZE_BOND_ROLE, penalizeRoleMembers[i]); - } LIDO_LOCATOR = ILidoLocator(lidoLocator); CSM = ICSModule(communityStakingModule); WSTETH = IWstETH(wstETH); COMMON_BOND_SIZE = commonBondSize; + BLOCKED_BOND_RETENTION_PERIOD = blockedBondRetentionPeriod; } function setFeeDistributor( @@ -139,7 +158,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { bytes32[] memory rewardsProof, uint256 nodeOperatorId, uint256 cumulativeFeeShares - ) public view returns (uint256) { + ) public view onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { (uint256 current, uint256 required) = _bondSharesSummary( _getNodeOperatorActiveKeys(nodeOperatorId) ); @@ -219,7 +238,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { /// @return missing bond ETH. function getMissingBondETH( uint256 nodeOperatorId - ) public view returns (uint256) { + ) public view onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { (uint256 current, uint256 required) = _bondETHSummary(nodeOperatorId); return required > current ? required - current : 0; } @@ -242,6 +261,18 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { return WSTETH.getWstETHByStETH(getMissingBondStETH(nodeOperatorId)); } + /// @notice Returns the amount of ETH blocked by the given node operator. + function getBlockedBondETH( + uint256 nodeOperatorId + ) public view returns (uint256) { + if ( + _blockedBondEther[nodeOperatorId].retentionUntil > block.timestamp + ) { + return _blockedBondEther[nodeOperatorId].ETHAmount; + } + return 0; + } + /// @notice Returns the required bond ETH (inc. missed and excess) for the given node operator to upload new keys. /// @param nodeOperatorId id of the node operator to get required bond for. /// @return required bond ETH. @@ -250,21 +281,10 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 additionalKeysCount ) public view returns (uint256) { (uint256 current, uint256 required) = _bondETHSummary(nodeOperatorId); - uint256 requiredForKeys = getRequiredBondETHForKeys( - additionalKeysCount - ); - - uint256 missing = required > current ? required - current : 0; - if (missing > 0) { - return missing + requiredForKeys; - } - - uint256 excess = current - required; - if (excess >= requiredForKeys) { - return 0; - } - - return requiredForKeys - excess; + required += + getRequiredBondETHForKeys(additionalKeysCount) + + getBlockedBondETH(nodeOperatorId); + return required > current ? required - current : 0; } /// @notice Returns the required bond stETH (inc. missed and excess) for the given node operator to upload new keys. @@ -362,13 +382,13 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function depositETH( address from, uint256 nodeOperatorId - ) external payable returns (uint256) { + ) + external + payable + onlyExistingNodeOperator(nodeOperatorId) + returns (uint256) + { from = (from == address(0)) ? msg.sender : from; - // TODO: should be modifier. condition might be changed as well - require( - nodeOperatorId < CSM.getNodeOperatorsCount(), - "node operator does not exist" - ); uint256 shares = _lido().submit{ value: msg.value }(address(0)); _bondShares[nodeOperatorId] += shares; totalBondShares += shares; @@ -383,7 +403,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address from, uint256 nodeOperatorId, uint256 stETHAmount - ) external returns (uint256) { + ) external onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { from = (from == address(0)) ? msg.sender : from; return _depositStETH(from, nodeOperatorId, stETHAmount); } @@ -397,7 +417,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 stETHAmount, PermitInput calldata permit - ) external returns (uint256) { + ) external onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { from = (from == address(0)) ? msg.sender : from; // solhint-disable-next-line func-named-parameters _lido().permit( @@ -417,10 +437,6 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 stETHAmount ) internal returns (uint256) { - require( - nodeOperatorId < CSM.getNodeOperatorsCount(), - "node operator does not exist" - ); uint256 shares = _sharesByEth(stETHAmount); _lido().transferSharesFrom(from, address(this), shares); _bondShares[nodeOperatorId] += shares; @@ -437,7 +453,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address from, uint256 nodeOperatorId, uint256 wstETHAmount - ) external returns (uint256) { + ) external onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { from = (from == address(0)) ? msg.sender : from; return _depositWstETH(from, nodeOperatorId, wstETHAmount); } @@ -452,7 +468,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 wstETHAmount, PermitInput calldata permit - ) external returns (uint256) { + ) external onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { // solhint-disable-next-line func-named-parameters WSTETH.permit( from, @@ -471,10 +487,6 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 wstETHAmount ) internal returns (uint256) { - require( - nodeOperatorId < CSM.getNodeOperatorsCount(), - "node operator does not exist" - ); WSTETH.transferFrom(from, address(this), wstETHAmount); uint256 stETHAmount = WSTETH.unwrap(wstETHAmount); uint256 shares = _sharesByEth(stETHAmount); @@ -494,7 +506,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 cumulativeFeeShares, uint256 stETHAmount - ) external { + ) external onlyExistingNodeOperator(nodeOperatorId) { ( address managerAddress, address rewardAddress @@ -505,7 +517,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { nodeOperatorId, cumulativeFeeShares ); - if (claimableShares == 0) { + if (claimableShares == 0 || getBlockedBondETH(nodeOperatorId) > 0) { emit StETHRewardsClaimed(nodeOperatorId, rewardAddress, 0); return; } @@ -532,7 +544,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 cumulativeFeeShares, uint256 wstETHAmount - ) external { + ) external onlyExistingNodeOperator(nodeOperatorId) { ( address managerAddress, address rewardAddress @@ -543,7 +555,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { nodeOperatorId, cumulativeFeeShares ); - if (claimableShares == 0) { + if (claimableShares == 0 || getBlockedBondETH(nodeOperatorId) > 0) { emit WstETHRewardsClaimed(nodeOperatorId, rewardAddress, 0); return; } @@ -568,7 +580,11 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId, uint256 cumulativeFeeShares, uint256 ETHAmount - ) external returns (uint256[] memory requestIds) { + ) + external + onlyExistingNodeOperator(nodeOperatorId) + returns (uint256[] memory requestIds) + { ( address managerAddress, address rewardAddress @@ -579,6 +595,10 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { nodeOperatorId, cumulativeFeeShares ); + if (claimableShares == 0 || getBlockedBondETH(nodeOperatorId) > 0) { + emit ETHRewardsRequested(nodeOperatorId, rewardAddress, 0); + return requestIds; + } uint256 toClaim = ETHAmount < _ethByShares(claimableShares) ? _sharesByEth(ETHAmount) : claimableShares; @@ -594,23 +614,153 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { return requestIds; } - /// @notice Penalize bond by burning shares + /// @notice Reports EL rewards stealing for the given node operator. + /// @param nodeOperatorId id of the node operator to report EL rewards stealing for. + /// @param proposedBlockNumber block number of the proposed block with EL rewards stealing. + /// @param stolenAmount amount of stolen EL rewards. + function initELRewardsStealingPenalty( + uint256 nodeOperatorId, + uint256 proposedBlockNumber, + uint256 stolenAmount + ) + external + onlyRole(EL_REWARDS_STEALING_PENALTY_ROLE) + onlyExistingNodeOperator(nodeOperatorId) + { + require(stolenAmount > 0, "stolen amount should be greater than zero"); + emit ELRewardsStealingReported( + nodeOperatorId, + proposedBlockNumber, + stolenAmount + ); + _changeBlockedBondState({ + nodeOperatorId: nodeOperatorId, + ETHAmount: _blockedBondEther[nodeOperatorId].ETHAmount + + stolenAmount, + retentionUntil: block.timestamp + BLOCKED_BOND_RETENTION_PERIOD + }); + } + + /// @notice Releases blocked bond ETH for the given node operator. + /// @param nodeOperatorId id of the node operator to release blocked bond for. + /// @param amount amount of ETH to release. + function releaseBlockedBondETH( + uint256 nodeOperatorId, + uint256 amount + ) + external + onlyRole(EL_REWARDS_STEALING_PENALTY_ROLE) + onlyExistingNodeOperator(nodeOperatorId) + { + _releaseBlockedBondETH(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( + uint256 nodeOperatorId + ) external payable onlyExistingNodeOperator(nodeOperatorId) { + require(msg.value > 0, "value should be greater than zero"); + payable(LIDO_LOCATOR.elRewardsVault()).transfer(msg.value); + _releaseBlockedBondETH(nodeOperatorId, msg.value); + } + + function _releaseBlockedBondETH( + 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 compensate" + ); + _changeBlockedBondState( + nodeOperatorId, + _blockedBondEther[nodeOperatorId].ETHAmount - amount, + _blockedBondEther[nodeOperatorId].retentionUntil + ); + } + + /// @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 settleBlockedBondETH( + uint256[] memory nodeOperatorIds + ) external onlyRole(EASY_TRACK_MOTION_AGENT_ROLE) { + uint256 nosCount = CSM.getNodeOperatorsCount(); + for (uint256 i; i < nodeOperatorIds.length; ++i) { + uint256 nodeOperatorId = nodeOperatorIds[i]; + if (nodeOperatorId >= nosCount) { + continue; + } + BlockedBondEther storage data = _blockedBondEther[nodeOperatorId]; + if ( + block.timestamp + + BLOCKED_BOND_RETENTION_PERIOD - + data.retentionUntil < + 1 days + ) { + // blocked bond in safe frame to manage it by committee or node operator + continue; + } + uint256 blockedAmount = getBlockedBondETH(nodeOperatorId); + _changeBlockedBondState({ + nodeOperatorId: nodeOperatorId, + ETHAmount: blockedAmount > 0 + ? penalize(nodeOperatorId, blockedAmount) + : 0, + retentionUntil: data.retentionUntil + }); + } + } + + function _changeBlockedBondState( + uint256 nodeOperatorId, + uint256 ETHAmount, + uint256 retentionUntil + ) internal { + if (ETHAmount == 0) { + delete _blockedBondEther[nodeOperatorId]; + emit BlockedBondChanged(nodeOperatorId, 0, 0); + return; + } + _blockedBondEther[nodeOperatorId] = BlockedBondEther({ + ETHAmount: ETHAmount, + retentionUntil: retentionUntil + }); + emit BlockedBondChanged(nodeOperatorId, ETHAmount, retentionUntil); + } + + /// @notice Penalize bond by burning shares of the given node operator. /// @param nodeOperatorId id of the node operator to penalize bond for. - /// @param shares amount shares to burn. + /// @param ETHAmount amount of ETH to penalize. + /// @return uncovered amount. function penalize( uint256 nodeOperatorId, - uint256 shares - ) external onlyRole(PENALIZE_BOND_ROLE) { - uint256 currentBond = getBondShares(nodeOperatorId); - uint256 coveringShares = shares < currentBond ? shares : currentBond; + uint256 ETHAmount + ) + public + onlyRole(PENALIZE_BOND_ROLE) + onlyExistingNodeOperator(nodeOperatorId) + returns (uint256) + { + uint256 penaltyShares = _sharesByEth(ETHAmount); + uint256 currentShares = getBondShares(nodeOperatorId); + uint256 sharesToBurn = penaltyShares < currentShares + ? penaltyShares + : currentShares; _lido().transferSharesFrom( address(this), LIDO_LOCATOR.burner(), - coveringShares + sharesToBurn ); - _bondShares[nodeOperatorId] -= coveringShares; - totalBondShares -= coveringShares; - emit BondPenalized(nodeOperatorId, shares, coveringShares); + _bondShares[nodeOperatorId] -= sharesToBurn; + totalBondShares -= sharesToBurn; + uint256 penaltyEth = _ethByShares(penaltyShares); + uint256 coveringEth = _ethByShares(sharesToBurn); + emit BondPenalized(nodeOperatorId, penaltyEth, coveringEth); + return penaltyEth - coveringEth; } function _lido() internal view returns (ILido) { @@ -694,4 +844,12 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function _ethByShares(uint256 shares) internal view returns (uint256) { return _lido().getPooledEthByShares(shares); } + + modifier onlyExistingNodeOperator(uint256 nodeOperatorId) { + require( + nodeOperatorId < CSM.getNodeOperatorsCount(), + "node operator does not exist" + ); + _; + } } diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index e2ff1a02..86859dbd 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -45,9 +45,6 @@ contract CSAccountingTest is user = address(2); stranger = address(777); - address[] memory penalizeRoleMembers = new address[](1); - penalizeRoleMembers[0] = admin; - (locator, wstETH, stETH, burner) = initLido(); stakingModule = new CommunityStakingModuleMock(); @@ -57,14 +54,21 @@ contract CSAccountingTest is address(locator), address(wstETH), address(stakingModule), - penalizeRoleMembers + 8 weeks ); feeDistributor = new CommunityStakingFeeDistributorMock( address(locator), address(accounting) ); - vm.prank(admin); + vm.startPrank(admin); accounting.setFeeDistributor(address(feeDistributor)); + accounting.grantRole(accounting.PENALIZE_BOND_ROLE(), admin); + accounting.grantRole( + accounting.EL_REWARDS_STEALING_PENALTY_ROLE(), + admin + ); + accounting.grantRole(accounting.EASY_TRACK_MOTION_AGENT_ROLE(), admin); + vm.stopPrank(); } function test_totalBondShares() public { @@ -1048,26 +1052,28 @@ contract CSAccountingTest is accounting.depositStETH(user, 0, 32 ether); vm.stopPrank(); + uint256 shares = stETH.getSharesByPooledEth(1 ether); + uint256 penalized = stETH.getPooledEthByShares(shares); vm.expectEmit(true, true, true, true, address(accounting)); - emit BondPenalized(0, 1e18, 1e18); + emit BondPenalized(0, penalized, penalized); uint256 bondSharesBefore = accounting.getBondShares(0); vm.prank(admin); - accounting.penalize(0, 1e18); + accounting.penalize(0, 1 ether); assertEq( accounting.getBondShares(0), - bondSharesBefore - 1e18, + bondSharesBefore - shares, "bond shares should be decreased by penalty" ); assertEq( stETH.sharesOf(address(accounting)), - bondSharesBefore - 1e18, + bondSharesBefore - shares, "bond manager shares should be decreased by penalty" ); assertEq( stETH.sharesOf(address(burner)), - 1e18, + shares, "burner shares should be equal to penalty" ); } @@ -1081,12 +1087,16 @@ contract CSAccountingTest is vm.stopPrank(); uint256 bondSharesBefore = accounting.getBondShares(0); - + uint256 penaltyShares = stETH.getSharesByPooledEth(33 ether); vm.expectEmit(true, true, true, true, address(accounting)); - emit BondPenalized(0, 32 * 1e18, bondSharesBefore); + emit BondPenalized( + 0, + stETH.getPooledEthByShares(penaltyShares), + stETH.getPooledEthByShares(bondSharesBefore) + ); vm.prank(admin); - accounting.penalize(0, 32 * 1e18); + accounting.penalize(0, 33 ether); assertEq( accounting.getBondShares(0), @@ -1114,11 +1124,12 @@ contract CSAccountingTest is vm.stopPrank(); uint256 shares = stETH.getSharesByPooledEth(32 ether); + uint256 penalized = stETH.getPooledEthByShares(shares); vm.expectEmit(true, true, true, true, address(accounting)); - emit BondPenalized(0, shares, shares); + emit BondPenalized(0, penalized, penalized); vm.prank(admin); - accounting.penalize(0, shares); + accounting.penalize(0, 32 ether); assertEq( accounting.getBondShares(0), diff --git a/test/CSMAddValidator.t.sol b/test/CSMAddValidator.t.sol index 7398533a..034c24ca 100644 --- a/test/CSMAddValidator.t.sol +++ b/test/CSMAddValidator.t.sol @@ -49,7 +49,7 @@ contract CSMCommon is Test, Fixtures, Utilities, CSModuleBase { address(locator), address(wstETH), address(csm), - penalizeRoleMembers + 8 weeks ); csm.setAccounting(address(accounting)); } diff --git a/test/CSMInit.t.sol b/test/CSMInit.t.sol index c7d88191..bb46ec9f 100644 --- a/test/CSMInit.t.sol +++ b/test/CSMInit.t.sol @@ -42,7 +42,7 @@ contract CSMInitTest is Test, Fixtures { address(locator), address(wstETH), address(csm), - penalizeRoleMembers + 8 weeks ); } diff --git a/test/integration/DepositInTokens.t.sol b/test/integration/DepositInTokens.t.sol index 68c76806..529870e0 100644 --- a/test/integration/DepositInTokens.t.sol +++ b/test/integration/DepositInTokens.t.sol @@ -55,8 +55,6 @@ contract DepositIntegrationTest is Test, PermitHelper { user = vm.addr(userPrivateKey); strangerPrivateKey = 0x517a4637; stranger = vm.addr(strangerPrivateKey); - address[] memory penalizeRoleMembers = new address[](1); - penalizeRoleMembers[0] = user; accounting = new CSAccounting( 2 ether, @@ -64,7 +62,7 @@ contract DepositIntegrationTest is Test, PermitHelper { address(locator), address(wstETH), address(csm), - penalizeRoleMembers + 8 weeks ); csm.setNodeOperator({ diff --git a/test/integration/StakingRouter.t.sol b/test/integration/StakingRouter.t.sol index 88e189b7..700d58d2 100644 --- a/test/integration/StakingRouter.t.sol +++ b/test/integration/StakingRouter.t.sol @@ -49,15 +49,13 @@ contract StakingRouterIntegrationTest is Test, Utilities { vm.label(address(stakingRouter), "stakingRouter"); csm = new CSModule("community-staking-module", address(locator)); - address[] memory penalizeRoleMembers = new address[](1); - penalizeRoleMembers[0] = address(csm); CSAccounting accounting = new CSAccounting( 2 ether, address(csm), address(locator), address(wstETH), address(csm), - penalizeRoleMembers + 8 weeks ); csm.setAccounting(address(accounting)); From fc5187ab3d10593cc703b0726bbd2e6c6219ba81 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 1 Nov 2023 17:41:45 +0400 Subject: [PATCH 3/8] feat: tests --- src/CSAccounting.sol | 40 +- test/CSAccounting.blockedBond.t.sol | 729 +++++++++++++++++++++++++ test/helpers/Fixtures.sol | 4 +- test/helpers/mocks/LidoLocatorMock.sol | 8 +- test/helpers/mocks/Stub.sol | 4 +- 5 files changed, 767 insertions(+), 18 deletions(-) create mode 100644 test/CSAccounting.blockedBond.t.sol diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index 34f174a2..025332d7 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -43,7 +43,7 @@ contract CSAccountingBase { address to, uint256 amount ); - event ELRewardsStealingReported( + event ELRewardsStealingPenaltyInitiated( uint256 indexed nodeOperatorId, uint256 proposedBlockNumber, uint256 stolenAmount @@ -57,6 +57,10 @@ contract CSAccountingBase { uint256 indexed nodeOperatorId, uint256 ETHAmount ); + event BlockedBondReleased( + uint256 indexed nodeOperatorId, + uint256 ETHAmount + ); event BondPenalized( uint256 indexed nodeOperatorId, uint256 penaltyETH, @@ -94,8 +98,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address public FEE_DISTRIBUTOR; uint256 public totalBondShares; - mapping(uint256 => uint256) private _bondShares; - mapping(uint256 => BlockedBondEther) private _blockedBondEther; + mapping(uint256 => uint256) internal _bondShares; + mapping(uint256 => BlockedBondEther) internal _blockedBondEther; error NotOwnerToClaim(address msgSender, address owner); @@ -212,6 +216,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId ) public view returns (uint256) { (uint256 current, uint256 required) = _bondETHSummary(nodeOperatorId); + required += getBlockedBondETH(nodeOperatorId); return current > required ? current - required : 0; } @@ -266,7 +271,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId ) public view returns (uint256) { if ( - _blockedBondEther[nodeOperatorId].retentionUntil > block.timestamp + _blockedBondEther[nodeOperatorId].retentionUntil >= block.timestamp ) { return _blockedBondEther[nodeOperatorId].ETHAmount; } @@ -517,7 +522,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { nodeOperatorId, cumulativeFeeShares ); - if (claimableShares == 0 || getBlockedBondETH(nodeOperatorId) > 0) { + if (claimableShares == 0) { emit StETHRewardsClaimed(nodeOperatorId, rewardAddress, 0); return; } @@ -555,7 +560,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { nodeOperatorId, cumulativeFeeShares ); - if (claimableShares == 0 || getBlockedBondETH(nodeOperatorId) > 0) { + if (claimableShares == 0) { emit WstETHRewardsClaimed(nodeOperatorId, rewardAddress, 0); return; } @@ -595,7 +600,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { nodeOperatorId, cumulativeFeeShares ); - if (claimableShares == 0 || getBlockedBondETH(nodeOperatorId) > 0) { + if (claimableShares == 0) { emit ETHRewardsRequested(nodeOperatorId, rewardAddress, 0); return requestIds; } @@ -628,7 +633,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { onlyExistingNodeOperator(nodeOperatorId) { require(stolenAmount > 0, "stolen amount should be greater than zero"); - emit ELRewardsStealingReported( + emit ELRewardsStealingPenaltyInitiated( nodeOperatorId, proposedBlockNumber, stolenAmount @@ -652,7 +657,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { onlyRole(EL_REWARDS_STEALING_PENALTY_ROLE) onlyExistingNodeOperator(nodeOperatorId) { - _releaseBlockedBondETH(nodeOperatorId, amount); + emit BlockedBondReleased(nodeOperatorId, amount); + _reduceBlockedBondETH(nodeOperatorId, amount); } /// @notice Compensates blocked bond ETH for the given node operator. @@ -662,10 +668,11 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { ) external payable onlyExistingNodeOperator(nodeOperatorId) { require(msg.value > 0, "value should be greater than zero"); payable(LIDO_LOCATOR.elRewardsVault()).transfer(msg.value); - _releaseBlockedBondETH(nodeOperatorId, msg.value); + emit BlockedBondCompensated(nodeOperatorId, msg.value); + _reduceBlockedBondETH(nodeOperatorId, msg.value); } - function _releaseBlockedBondETH( + function _reduceBlockedBondETH( uint256 nodeOperatorId, uint256 amount ) internal { @@ -673,7 +680,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { require(blocked > 0, "no blocked bond to release"); require( _blockedBondEther[nodeOperatorId].ETHAmount >= amount, - "blocked bond is less than amount to compensate" + "blocked bond is less than amount to release" ); _changeBlockedBondState( nodeOperatorId, @@ -705,11 +712,13 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { continue; } uint256 blockedAmount = getBlockedBondETH(nodeOperatorId); + uint256 uncovered; + if (blockedAmount > 0) { + uncovered = penalize(nodeOperatorId, blockedAmount); + } _changeBlockedBondState({ nodeOperatorId: nodeOperatorId, - ETHAmount: blockedAmount > 0 - ? penalize(nodeOperatorId, blockedAmount) - : 0, + ETHAmount: uncovered, retentionUntil: data.retentionUntil }); } @@ -816,6 +825,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { (uint256 current, uint256 required) = _bondSharesSummary( nodeOperatorId ); + required += _sharesByEth(getBlockedBondETH(nodeOperatorId)); claimableShares = current > required ? current - required : 0; } diff --git a/test/CSAccounting.blockedBond.t.sol b/test/CSAccounting.blockedBond.t.sol new file mode 100644 index 00000000..8ed62536 --- /dev/null +++ b/test/CSAccounting.blockedBond.t.sol @@ -0,0 +1,729 @@ +// 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 + ) + CSAccounting( + commonBondSize, + admin, + lidoLocator, + wstETH, + communityStakingModule, + blockedBondRetentionPeriod + ) + {} + + function _bondShares_set_value( + uint256 nodeOperatorId, + uint256 value + ) public { + _bondShares[nodeOperatorId] = value; + } + + function _blockedBondEther_get_value( + uint256 nodeOperatorId + ) public view returns (BlockedBondEther memory) { + return _blockedBondEther[nodeOperatorId]; + } + + function _blockedBondEther_set_value( + uint256 nodeOperatorId, + BlockedBondEther 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 + ); + feeDistributor = new CommunityStakingFeeDistributorMock( + address(locator), + address(accounting) + ); + vm.startPrank(admin); + accounting.setFeeDistributor(address(feeDistributor)); + accounting.grantRole(accounting.PENALIZE_BOND_ROLE(), admin); + accounting.grantRole( + accounting.EL_REWARDS_STEALING_PENALTY_ROLE(), + admin + ); + accounting.grantRole(accounting.EASY_TRACK_MOTION_AGENT_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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther 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, + proposedBlockNumber: proposedBlockNumber, + stolenAmount: 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, + proposedBlockNumber: proposedBlockNumber, + stolenAmount: 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, + proposedBlockNumber: 100500, + stolenAmount: 100 ether + }); + } + + function test_initELRewardsStealingPenalty_revertWhenZero() public { + _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); + + vm.expectRevert("stolen amount should be greater than zero"); + + vm.prank(admin); + accounting.initELRewardsStealingPenalty({ + nodeOperatorId: 0, + proposedBlockNumber: 100500, + stolenAmount: 0 + }); + } + + function test_initELRewardsStealingPenalty_revertWhenNoRole() public { + _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); + + vm.expectRevert( + "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xd5726f791c124091830fa80135a85d3ea48ec1d26b2c06296f2175f326b23db3" + ); + + accounting.initELRewardsStealingPenalty({ + nodeOperatorId: 0, + proposedBlockNumber: 100500, + stolenAmount: 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.BlockedBondEther({ + 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.BlockedBondEther 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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.BlockedBondEther 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.BlockedBondEther({ + 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.BlockedBondEther({ + 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 0xd5726f791c124091830fa80135a85d3ea48ec1d26b2c06296f2175f326b23db3" + ); + 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.BlockedBondEther({ + 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/helpers/Fixtures.sol b/test/helpers/Fixtures.sol index b9c83a03..18b0bd93 100644 --- a/test/helpers/Fixtures.sol +++ b/test/helpers/Fixtures.sol @@ -25,11 +25,13 @@ contract Fixtures is StdCheats { _sharesAmount: 7059313073779349112833523 }); burner = new Stub(); + Stub elVault = new Stub(); WithdrawalQueueMock wq = new WithdrawalQueueMock(address(stETH)); locator = new LidoLocatorMock( address(stETH), address(burner), - address(wq) + address(wq), + address(elVault) ); wstETH = new WstETHMock(address(stETH)); } diff --git a/test/helpers/mocks/LidoLocatorMock.sol b/test/helpers/mocks/LidoLocatorMock.sol index 66fe8afd..754871bb 100644 --- a/test/helpers/mocks/LidoLocatorMock.sol +++ b/test/helpers/mocks/LidoLocatorMock.sol @@ -7,11 +7,13 @@ contract LidoLocatorMock { address public l; address public b; address public wq; + address public el; - constructor(address _lido, address _burner, address _wq) { + constructor(address _lido, address _burner, address _wq, address _el) { l = _lido; b = _burner; wq = _wq; + el = _el; } function lido() external view returns (address) { @@ -25,4 +27,8 @@ contract LidoLocatorMock { function withdrawalQueue() external view returns (address) { return wq; } + + function elRewardsVault() external view returns (address) { + return el; + } } diff --git a/test/helpers/mocks/Stub.sol b/test/helpers/mocks/Stub.sol index 79898ed5..500018c5 100644 --- a/test/helpers/mocks/Stub.sol +++ b/test/helpers/mocks/Stub.sol @@ -2,4 +2,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.21; -contract Stub {} +contract Stub { + receive() external payable {} +} From ba2b6956a67e133c655ec5639b02d58402fbea16 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 3 Nov 2023 12:42:50 +0400 Subject: [PATCH 4/8] feat: more tests --- test/CSAccounting.t.sol | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index 86859dbd..d433b367 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -597,6 +597,42 @@ contract CSAccountingTest is assertEq(accounting.getUnbondedKeysCount(0), 7); } + 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); + } + + 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); + } + + function test_getKeysCountByBondWstETH() public { + assertEq(accounting.getKeysCountByBondWstETH(0), 0); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(1.99 ether) + ), + 0 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(2 ether) + ), + 1 + ); + assertEq( + accounting.getKeysCountByBondWstETH( + wstETH.getWstETHByStETH(4 ether) + ), + 2 + ); + } + function test_claimRewardsStETH() public { _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); vm.deal(address(feeDistributor), 0.1 ether); From 54e6ab547f9a584b6877cec709de0ba6f6e9da93 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 6 Nov 2023 11:01:52 +0400 Subject: [PATCH 5/8] fix: required --- src/CSAccounting.sol | 37 +++++++++++++++++++++++++------------ test/CSAccounting.t.sol | 20 ++++++++++---------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index 025332d7..e789b73f 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -216,7 +216,6 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId ) public view returns (uint256) { (uint256 current, uint256 required) = _bondETHSummary(nodeOperatorId); - required += getBlockedBondETH(nodeOperatorId); return current > required ? current - required : 0; } @@ -286,10 +285,21 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 additionalKeysCount ) public view returns (uint256) { (uint256 current, uint256 required) = _bondETHSummary(nodeOperatorId); - required += - getRequiredBondETHForKeys(additionalKeysCount) + - getBlockedBondETH(nodeOperatorId); - return required > current ? required - current : 0; + uint256 requiredForKeys = getRequiredBondETHForKeys( + additionalKeysCount + ); + + uint256 missing = required > current ? required - current : 0; + if (missing > 0) { + return missing + requiredForKeys; + } + + uint256 excess = current - required; + if (excess >= requiredForKeys) { + return 0; + } + + return requiredForKeys - excess; } /// @notice Returns the required bond stETH (inc. missed and excess) for the given node operator to upload new keys. @@ -825,7 +835,6 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { (uint256 current, uint256 required) = _bondSharesSummary( nodeOperatorId ); - required += _sharesByEth(getBlockedBondETH(nodeOperatorId)); claimableShares = current > required ? current - required : 0; } @@ -833,18 +842,22 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nodeOperatorId ) internal view returns (uint256 current, uint256 required) { current = _ethByShares(getBondShares(nodeOperatorId)); - required = getRequiredBondETHForKeys( - _getNodeOperatorActiveKeys(nodeOperatorId) - ); + required = + getRequiredBondETHForKeys( + _getNodeOperatorActiveKeys(nodeOperatorId) + ) + + getBlockedBondETH(nodeOperatorId); } function _bondSharesSummary( uint256 nodeOperatorId ) internal view returns (uint256 current, uint256 required) { current = getBondShares(nodeOperatorId); - required = _getRequiredBondSharesForKeys( - _getNodeOperatorActiveKeys(nodeOperatorId) - ); + required = + _getRequiredBondSharesForKeys( + _getNodeOperatorActiveKeys(nodeOperatorId) + ) + + _sharesByEth(getBlockedBondETH(nodeOperatorId)); } function _sharesByEth(uint256 ethAmount) internal view returns (uint256) { diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index d433b367..ab232ba1 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -138,12 +138,12 @@ contract CSAccountingTest is _createNodeOperator({ ongoingVals: 16, withdrawnVals: 0 }); vm.deal(user, 64 ether); vm.startPrank(user); - accounting.depositETH{ value: 64 ether }(user, 0); + accounting.depositETH{ value: 63 ether }(user, 0); assertApproxEqAbs( accounting.getRequiredBondETH(0, 16), - 0, + 1 ether, 1, // max accuracy error - "required ETH should be ~0 for the next 16 validators to deposit" + "required ETH should be ~1 ether for the next 16 validators to deposit" ); } @@ -152,12 +152,12 @@ contract CSAccountingTest is vm.deal(user, 64 ether); vm.startPrank(user); stETH.submit{ value: 64 ether }({ _referal: address(0) }); - accounting.depositStETH(user, 0, 64 ether); + accounting.depositStETH(user, 0, 63 ether); assertApproxEqAbs( accounting.getRequiredBondStETH(0, 16), - 0, + 1 ether, 1, // max accuracy error - "required stETH should be ~0 for the next 16 validators to deposit" + "required stETH should be ~1 ether for the next 16 validators to deposit" ); } @@ -166,13 +166,13 @@ contract CSAccountingTest is vm.deal(user, 64 ether); vm.startPrank(user); stETH.submit{ value: 64 ether }({ _referal: address(0) }); - uint256 amount = wstETH.wrap(64 ether); + uint256 amount = wstETH.wrap(63 ether); accounting.depositWstETH(user, 0, amount); assertApproxEqAbs( accounting.getRequiredBondWstETH(0, 16), - 0, - 1, // max accuracy error - "required wstETH should be ~0 for the next 16 validators to deposit" + stETH.getSharesByPooledEth(1 ether), + 2, // max accuracy error + "required wstETH should be ~1 ether for the next 16 validators to deposit" ); } From 3aab7708783fc0e2595350a328e35636b74a13e5 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 7 Nov 2023 14:02:18 +0400 Subject: [PATCH 6/8] fix: remarks --- script/Deploy.s.sol | 13 ++- src/CSAccounting.sol | 118 ++++++++++++++++--------- test/CSAccounting.blockedBond.t.sol | 44 +++++---- test/CSAccounting.t.sol | 14 +-- test/CSMAddValidator.t.sol | 3 +- test/CSMInit.t.sol | 3 +- test/integration/DepositInTokens.t.sol | 3 +- test/integration/StakingRouter.t.sol | 3 +- 8 files changed, 129 insertions(+), 72 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 300125fc..78dc5d78 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -43,7 +43,9 @@ contract Deploy is Script { lidoLocator: address(locator), communityStakingModule: address(csm), wstETH: address(wstETH), - blockedBondRetentionPeriod: 8 weeks + // todo: arguable. should be discussed + blockedBondRetentionPeriod: 8 weeks, + blockedBondManagementPeriod: 1 weeks }); CSFeeOracle feeOracle = new CSFeeOracle({ secondsPerBlock: 12, @@ -64,13 +66,16 @@ contract Deploy is Script { }); accounting.setFeeDistributor(address(feeDistributor)); // TODO: temporary - accounting.grantRole(accounting.PENALIZE_BOND_ROLE(), deployerAddress); accounting.grantRole( - accounting.EL_REWARDS_STEALING_PENALTY_ROLE(), + accounting.INSTANT_PENALIZE_BOND_ROLE(), deployerAddress ); accounting.grantRole( - accounting.EASY_TRACK_MOTION_AGENT_ROLE(), + accounting.EL_REWARDS_STEALING_PENALTY_INIT_ROLE(), + deployerAddress + ); + accounting.grantRole( + accounting.EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE(), deployerAddress ); // TODO: csm.setBondManager(address(accounting)); diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index e789b73f..a4be4926 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -50,16 +50,16 @@ contract CSAccountingBase { ); event BlockedBondChanged( uint256 indexed nodeOperatorId, - uint256 newETHAmount, + uint256 newAmountETH, uint256 retentionUntil ); event BlockedBondCompensated( uint256 indexed nodeOperatorId, - uint256 ETHAmount + uint256 amountETH ); event BlockedBondReleased( uint256 indexed nodeOperatorId, - uint256 ETHAmount + uint256 amountETH ); event BondPenalized( uint256 indexed nodeOperatorId, @@ -81,15 +81,14 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 retentionUntil; } - bytes32 public constant PENALIZE_BOND_ROLE = - keccak256("PENALIZE_BOND_ROLE"); - bytes32 public constant EL_REWARDS_STEALING_PENALTY_ROLE = - keccak256("EL_REWARDS_STEALING_PENALTY_ROLE"); - bytes32 public constant EASY_TRACK_MOTION_AGENT_ROLE = - keccak256("EASY_TRACK_MOTION_AGENT_ROLE"); + bytes32 public constant INSTANT_PENALIZE_BOND_ROLE = + keccak256("INSTANT_PENALIZE_BOND_ROLE"); + bytes32 public constant EL_REWARDS_STEALING_PENALTY_INIT_ROLE = + keccak256("EL_REWARDS_STEALING_PENALTY_INIT_ROLE"); + bytes32 public constant EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE = + keccak256("EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE"); uint256 public immutable COMMON_BOND_SIZE; - uint256 public immutable BLOCKED_BOND_RETENTION_PERIOD; ILidoLocator private immutable LIDO_LOCATOR; ICSModule private immutable CSM; @@ -98,10 +97,15 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address public FEE_DISTRIBUTOR; uint256 public totalBondShares; + uint256 public BLOCKED_BOND_RETENTION_PERIOD; + uint256 public BLOCKED_BOND_MANAGEMENT_PERIOD; + mapping(uint256 => uint256) internal _bondShares; mapping(uint256 => BlockedBondEther) internal _blockedBondEther; error NotOwnerToClaim(address msgSender, address owner); + error InvalidBlockedBondRetentionPeriod(); + error InvalidStolenAmount(); /// @param commonBondSize common bond size in ETH for all node operators. /// @param admin admin role member address @@ -115,7 +119,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address lidoLocator, address wstETH, address communityStakingModule, - uint256 blockedBondRetentionPeriod + uint256 blockedBondRetentionPeriod, + uint256 blockedBondManagementPeriod ) { // check zero addresses require(admin != address(0), "admin is zero address"); @@ -125,11 +130,10 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { "community staking module is zero address" ); require(wstETH != address(0), "wstETH is zero address"); - require( - blockedBondRetentionPeriod > 7 days, - "blocked bond retention period should be greater than 7 days" + _validateBlockedBondPeriods( + blockedBondRetentionPeriod, + blockedBondManagementPeriod ); - _setupRole(DEFAULT_ADMIN_ROLE, admin); LIDO_LOCATOR = ILidoLocator(lidoLocator); @@ -137,7 +141,9 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { WSTETH = IWstETH(wstETH); COMMON_BOND_SIZE = commonBondSize; + BLOCKED_BOND_RETENTION_PERIOD = blockedBondRetentionPeriod; + BLOCKED_BOND_MANAGEMENT_PERIOD = blockedBondManagementPeriod; } function setFeeDistributor( @@ -146,6 +152,30 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { FEE_DISTRIBUTOR = fdAddress; } + function setBlockedBondPeriods( + uint256 retention, + uint256 management + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + _validateBlockedBondPeriods(retention, management); + BLOCKED_BOND_RETENTION_PERIOD = retention; + BLOCKED_BOND_RETENTION_PERIOD = management; + } + + function _validateBlockedBondPeriods( + uint256 retention, + uint256 management + ) internal pure { + if ( + retention == 0 || + retention == type(uint256).max || + management == 0 || + management == type(uint256).max || + retention < management + ) { + revert InvalidBlockedBondRetentionPeriod(); + } + } + /// @notice Returns the bond shares for the given node operator. /// @param nodeOperatorId id of the node operator to get bond for. /// @return bond shares. @@ -162,7 +192,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { bytes32[] memory rewardsProof, uint256 nodeOperatorId, uint256 cumulativeFeeShares - ) public view onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { + ) public view returns (uint256) { (uint256 current, uint256 required) = _bondSharesSummary( _getNodeOperatorActiveKeys(nodeOperatorId) ); @@ -631,27 +661,28 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { /// @notice Reports EL rewards stealing for the given node operator. /// @param nodeOperatorId id of the node operator to report EL rewards stealing for. - /// @param proposedBlockNumber block number of the proposed block with EL rewards stealing. - /// @param stolenAmount amount of stolen EL rewards. + /// @param blockNumber consensus layer block number of the proposed block with EL rewards stealing. + /// @param amount amount of stolen EL rewards. function initELRewardsStealingPenalty( uint256 nodeOperatorId, - uint256 proposedBlockNumber, - uint256 stolenAmount + uint256 blockNumber, + uint256 amount ) external - onlyRole(EL_REWARDS_STEALING_PENALTY_ROLE) + onlyRole(EL_REWARDS_STEALING_PENALTY_INIT_ROLE) onlyExistingNodeOperator(nodeOperatorId) { - require(stolenAmount > 0, "stolen amount should be greater than zero"); + if (amount == 0) { + revert InvalidStolenAmount(); + } emit ELRewardsStealingPenaltyInitiated( nodeOperatorId, - proposedBlockNumber, - stolenAmount + blockNumber, + amount ); _changeBlockedBondState({ nodeOperatorId: nodeOperatorId, - ETHAmount: _blockedBondEther[nodeOperatorId].ETHAmount + - stolenAmount, + ETHAmount: _blockedBondEther[nodeOperatorId].ETHAmount + amount, retentionUntil: block.timestamp + BLOCKED_BOND_RETENTION_PERIOD }); } @@ -664,7 +695,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 amount ) external - onlyRole(EL_REWARDS_STEALING_PENALTY_ROLE) + onlyRole(EL_REWARDS_STEALING_PENALTY_INIT_ROLE) onlyExistingNodeOperator(nodeOperatorId) { emit BlockedBondReleased(nodeOperatorId, amount); @@ -704,32 +735,36 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { /// @param nodeOperatorIds ids of the node operators to settle blocked bond for. function settleBlockedBondETH( uint256[] memory nodeOperatorIds - ) external onlyRole(EASY_TRACK_MOTION_AGENT_ROLE) { + ) external onlyRole(EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE) { uint256 nosCount = CSM.getNodeOperatorsCount(); for (uint256 i; i < nodeOperatorIds.length; ++i) { uint256 nodeOperatorId = nodeOperatorIds[i]; if (nodeOperatorId >= nosCount) { continue; } - BlockedBondEther storage data = _blockedBondEther[nodeOperatorId]; + BlockedBondEther storage blockedBond = _blockedBondEther[ + nodeOperatorId + ]; if ( block.timestamp + BLOCKED_BOND_RETENTION_PERIOD - - data.retentionUntil < - 1 days + blockedBond.retentionUntil < + BLOCKED_BOND_MANAGEMENT_PERIOD ) { // blocked bond in safe frame to manage it by committee or node operator continue; } - uint256 blockedAmount = getBlockedBondETH(nodeOperatorId); uint256 uncovered; - if (blockedAmount > 0) { - uncovered = penalize(nodeOperatorId, blockedAmount); + if ( + blockedBond.ETHAmount > 0 && + blockedBond.retentionUntil >= block.timestamp + ) { + uncovered = _penalize(nodeOperatorId, blockedBond.ETHAmount); } _changeBlockedBondState({ nodeOperatorId: nodeOperatorId, ETHAmount: uncovered, - retentionUntil: data.retentionUntil + retentionUntil: blockedBond.retentionUntil }); } } @@ -752,18 +787,21 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { } /// @notice Penalize bond by burning shares of the given node operator. - /// @param nodeOperatorId id of the node operator to penalize bond for. - /// @param ETHAmount amount of ETH to penalize. - /// @return uncovered amount. function penalize( uint256 nodeOperatorId, uint256 ETHAmount ) public - onlyRole(PENALIZE_BOND_ROLE) + onlyRole(INSTANT_PENALIZE_BOND_ROLE) onlyExistingNodeOperator(nodeOperatorId) - returns (uint256) { + _penalize(nodeOperatorId, ETHAmount); + } + + function _penalize( + uint256 nodeOperatorId, + uint256 ETHAmount + ) internal returns (uint256) { uint256 penaltyShares = _sharesByEth(ETHAmount); uint256 currentShares = getBondShares(nodeOperatorId); uint256 sharesToBurn = penaltyShares < currentShares diff --git a/test/CSAccounting.blockedBond.t.sol b/test/CSAccounting.blockedBond.t.sol index 8ed62536..d9273c3e 100644 --- a/test/CSAccounting.blockedBond.t.sol +++ b/test/CSAccounting.blockedBond.t.sol @@ -24,7 +24,8 @@ contract CSAccounting_revealed is CSAccounting { address lidoLocator, address wstETH, address communityStakingModule, - uint256 blockedBondRetentionPeriod + uint256 blockedBondRetentionPeriod, + uint256 blockedBondManagementPeriod ) CSAccounting( commonBondSize, @@ -32,7 +33,8 @@ contract CSAccounting_revealed is CSAccounting { lidoLocator, wstETH, communityStakingModule, - blockedBondRetentionPeriod + blockedBondRetentionPeriod, + blockedBondManagementPeriod ) {} @@ -104,7 +106,8 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { address(locator), address(wstETH), address(stakingModule), - 8 weeks + 8 weeks, + 1 days ); feeDistributor = new CommunityStakingFeeDistributorMock( address(locator), @@ -112,12 +115,15 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { ); vm.startPrank(admin); accounting.setFeeDistributor(address(feeDistributor)); - accounting.grantRole(accounting.PENALIZE_BOND_ROLE(), admin); + accounting.grantRole(accounting.INSTANT_PENALIZE_BOND_ROLE(), admin); accounting.grantRole( - accounting.EL_REWARDS_STEALING_PENALTY_ROLE(), + accounting.EL_REWARDS_STEALING_PENALTY_INIT_ROLE(), + admin + ); + accounting.grantRole( + accounting.EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE(), admin ); - accounting.grantRole(accounting.EASY_TRACK_MOTION_AGENT_ROLE(), admin); vm.stopPrank(); } @@ -361,8 +367,8 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { vm.prank(admin); accounting.initELRewardsStealingPenalty({ nodeOperatorId: noId, - proposedBlockNumber: proposedBlockNumber, - stolenAmount: firstStolenAmount + blockNumber: proposedBlockNumber, + amount: firstStolenAmount }); assertEq( @@ -396,8 +402,8 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { vm.prank(admin); accounting.initELRewardsStealingPenalty({ nodeOperatorId: noId, - proposedBlockNumber: proposedBlockNumber, - stolenAmount: secondStolenAmount + blockNumber: proposedBlockNumber, + amount: secondStolenAmount }); assertEq( @@ -418,21 +424,21 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { vm.prank(admin); accounting.initELRewardsStealingPenalty({ nodeOperatorId: 0, - proposedBlockNumber: 100500, - stolenAmount: 100 ether + blockNumber: 100500, + amount: 100 ether }); } function test_initELRewardsStealingPenalty_revertWhenZero() public { _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); - vm.expectRevert("stolen amount should be greater than zero"); + vm.expectRevert(0x7edd4cfd); vm.prank(admin); accounting.initELRewardsStealingPenalty({ nodeOperatorId: 0, - proposedBlockNumber: 100500, - stolenAmount: 0 + blockNumber: 100500, + amount: 0 }); } @@ -440,13 +446,13 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xd5726f791c124091830fa80135a85d3ea48ec1d26b2c06296f2175f326b23db3" + "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f" ); accounting.initELRewardsStealingPenalty({ nodeOperatorId: 0, - proposedBlockNumber: 100500, - stolenAmount: 100 ether + blockNumber: 100500, + amount: 100 ether }); } @@ -662,7 +668,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xd5726f791c124091830fa80135a85d3ea48ec1d26b2c06296f2175f326b23db3" + "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f" ); accounting.releaseBlockedBondETH(0, 1 ether); } diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index ab232ba1..07f3bcde 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -54,7 +54,8 @@ contract CSAccountingTest is address(locator), address(wstETH), address(stakingModule), - 8 weeks + 8 weeks, + 1 days ); feeDistributor = new CommunityStakingFeeDistributorMock( address(locator), @@ -62,12 +63,15 @@ contract CSAccountingTest is ); vm.startPrank(admin); accounting.setFeeDistributor(address(feeDistributor)); - accounting.grantRole(accounting.PENALIZE_BOND_ROLE(), admin); + accounting.grantRole(accounting.INSTANT_PENALIZE_BOND_ROLE(), admin); accounting.grantRole( - accounting.EL_REWARDS_STEALING_PENALTY_ROLE(), + accounting.EL_REWARDS_STEALING_PENALTY_INIT_ROLE(), + admin + ); + accounting.grantRole( + accounting.EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE(), admin ); - accounting.grantRole(accounting.EASY_TRACK_MOTION_AGENT_ROLE(), admin); vm.stopPrank(); } @@ -1186,7 +1190,7 @@ contract CSAccountingTest is function test_penalize_RevertWhenCallerHasNoRole() public { vm.expectRevert( - "AccessControl: account 0x0000000000000000000000000000000000000309 is missing role 0xf3c54f9b8dbd8c6d8596d09d52b61d4bdce01620000dd9d49c5017dca6e62158" + "AccessControl: account 0x0000000000000000000000000000000000000309 is missing role 0x9909cf24c2d3bafa8c229558d86a1b726ba57c3ef6350848dcf434a4181b56c7" ); vm.prank(stranger); accounting.penalize(0, 20); diff --git a/test/CSMAddValidator.t.sol b/test/CSMAddValidator.t.sol index 034c24ca..b8f504e0 100644 --- a/test/CSMAddValidator.t.sol +++ b/test/CSMAddValidator.t.sol @@ -49,7 +49,8 @@ contract CSMCommon is Test, Fixtures, Utilities, CSModuleBase { address(locator), address(wstETH), address(csm), - 8 weeks + 8 weeks, + 1 days ); csm.setAccounting(address(accounting)); } diff --git a/test/CSMInit.t.sol b/test/CSMInit.t.sol index bb46ec9f..db7ec6d6 100644 --- a/test/CSMInit.t.sol +++ b/test/CSMInit.t.sol @@ -42,7 +42,8 @@ contract CSMInitTest is Test, Fixtures { address(locator), address(wstETH), address(csm), - 8 weeks + 8 weeks, + 1 days ); } diff --git a/test/integration/DepositInTokens.t.sol b/test/integration/DepositInTokens.t.sol index 529870e0..18ddeb8e 100644 --- a/test/integration/DepositInTokens.t.sol +++ b/test/integration/DepositInTokens.t.sol @@ -62,7 +62,8 @@ contract DepositIntegrationTest is Test, PermitHelper { address(locator), address(wstETH), address(csm), - 8 weeks + 8 weeks, + 1 days ); csm.setNodeOperator({ diff --git a/test/integration/StakingRouter.t.sol b/test/integration/StakingRouter.t.sol index 700d58d2..3b0497e9 100644 --- a/test/integration/StakingRouter.t.sol +++ b/test/integration/StakingRouter.t.sol @@ -55,7 +55,8 @@ contract StakingRouterIntegrationTest is Test, Utilities { address(locator), address(wstETH), address(csm), - 8 weeks + 8 weeks, + 1 days ); csm.setAccounting(address(accounting)); From 8d65197ac280dd69f5efd68162dda67f5e85a7f8 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 7 Nov 2023 15:45:21 +0400 Subject: [PATCH 7/8] Update blocked bond retention and management periods --- script/Deploy.s.sol | 4 ++-- src/CSAccounting.sol | 44 +++++++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 78dc5d78..f8d4c20b 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -44,8 +44,8 @@ contract Deploy is Script { communityStakingModule: address(csm), wstETH: address(wstETH), // todo: arguable. should be discussed - blockedBondRetentionPeriod: 8 weeks, - blockedBondManagementPeriod: 1 weeks + _blockedBondRetentionPeriod: 8 weeks, + _blockedBondManagementPeriod: 1 weeks }); CSFeeOracle feeOracle = new CSFeeOracle({ secondsPerBlock: 12, diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index a4be4926..ef715a81 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -88,6 +88,12 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { bytes32 public constant EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE = keccak256("EL_REWARDS_STEALING_PENALTY_SETTLE_ROLE"); + // 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 immutable COMMON_BOND_SIZE; ILidoLocator private immutable LIDO_LOCATOR; @@ -97,8 +103,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { address public FEE_DISTRIBUTOR; uint256 public totalBondShares; - uint256 public BLOCKED_BOND_RETENTION_PERIOD; - uint256 public BLOCKED_BOND_MANAGEMENT_PERIOD; + uint256 public blockedBondRetentionPeriod; + uint256 public blockedBondManagementPeriod; mapping(uint256 => uint256) internal _bondShares; mapping(uint256 => BlockedBondEther) internal _blockedBondEther; @@ -112,15 +118,16 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { /// @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 _blockedBondRetentionPeriod retention period for blocked bond in seconds + /// @param _blockedBondManagementPeriod management period for blocked bond in seconds constructor( uint256 commonBondSize, address admin, address lidoLocator, address wstETH, address communityStakingModule, - uint256 blockedBondRetentionPeriod, - uint256 blockedBondManagementPeriod + uint256 _blockedBondRetentionPeriod, + uint256 _blockedBondManagementPeriod ) { // check zero addresses require(admin != address(0), "admin is zero address"); @@ -131,8 +138,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { ); require(wstETH != address(0), "wstETH is zero address"); _validateBlockedBondPeriods( - blockedBondRetentionPeriod, - blockedBondManagementPeriod + _blockedBondRetentionPeriod, + _blockedBondManagementPeriod ); _setupRole(DEFAULT_ADMIN_ROLE, admin); @@ -142,8 +149,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { COMMON_BOND_SIZE = commonBondSize; - BLOCKED_BOND_RETENTION_PERIOD = blockedBondRetentionPeriod; - BLOCKED_BOND_MANAGEMENT_PERIOD = blockedBondManagementPeriod; + blockedBondRetentionPeriod = _blockedBondRetentionPeriod; + blockedBondManagementPeriod = _blockedBondManagementPeriod; } function setFeeDistributor( @@ -157,8 +164,8 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 management ) external onlyRole(DEFAULT_ADMIN_ROLE) { _validateBlockedBondPeriods(retention, management); - BLOCKED_BOND_RETENTION_PERIOD = retention; - BLOCKED_BOND_RETENTION_PERIOD = management; + blockedBondRetentionPeriod = retention; + blockedBondManagementPeriod = management; } function _validateBlockedBondPeriods( @@ -166,11 +173,10 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 management ) internal pure { if ( - retention == 0 || - retention == type(uint256).max || - management == 0 || - management == type(uint256).max || - retention < management + 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(); } @@ -683,7 +689,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { _changeBlockedBondState({ nodeOperatorId: nodeOperatorId, ETHAmount: _blockedBondEther[nodeOperatorId].ETHAmount + amount, - retentionUntil: block.timestamp + BLOCKED_BOND_RETENTION_PERIOD + retentionUntil: block.timestamp + blockedBondRetentionPeriod }); } @@ -747,9 +753,9 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { ]; if ( block.timestamp + - BLOCKED_BOND_RETENTION_PERIOD - + blockedBondRetentionPeriod - blockedBond.retentionUntil < - BLOCKED_BOND_MANAGEMENT_PERIOD + blockedBondManagementPeriod ) { // blocked bond in safe frame to manage it by committee or node operator continue; From 10264f2c02325c4117bd32792c8a25fdb7226f97 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 7 Nov 2023 17:54:42 +0400 Subject: [PATCH 8/8] fix: some remarks --- src/CSAccounting.sol | 21 +++++---------- test/CSAccounting.blockedBond.t.sol | 40 ++++++++++++++--------------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index ef715a81..20c013a7 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -76,7 +76,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { bytes32 r; bytes32 s; } - struct BlockedBondEther { + struct BlockedBond { uint256 ETHAmount; uint256 retentionUntil; } @@ -107,7 +107,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 public blockedBondManagementPeriod; mapping(uint256 => uint256) internal _bondShares; - mapping(uint256 => BlockedBondEther) internal _blockedBondEther; + mapping(uint256 => BlockedBond) internal _blockedBondEther; error NotOwnerToClaim(address msgSender, address owner); error InvalidBlockedBondRetentionPeriod(); @@ -745,12 +745,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { uint256 nosCount = CSM.getNodeOperatorsCount(); for (uint256 i; i < nodeOperatorIds.length; ++i) { uint256 nodeOperatorId = nodeOperatorIds[i]; - if (nodeOperatorId >= nosCount) { - continue; - } - BlockedBondEther storage blockedBond = _blockedBondEther[ - nodeOperatorId - ]; + BlockedBond storage blockedBond = _blockedBondEther[nodeOperatorId]; if ( block.timestamp + blockedBondRetentionPeriod - @@ -785,7 +780,7 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { emit BlockedBondChanged(nodeOperatorId, 0, 0); return; } - _blockedBondEther[nodeOperatorId] = BlockedBondEther({ + _blockedBondEther[nodeOperatorId] = BlockedBond({ ETHAmount: ETHAmount, retentionUntil: retentionUntil }); @@ -796,18 +791,14 @@ contract CSAccounting is CSAccountingBase, AccessControlEnumerable { function penalize( uint256 nodeOperatorId, uint256 ETHAmount - ) - public - onlyRole(INSTANT_PENALIZE_BOND_ROLE) - onlyExistingNodeOperator(nodeOperatorId) - { + ) public onlyRole(INSTANT_PENALIZE_BOND_ROLE) { _penalize(nodeOperatorId, ETHAmount); } function _penalize( uint256 nodeOperatorId, uint256 ETHAmount - ) internal returns (uint256) { + ) internal onlyExistingNodeOperator(nodeOperatorId) returns (uint256) { uint256 penaltyShares = _sharesByEth(ETHAmount); uint256 currentShares = getBondShares(nodeOperatorId); uint256 sharesToBurn = penaltyShares < currentShares diff --git a/test/CSAccounting.blockedBond.t.sol b/test/CSAccounting.blockedBond.t.sol index d9273c3e..df109088 100644 --- a/test/CSAccounting.blockedBond.t.sol +++ b/test/CSAccounting.blockedBond.t.sol @@ -47,13 +47,13 @@ contract CSAccounting_revealed is CSAccounting { function _blockedBondEther_get_value( uint256 nodeOperatorId - ) public view returns (BlockedBondEther memory) { + ) public view returns (BlockedBond memory) { return _blockedBondEther[nodeOperatorId]; } function _blockedBondEther_set_value( uint256 nodeOperatorId, - BlockedBondEther memory value + BlockedBond memory value ) public { _blockedBondEther[nodeOperatorId] = value; } @@ -134,7 +134,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -158,7 +158,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -186,7 +186,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -214,7 +214,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -228,7 +228,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: 1 ether, retentionUntil: retentionUntil }) @@ -256,7 +256,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -270,7 +270,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: 1 ether, retentionUntil: retentionUntil }) @@ -297,7 +297,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._bondShares_set_value(0, 100 ether); accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -323,7 +323,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { retentionUntil: retentionUntil }); - CSAccounting.BlockedBondEther memory value = accounting + CSAccounting.BlockedBond memory value = accounting ._blockedBondEther_get_value(noId); assertEq(value.ETHAmount, amount); @@ -474,7 +474,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( 0, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: 1 ether, retentionUntil: retentionUntil }) @@ -486,7 +486,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { vm.prank(admin); accounting.settleBlockedBondETH(nosToPenalize); - CSAccounting.BlockedBondEther memory value = accounting + CSAccounting.BlockedBond memory value = accounting ._blockedBondEther_get_value(0); assertEq(value.ETHAmount, 1 ether); @@ -516,7 +516,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { // penalty amount is greater than the bond accounting._blockedBondEther_set_value( 0, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: 100 ether, retentionUntil: retentionUntil }) @@ -544,7 +544,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { // retention period expired accounting._blockedBondEther_set_value( 0, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: 100 ether, retentionUntil: retentionUntil }) @@ -569,7 +569,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -584,7 +584,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._reduceBlockedBondETH_revealed(noId, toReduce); - CSAccounting.BlockedBondEther memory value = accounting + CSAccounting.BlockedBond memory value = accounting ._blockedBondEther_get_value(noId); assertEq(value.ETHAmount, rest); @@ -620,7 +620,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -639,7 +639,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil }) @@ -682,7 +682,7 @@ contract CSAccountingTest is Test, Fixtures, CSAccountingBase { accounting._blockedBondEther_set_value( noId, - CSAccounting.BlockedBondEther({ + CSAccounting.BlockedBond({ ETHAmount: amount, retentionUntil: retentionUntil })