diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index ca13957c..d2ecfb11 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -176,9 +176,9 @@ contract CSAccounting is } function setBondCurve( - uint256[] memory bondCurve_ + uint256[] memory bondCurve ) external onlyRole(SET_BOND_CURVE_ROLE) { - _setBondCurve(bondCurve_); + _setBondCurve(bondCurve); } /// @notice Sets basis points of the bond multiplier for the given node operator. diff --git a/src/CSBondCurve.sol b/src/CSBondCurve.sol index 394b563a..a6b08e5d 100644 --- a/src/CSBondCurve.sol +++ b/src/CSBondCurve.sol @@ -8,11 +8,12 @@ abstract contract CSBondCurve { error InvalidMultiplier(); // @dev Keys count to bond amount mapping - // x - keys count - // y - bond amount for x keys + // i | array index + // i + 1 | keys count for particular bond amount + // bondCurve[i] | bond amount for `i + 1` keys count uint256[] public bondCurve; - uint256 internal constant MIN_CURVE_LENGTH = 2; + uint256 internal constant MIN_CURVE_LENGTH = 1; // todo: might be redefined in the future uint256 internal constant MAX_CURVE_LENGTH = 20; uint256 internal constant BASIS_POINTS = 10000; @@ -23,14 +24,18 @@ abstract contract CSBondCurve { /// By default, all Node Operators have x1 multiplier (10000 basis points). mapping(uint256 => uint256) internal _bondMultiplierBP; + uint256 internal _bondCurveTrend; + constructor(uint256[] memory _bondCurve) { - _checkCurveLength(_bondCurve); - bondCurve = _bondCurve; + _setBondCurve(_bondCurve); } function _setBondCurve(uint256[] memory _bondCurve) internal { _checkCurveLength(_bondCurve); bondCurve = _bondCurve; + _bondCurveTrend = + _bondCurve[_bondCurve.length - 1] - + (_bondCurve.length > 1 ? _bondCurve[_bondCurve.length - 2] : 0); } function _setBondMultiplier( @@ -74,14 +79,13 @@ abstract contract CSBondCurve { uint256 amount ) internal view returns (uint256) { uint256 mult = getBondMultiplier(nodeOperatorId); + uint256 first = (bondCurve[0] * mult) / BASIS_POINTS; + if (amount < first) return 0; uint256 last = (bondCurve[bondCurve.length - 1] * mult) / BASIS_POINTS; if (amount >= last) { return bondCurve.length + - ((amount - last) / - (last - - (bondCurve[bondCurve.length - 2] * mult) / - BASIS_POINTS)); + ((amount - last) / ((_bondCurveTrend * mult) / BASIS_POINTS)); } return _searchKeysByBond(amount, mult); } @@ -94,14 +98,18 @@ abstract contract CSBondCurve { uint256 high = bondCurve.length - 1; while (low <= high) { uint256 mid = (low + high) / 2; - if ( - (bondCurve[mid] * multiplier) / BASIS_POINTS > value && mid != 0 - ) { + uint256 midValue = (bondCurve[mid] * multiplier) / BASIS_POINTS; + if (value == midValue) { + return mid + 1; + } + if (value < midValue) { + // zero mid is avoided above high = mid - 1; - } else if ((bondCurve[mid] * multiplier) / BASIS_POINTS <= value) { + continue; + } + if (value > midValue) { low = mid + 1; - } else { - return mid; + continue; } } return low; @@ -117,18 +125,15 @@ abstract contract CSBondCurve { uint256 nodeOperatorId, uint256 keys ) internal view returns (uint256) { - uint256 mult = getBondMultiplier(nodeOperatorId); if (keys == 0) return 0; + uint256 mult = getBondMultiplier(nodeOperatorId); if (keys <= bondCurve.length) { return (bondCurve[keys - 1] * mult) / BASIS_POINTS; } else { - uint256 last = (bondCurve[bondCurve.length - 1] * mult) / - BASIS_POINTS; return - last + + ((bondCurve[bondCurve.length - 1] * mult) / BASIS_POINTS) + (keys - bondCurve.length) * - (last - - ((bondCurve[bondCurve.length - 2] * mult) / BASIS_POINTS)); + ((_bondCurveTrend * mult) / BASIS_POINTS); } } } diff --git a/test/CSAccounting.blockedBond.t.sol b/test/CSAccounting.blockedBond.t.sol index 7cf569cd..47249e7e 100644 --- a/test/CSAccounting.blockedBond.t.sol +++ b/test/CSAccounting.blockedBond.t.sol @@ -15,6 +15,7 @@ import { CommunityStakingModuleMock } from "./helpers/mocks/CommunityStakingModu import { CommunityStakingFeeDistributorMock } from "./helpers/mocks/CommunityStakingFeeDistributorMock.sol"; import { WithdrawalQueueMockBase, WithdrawalQueueMock } from "./helpers/mocks/WithdrawalQueueMock.sol"; +import { Utilities } from "./helpers/Utilities.sol"; import { Fixtures } from "./helpers/Fixtures.sol"; contract CSAccounting_revealed is CSAccounting { @@ -74,7 +75,12 @@ contract CSAccounting_revealed is CSAccounting { } } -contract CSAccounting_BlockedBondTest is Test, Fixtures, CSAccountingBase { +contract CSAccounting_BlockedBondTest is + Test, + Fixtures, + Utilities, + CSAccountingBase +{ using stdStorage for StdStorage; LidoLocatorMock internal locator; @@ -449,9 +455,15 @@ contract CSAccounting_BlockedBondTest is Test, Fixtures, CSAccountingBase { _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f" + bytes( + Utilities.accessErrorString( + address(stranger), + accounting.EL_REWARDS_STEALING_PENALTY_INIT_ROLE() + ) + ) ); + vm.prank(stranger); accounting.initELRewardsStealingPenalty({ nodeOperatorId: 0, blockNumber: 100500, @@ -671,8 +683,15 @@ contract CSAccounting_BlockedBondTest is Test, Fixtures, CSAccountingBase { _createNodeOperator({ ongoingVals: 1, withdrawnVals: 0 }); vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0xcc2e7ce7be452f766dd24d55d87a3d42901c31ffa5b600cd1dff475abec91c1f" + bytes( + Utilities.accessErrorString( + address(stranger), + accounting.EL_REWARDS_STEALING_PENALTY_INIT_ROLE() + ) + ) ); + + vm.prank(stranger); accounting.releaseBlockedBondETH(0, 1 ether); } diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index 2f1196b8..021fef41 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -16,6 +16,7 @@ import { CommunityStakingModuleMock } from "./helpers/mocks/CommunityStakingModu import { CommunityStakingFeeDistributorMock } from "./helpers/mocks/CommunityStakingFeeDistributorMock.sol"; import { WithdrawalQueueMockBase, WithdrawalQueueMock } from "./helpers/mocks/WithdrawalQueueMock.sol"; +import { Utilities } from "./helpers/Utilities.sol"; import { Fixtures } from "./helpers/Fixtures.sol"; contract CSAccounting_revealed is CSAccounting { @@ -39,7 +40,7 @@ contract CSAccounting_revealed is CSAccounting { ) {} - function setBondCurve() public { + function setBondCurveForTests() public { uint256[] memory _bondCurve = new uint256[](11); _bondCurve[0] = 2 ether; _bondCurve[1] = 3.90 ether; // 1.9 @@ -52,13 +53,14 @@ contract CSAccounting_revealed is CSAccounting { _bondCurve[8] = 14.30 ether; // 1.2 _bondCurve[9] = 15.40 ether; // 1.1 _bondCurve[10] = 16.40 ether; // 1.0 - bondCurve = _bondCurve; + _setBondCurve(_bondCurve); } } contract CSAccountingTest is Test, Fixtures, + Utilities, PermitTokenBase, CSAccountingBase, WithdrawalQueueMockBase @@ -137,9 +139,15 @@ contract CSAccountingTest is _bondCurve[1] = 4 ether; vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0x645c9e6d2a86805cb5a28b1e4751c0dab493df7cf935070ce405489ba1a7bf72" + bytes( + Utilities.accessErrorString( + stranger, + accounting.SET_BOND_CURVE_ROLE() + ) + ) ); + vm.prank(stranger); accounting.setBondCurve(_bondCurve); } @@ -152,9 +160,15 @@ contract CSAccountingTest is function test_setBondMultiplier_RevertWhen_DoesNotHaveRole() public { vm.expectRevert( - "AccessControl: account 0x7fa9385be102ac3eac297483dd6233d62b3e1496 is missing role 0x62131145aee19b18b85aa8ead52ba87f0efb6e61e249155edc68a2c24e8f79b5" + bytes( + Utilities.accessErrorString( + stranger, + accounting.SET_BOND_MULTIPLIER_ROLE() + ) + ) ); + vm.prank(stranger); accounting.setBondMultiplier(0, 9500); // 0.95 } @@ -717,7 +731,7 @@ contract CSAccountingTest is assertEq(totalRewards, ETHAsFee); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); totalRewards = accounting.getTotalRewardsETH( new bytes32[](1), @@ -763,7 +777,7 @@ contract CSAccountingTest is assertEq(totalRewards, stETHAsFee); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); totalRewards = accounting.getTotalRewardsStETH( new bytes32[](1), 0, @@ -810,7 +824,7 @@ contract CSAccountingTest is assertApproxEqAbs(totalRewards, wstETHAsFee, 1); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); totalRewards = accounting.getTotalRewardsWstETH( new bytes32[](1), 0, @@ -850,7 +864,7 @@ contract CSAccountingTest is assertApproxEqAbs(accounting.getExcessBondETH(0), 32 ether, 1); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertApproxEqAbs(accounting.getExcessBondETH(0), 42.6 ether, 1); @@ -874,7 +888,7 @@ contract CSAccountingTest is assertApproxEqAbs(accounting.getExcessBondStETH(0), 32 ether, 1); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertApproxEqAbs(accounting.getExcessBondStETH(0), 42.6 ether, 1); @@ -902,7 +916,7 @@ contract CSAccountingTest is ); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertApproxEqAbs( accounting.getExcessBondWstETH(0), @@ -930,7 +944,7 @@ contract CSAccountingTest is assertApproxEqAbs(accounting.getMissingBondETH(0), 16 ether, 1); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertApproxEqAbs(accounting.getMissingBondETH(0), 5.4 ether, 1); @@ -954,7 +968,7 @@ contract CSAccountingTest is assertApproxEqAbs(accounting.getMissingBondStETH(0), 16 ether, 1); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertApproxEqAbs(accounting.getMissingBondStETH(0), 5.4 ether, 1); @@ -982,7 +996,7 @@ contract CSAccountingTest is ); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertApproxEqAbs( accounting.getMissingBondWstETH(0), @@ -1015,7 +1029,7 @@ contract CSAccountingTest is assertEq(accounting.getUnbondedKeysCount(0), 9); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertEq(accounting.getUnbondedKeysCount(0), 7); @@ -1034,7 +1048,7 @@ contract CSAccountingTest is assertEq(accounting.getKeysCountByBondETH(16 ether), 8); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertEq(accounting.getKeysCountByBondETH(16 ether), 10); } @@ -1047,7 +1061,7 @@ contract CSAccountingTest is assertEq(accounting.getKeysCountByBondETH(16 ether), 8); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertEq(accounting.getKeysCountByBondStETH(16 ether), 10); } @@ -1080,7 +1094,7 @@ contract CSAccountingTest is ); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); assertEq( accounting.getKeysCountByBondWstETH( @@ -1135,7 +1149,7 @@ contract CSAccountingTest is ); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); vm.deal(address(feeDistributor), 0.1 ether); vm.prank(address(feeDistributor)); @@ -1400,7 +1414,7 @@ contract CSAccountingTest is ); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); vm.deal(address(feeDistributor), 0.1 ether); vm.prank(address(feeDistributor)); @@ -1579,7 +1593,7 @@ contract CSAccountingTest is assertEq(stETH.sharesOf(address(user)), 0, "user shares should be 0"); // set sophisticated curve - accounting.setBondCurve(); + accounting.setBondCurveForTests(); vm.deal(address(feeDistributor), 0.1 ether); vm.prank(address(feeDistributor)); @@ -1798,8 +1812,14 @@ contract CSAccountingTest is function test_penalize_RevertWhenCallerHasNoRole() public { vm.expectRevert( - "AccessControl: account 0x0000000000000000000000000000000000000309 is missing role 0x9909cf24c2d3bafa8c229558d86a1b726ba57c3ef6350848dcf434a4181b56c7" + bytes( + Utilities.accessErrorString( + stranger, + accounting.INSTANT_PENALIZE_BOND_ROLE() + ) + ) ); + vm.prank(stranger); accounting.penalize(0, 20); } diff --git a/test/CSBondCurve.t.sol b/test/CSBondCurve.t.sol index af4a5ce0..3b10e218 100644 --- a/test/CSBondCurve.t.sol +++ b/test/CSBondCurve.t.sol @@ -68,8 +68,7 @@ contract CSBondCurveTest is Test { } function test_setBondCurve_RevertWhen_LessThanMinBondCurveLength() public { - uint256[] memory _bondCurve = new uint256[](1); - _bondCurve[0] = 2 ether; + uint256[] memory _bondCurve = new uint256[](0); vm.expectRevert(CSBondCurve.InvalidBondCurveLength.selector); diff --git a/test/helpers/Utilities.sol b/test/helpers/Utilities.sol index 73695604..1babf78c 100644 --- a/test/helpers/Utilities.sol +++ b/test/helpers/Utilities.sol @@ -55,4 +55,15 @@ contract Utilities is CommonBase { revert("wrong chain id"); } } + + function accessErrorString( + address account, + bytes32 role + ) internal pure returns (string memory) { + string memory errorString = "AccessControl: account "; + errorString = string.concat(errorString, vm.toString(account)); + errorString = string.concat(errorString, " is missing role "); + errorString = string.concat(errorString, vm.toString(role)); + return errorString; + } }