From 28cc3351e0fc8174eab6c4cf768ca5d364c75764 Mon Sep 17 00:00:00 2001 From: Dmitry Gusakov Date: Mon, 13 Jan 2025 09:36:01 +0100 Subject: [PATCH] feat: Add public view method getClaimableBondShares (#378) Co-authored-by: Sergey Khomutinin <31664571+skhomuti@users.noreply.github.com> --- src/CSAccounting.sol | 7 ++ src/interfaces/ICSAccounting.sol | 7 ++ test/CSAccounting.t.sol | 168 ++++++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index 66f33b28..dcc18fd0 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -466,6 +466,13 @@ contract CSAccounting is ); } + /// @inheritdoc ICSAccounting + function getClaimableBondShares( + uint256 nodeOperatorId + ) public view returns (uint256) { + return _getClaimableBondShares(nodeOperatorId); + } + function _pullFeeRewards( uint256 nodeOperatorId, uint256 cumulativeFeeShares, diff --git a/src/interfaces/ICSAccounting.sol b/src/interfaces/ICSAccounting.sol index e25ba5db..a55983ad 100644 --- a/src/interfaces/ICSAccounting.sol +++ b/src/interfaces/ICSAccounting.sol @@ -160,6 +160,13 @@ interface ICSAccounting is uint256 nodeOperatorId ) external view returns (uint256 current, uint256 required); + /// @notice Get current claimable bond in stETH shares for the given Node Operator + /// @param nodeOperatorId ID of the Node Operator + /// @return Current claimable bond in stETH shares + function getClaimableBondShares( + uint256 nodeOperatorId + ) external view returns (uint256); + /// @notice Unwrap the user's wstETH and deposit stETH to the bond for the given Node Operator /// @dev Called by CSM exclusively /// @param from Address to unwrap wstETH from diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index 6919b24a..4f43507c 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -2459,7 +2459,7 @@ contract CSAccountingClaimWstETHRewardsTest is } } -contract CSAccountingclaimRewardsUnstETHTest is +contract CSAccountingClaimRewardsUnstETHTest is CSAccountingClaimRewardsBaseTest { function test_default() public override assertInvariants { @@ -2870,6 +2870,172 @@ contract CSAccountingclaimRewardsUnstETHTest is } } +contract CSAccountingClaimableBondTest is CSAccountingRewardsBaseTest { + function test_default() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertEq( + claimableBondShares, + 0, + "claimable bond shares should be zero" + ); + } + + function test_WithCurve() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + _rewards({ fee: 0.1 ether }); + _curve(defaultCurve); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertApproxEqAbs( + claimableBondShares, + stETH.getSharesByPooledEth(15 ether), + 1 wei, + "claimable bond shares should be equal to the curve discount" + ); + } + + function test_WithLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _rewards({ fee: 0.1 ether }); + _lock({ id: 0, amount: 1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertEq( + claimableBondShares, + 0, + "claimable bond shares should be zero" + ); + } + + function test_WithCurveAndLocked() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + _rewards({ fee: 0.1 ether }); + _curve(defaultCurve); + _lock({ id: 0, amount: 1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertApproxEqAbs( + claimableBondShares, + stETH.getSharesByPooledEth(14 ether), + 1 wei, + "claimable bond shares should be equal to the curve discount minus locked" + ); + } + + function test_WithOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertApproxEqAbs( + claimableBondShares, + stETH.getSharesByPooledEth(2 ether), + 1 wei, + "claimable bond shares should be equal to a single validator bond" + ); + } + + function test_WithBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 32 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertEq( + claimableBondShares, + 0, + "claimable bond shares should be zero" + ); + } + + function test_WithBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 32 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertApproxEqAbs( + claimableBondShares, + stETH.getSharesByPooledEth(2 ether), + 1 wei, + "claimable bond shares should be equal to a single validator bond" + ); + } + + function test_WithExcessBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 33 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertApproxEqAbs( + claimableBondShares, + stETH.getSharesByPooledEth(1 ether), + 1 wei, + "claimable bond shares should be equal to the excess bond" + ); + } + + function test_WithExcessBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 33 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertApproxEqAbs( + claimableBondShares, + stETH.getSharesByPooledEth(3 ether), + 1 wei, + "claimable bond shares should be equal to a single validator bond plus the excess bond" + ); + } + + function test_WithMissingBond() public override { + _operator({ ongoing: 16, withdrawn: 0 }); + _deposit({ bond: 16 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertEq( + claimableBondShares, + 0, + "claimable bond shares should be zero" + ); + } + + function test_WithMissingBondAndOneWithdrawnValidator() public override { + _operator({ ongoing: 16, withdrawn: 1 }); + _deposit({ bond: 16 ether }); + _rewards({ fee: 0.1 ether }); + + uint256 claimableBondShares = accounting.getClaimableBondShares(0); + + assertEq( + claimableBondShares, + 0, + "claimable bond shares should be zero" + ); + } +} + contract CSAccountingDepositEthTest is CSAccountingBaseTest { function setUp() public override { super.setUp();