diff --git a/.prettierignore b/.prettierignore index 473209b3..dc5229ae 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ lib/ !src/lib GAS.md artifacts/ +docs/ diff --git a/src/CSPerksRegistry.sol b/src/CSPerksRegistry.sol new file mode 100644 index 00000000..0d498fd4 --- /dev/null +++ b/src/CSPerksRegistry.sol @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.24; + +import { ICSPerksRegistry } from "./interfaces/ICSPerksRegistry.sol"; +import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +contract CSPerksRegistry is + ICSPerksRegistry, + Initializable, + AccessControlEnumerableUpgradeable +{ + uint256 internal constant MAX_BP = 10000; + + mapping(uint256 => uint256) internal _priorityQueueLimits; + + mapping(uint256 => uint256[]) internal _rewardSharePivotsData; + mapping(uint256 => uint256[]) internal _rewardShareValuesData; + + mapping(uint256 => uint256[]) internal _performanceLeewayPivotsData; + mapping(uint256 => uint256[]) internal _performanceLeewayValuesData; + + constructor() { + _disableInitializers(); + } + + /// @notice initialize contract + function initialize(address admin) external initializer { + if (admin == address(0)) revert ZeroAdminAddress(); + __AccessControlEnumerable_init(); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /// @inheritdoc ICSPerksRegistry + function setRewardShareData( + uint256 curveId, + uint256[] calldata keyPivots, + uint256[] calldata rewardShares + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (keyPivots.length + 1 != rewardShares.length) + revert InvalidRewardShareData(); + if (keyPivots.length > 0 && keyPivots[0] == 0) + revert InvalidRewardShareData(); + if (keyPivots.length > 1) { + for (uint256 i = 0; i < keyPivots.length - 1; ++i) { + if (keyPivots[i] >= keyPivots[i + 1]) + revert InvalidRewardShareData(); + } + } + for (uint256 i = 0; i < rewardShares.length; ++i) { + if (rewardShares[i] > MAX_BP) revert InvalidRewardShareData(); + } + _rewardSharePivotsData[curveId] = keyPivots; + _rewardShareValuesData[curveId] = rewardShares; + + emit RewardShareDataSet(curveId); + } + + /// @inheritdoc ICSPerksRegistry + function getRewardShareData( + uint256 curveId + ) + external + view + returns (uint256[] memory keyPivots, uint256[] memory rewardShares) + { + if (_rewardShareValuesData[curveId].length == 0) revert NoData(); + return ( + _rewardSharePivotsData[curveId], + _rewardShareValuesData[curveId] + ); + } + + /// @inheritdoc ICSPerksRegistry + function setPerformanceLeewayData( + uint256 curveId, + uint256[] calldata keyPivots, + uint256[] calldata performanceLeeways + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (keyPivots.length + 1 != performanceLeeways.length) + revert InvalidPerformanceLeewayData(); + if (keyPivots.length > 0 && keyPivots[0] == 0) + revert InvalidPerformanceLeewayData(); + if (keyPivots.length > 1) { + for (uint256 i = 0; i < keyPivots.length - 1; ++i) { + if (keyPivots[i] >= keyPivots[i + 1]) + revert InvalidPerformanceLeewayData(); + } + } + for (uint256 i = 0; i < performanceLeeways.length; ++i) { + if (performanceLeeways[i] > MAX_BP) + revert InvalidPerformanceLeewayData(); + } + _performanceLeewayPivotsData[curveId] = keyPivots; + _performanceLeewayValuesData[curveId] = performanceLeeways; + + emit PerformanceLeewayDataSet(curveId); + } + + /// @inheritdoc ICSPerksRegistry + function getPerformanceLeewayData( + uint256 curveId + ) + external + view + returns ( + uint256[] memory keyPivots, + uint256[] memory performanceLeeways + ) + { + if (_performanceLeewayValuesData[curveId].length == 0) revert NoData(); + return ( + _performanceLeewayPivotsData[curveId], + _performanceLeewayValuesData[curveId] + ); + } + + /// @inheritdoc ICSPerksRegistry + function setPriorityQueueLimit( + uint256 curveId, + uint256 limit + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + _priorityQueueLimits[curveId] = limit; + emit PriorityQueueLimitSet(curveId, limit); + } + + /// @inheritdoc ICSPerksRegistry + function getPriorityQueueLimit( + uint256 curveId + ) external view returns (uint256 limit) { + return _priorityQueueLimits[curveId]; + } +} diff --git a/src/interfaces/ICSPerksRegistry.sol b/src/interfaces/ICSPerksRegistry.sol new file mode 100644 index 00000000..bf90cc1d --- /dev/null +++ b/src/interfaces/ICSPerksRegistry.sol @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.24; + +interface ICSPerksRegistry { + event RewardShareDataSet(uint256 indexed curveId); + event PerformanceLeewayDataSet(uint256 indexed curveId); + event PriorityQueueLimitSet(uint256 indexed curveId, uint256 limit); + + error InvalidRewardShareData(); + error InvalidPerformanceLeewayData(); + error InvalidPriorityQueueLimit(); + error NoData(); + error ZeroAdminAddress(); + + /// @notice Set reward share parameters for the curveId + /// @dev keyPivots = [10, 50] and rewardShares = [10000, 8000, 5000] stands for + /// 100% rewards for the keys 1-10, 80% rewards for the keys 11-50, and 50% rewards for the keys > 50 + /// @param curveId Curve Id to associate reward share data with + /// @param keyPivots Pivot numbers of the keys (ex. [10, 50]) + /// @param rewardShares Reward share percentages in BP (ex. [10000, 8000, 5000]) + function setRewardShareData( + uint256 curveId, + uint256[] calldata keyPivots, + uint256[] calldata rewardShares + ) external; + + /// @notice Get reward share parameters by the curveId. + /// @dev Reverts if the values are not set for the given curveId. + /// @dev keyPivots = [10, 50] and rewardShares = [10000, 8000, 5000] stands for + /// 100% rewards for the keys 1-10, 80% rewards for the keys 11-50, and 50% rewards for the keys > 50 + /// @param curveId Curve Id to get reward share data for + /// @return keyPivots Pivot numbers of the keys (ex. [10, 50]) + /// @return rewardShares Reward share percentages in BP (ex. [10000, 8000, 5000]) + function getRewardShareData( + uint256 curveId + ) + external + view + returns (uint256[] memory keyPivots, uint256[] memory rewardShares); + + /// @notice Set performance leeway parameters for the curveId + /// @dev keyPivots = [100, 500] and performanceLeeways = [500, 450, 400] stands for + /// 5% performance leeway for the keys 1-100, 4.5% performance leeway for the keys 101-500, and 4% performance leeway for the keys > 500 + /// @param curveId Curve Id to associate performance leeway data with + /// @param keyPivots Pivot numbers of the keys (ex. [100, 500]) + /// @param performanceLeeways Performance leeway percentages in BP (ex. [500, 450, 400]) + function setPerformanceLeewayData( + uint256 curveId, + uint256[] calldata keyPivots, + uint256[] calldata performanceLeeways + ) external; + + /// @notice Get performance leeway parameters by the curveId. + /// @dev Reverts if the values are not set for the given curveId. + /// @dev keyPivots = [100, 500] and performanceLeeways = [500, 450, 400] stands for + /// 5% performance leeway for the keys 1-100, 4.5% performance leeway for the keys 101-500, and 4% performance leeway for the keys > 500 + /// @param curveId Curve Id to get performance leeway data for + /// @return keyPivots Pivot numbers of the keys (ex. [100, 500]) + /// @return performanceLeeways Performance leeway percentages in BP (ex. [500, 450, 400]) + function getPerformanceLeewayData( + uint256 curveId + ) + external + view + returns ( + uint256[] memory keyPivots, + uint256[] memory performanceLeeways + ); + + /// @notice Set priority queue limit for the curveId. + /// @dev The first `limit` keys for the Node Operator with the given `curveId` will be placed in the priority queue. + /// @param curveId Curve Id to associate priority queue limit with + /// @param limit Priority queue limit + function setPriorityQueueLimit(uint256 curveId, uint256 limit) external; + + /// @notice Get priority queue limit by the curveId. + /// @dev Zero is returned if the value is not set for the given curveId. + /// @dev The first `limit` keys for the Node Operator with the given `curveId` will be placed in the priority queue. + /// @param curveId Curve Id to get priority queue limit for + /// @return limit Priority queue limit + function getPriorityQueueLimit( + uint256 curveId + ) external view returns (uint256 limit); +} diff --git a/test/CSPerksRegistry.t.sol b/test/CSPerksRegistry.t.sol new file mode 100644 index 00000000..2e587e37 --- /dev/null +++ b/test/CSPerksRegistry.t.sol @@ -0,0 +1,456 @@ +// SPDX-FileCopyrightText: 2024 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.24; + +import "forge-std/Test.sol"; + +import { CSPerksRegistry } from "../src/CSPerksRegistry.sol"; +import { ICSPerksRegistry } from "../src/interfaces/ICSPerksRegistry.sol"; + +import { Utilities } from "./helpers/Utilities.sol"; +import { Fixtures } from "./helpers/Fixtures.sol"; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +contract CSPerksRegistryBaseTest is Test, Utilities, Fixtures { + address internal admin; + address internal stranger; + + CSPerksRegistry internal PerksRegistry; + + function setUp() public virtual { + admin = nextAddress("ADMIN"); + stranger = nextAddress("STRANGER"); + + PerksRegistry = new CSPerksRegistry(); + } +} + +contract CSPerksRegistryInitTest is CSPerksRegistryBaseTest { + function test_constructor_RevertWhen_InitOnImpl() public { + vm.expectRevert(Initializable.InvalidInitialization.selector); + PerksRegistry.initialize(admin); + } + + function test_initialize_happyPath() public { + _enableInitializers(address(PerksRegistry)); + + PerksRegistry.initialize(admin); + + assertTrue( + PerksRegistry.hasRole(PerksRegistry.DEFAULT_ADMIN_ROLE(), admin) + ); + } + + function test_initialize_RevertWhen_ZeroAdminAddress() public { + _enableInitializers(address(PerksRegistry)); + vm.expectRevert(ICSPerksRegistry.ZeroAdminAddress.selector); + PerksRegistry.initialize(address(0)); + } +} + +contract CSPerksRegistryRewardShareDataTest is CSPerksRegistryBaseTest { + function setUp() public virtual override { + super.setUp(); + _enableInitializers(address(PerksRegistry)); + PerksRegistry.initialize(admin); + } + + function test_setRewardShareData_set_valid_data() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 10; + + uint256[] memory rewardShares = new uint256[](2); + rewardShares[0] = 10000; + rewardShares[1] = 8000; + + vm.expectEmit(true, true, true, true, address(PerksRegistry)); + emit ICSPerksRegistry.RewardShareDataSet(curveId); + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + } + + function test_setRewardShareData_RevertWhen_not_admin() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 10; + + uint256[] memory rewardShares = new uint256[](2); + rewardShares[0] = 10000; + rewardShares[1] = 8000; + + bytes32 role = PerksRegistry.DEFAULT_ADMIN_ROLE(); + expectRoleRevert(stranger, role); + vm.prank(stranger); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + } + + function test_setRewardShareData_RevertWhen_invalid_data_length() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](2); + keyPivots[0] = 10; + keyPivots[1] = 100; + + uint256[] memory rewardShares = new uint256[](2); + rewardShares[0] = 10000; + rewardShares[1] = 8000; + + vm.expectRevert(ICSPerksRegistry.InvalidRewardShareData.selector); + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + } + + function test_setRewardShareData_RevertWhen_invalid_pivots_sort() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](2); + keyPivots[0] = 100; + keyPivots[1] = 10; + + uint256[] memory rewardShares = new uint256[](3); + rewardShares[0] = 10000; + rewardShares[1] = 8000; + rewardShares[2] = 5000; + + vm.expectRevert(ICSPerksRegistry.InvalidRewardShareData.selector); + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + } + + function test_setRewardShareData_RevertWhen_first_pivot_is_zero() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](2); + keyPivots[0] = 0; + keyPivots[1] = 10; + + uint256[] memory rewardShares = new uint256[](3); + rewardShares[0] = 10000; + rewardShares[1] = 8000; + rewardShares[2] = 5000; + + vm.expectRevert(ICSPerksRegistry.InvalidRewardShareData.selector); + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + } + + function test_setRewardShareData_RevertWhen_invalid_bp_values() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 10; + + uint256[] memory rewardShares = new uint256[](2); + rewardShares[0] = 100000; + rewardShares[1] = 8000; + + vm.expectRevert(ICSPerksRegistry.InvalidRewardShareData.selector); + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + } + + function test_getRewardShareData_usual_data() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 10; + + uint256[] memory rewardShares = new uint256[](2); + rewardShares[0] = 10000; + rewardShares[1] = 8000; + + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + + ( + uint256[] memory keyPivotsOut, + uint256[] memory rewardSharesOut + ) = PerksRegistry.getRewardShareData(curveId); + + assertEq(keyPivotsOut.length, keyPivots.length); + for (uint256 i = 0; i < keyPivotsOut.length; ++i) { + assertEq(keyPivotsOut[i], keyPivots[i]); + } + + assertEq(rewardSharesOut.length, rewardShares.length); + for (uint256 i = 0; i < rewardSharesOut.length; ++i) { + assertEq(rewardSharesOut[i], rewardShares[i]); + } + } + + function test_getRewardShareData_no_pivots_data() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](0); + + uint256[] memory rewardShares = new uint256[](1); + rewardShares[0] = 8000; + + vm.prank(admin); + PerksRegistry.setRewardShareData(curveId, keyPivots, rewardShares); + + ( + uint256[] memory keyPivotsOut, + uint256[] memory rewardSharesOut + ) = PerksRegistry.getRewardShareData(curveId); + + assertEq(keyPivotsOut.length, keyPivots.length); + + assertEq(rewardSharesOut.length, rewardShares.length); + for (uint256 i = 0; i < rewardSharesOut.length; ++i) { + assertEq(rewardSharesOut[i], rewardShares[i]); + } + } + + function test_getRewardShareData_RevertWhen_no_data() public { + uint256 curveId = 0; + vm.expectRevert(ICSPerksRegistry.NoData.selector); + ( + uint256[] memory keyPivotsOut, + uint256[] memory rewardSharesOut + ) = PerksRegistry.getRewardShareData(curveId); + } +} + +contract CSPerksRegistryPerformanceLeewayDataTest is CSPerksRegistryBaseTest { + function setUp() public virtual override { + super.setUp(); + _enableInitializers(address(PerksRegistry)); + PerksRegistry.initialize(admin); + } + + function test_setPerformanceLeewayData_set_valid_data() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 100; + + uint256[] memory performanceLeeways = new uint256[](2); + performanceLeeways[0] = 500; + performanceLeeways[1] = 400; + + vm.expectEmit(true, true, true, true, address(PerksRegistry)); + emit ICSPerksRegistry.PerformanceLeewayDataSet(curveId); + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + } + + function test_setPerformanceLeewayData_RevertWhen_not_admin() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 100; + + uint256[] memory performanceLeeways = new uint256[](2); + performanceLeeways[0] = 500; + performanceLeeways[1] = 400; + + bytes32 role = PerksRegistry.DEFAULT_ADMIN_ROLE(); + expectRoleRevert(stranger, role); + vm.prank(stranger); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + } + + function test_setPerformanceLeewayData_RevertWhen_invalid_data_length() + public + { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](2); + keyPivots[0] = 10; + keyPivots[1] = 100; + + uint256[] memory performanceLeeways = new uint256[](2); + performanceLeeways[0] = 500; + performanceLeeways[1] = 400; + + vm.expectRevert(ICSPerksRegistry.InvalidPerformanceLeewayData.selector); + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + } + + function test_setPerformanceLeewayData_RevertWhen_invalid_pivots_sort() + public + { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](2); + keyPivots[0] = 100; + keyPivots[1] = 10; + + uint256[] memory performanceLeeways = new uint256[](3); + performanceLeeways[0] = 500; + performanceLeeways[1] = 400; + performanceLeeways[2] = 300; + + vm.expectRevert(ICSPerksRegistry.InvalidPerformanceLeewayData.selector); + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + } + + function test_setPerformanceLeewayData_RevertWhen_first_pivot_is_zero() + public + { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](2); + keyPivots[0] = 0; + keyPivots[1] = 10; + + uint256[] memory performanceLeeways = new uint256[](3); + performanceLeeways[0] = 500; + performanceLeeways[1] = 400; + performanceLeeways[2] = 300; + + vm.expectRevert(ICSPerksRegistry.InvalidPerformanceLeewayData.selector); + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + } + + function test_setPerformanceLeewayData_RevertWhen_invalid_bp_values() + public + { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 100; + + uint256[] memory performanceLeeways = new uint256[](2); + performanceLeeways[0] = 50000; + performanceLeeways[1] = 400; + + vm.expectRevert(ICSPerksRegistry.InvalidPerformanceLeewayData.selector); + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + } + + function test_getPerformanceLeewayData_usual_data() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](1); + keyPivots[0] = 100; + + uint256[] memory performanceLeeways = new uint256[](2); + performanceLeeways[0] = 500; + performanceLeeways[1] = 400; + + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + + ( + uint256[] memory keyPivotsOut, + uint256[] memory performanceLeewaysOut + ) = PerksRegistry.getPerformanceLeewayData(curveId); + + assertEq(keyPivotsOut.length, keyPivots.length); + for (uint256 i = 0; i < keyPivotsOut.length; ++i) { + assertEq(keyPivotsOut[i], keyPivots[i]); + } + + assertEq(performanceLeewaysOut.length, performanceLeeways.length); + for (uint256 i = 0; i < performanceLeewaysOut.length; ++i) { + assertEq(performanceLeewaysOut[i], performanceLeeways[i]); + } + } + + function test_getPerformanceLeewayData_no_pivots_data() public { + uint256 curveId = 1; + uint256[] memory keyPivots = new uint256[](0); + + uint256[] memory performanceLeeways = new uint256[](1); + performanceLeeways[0] = 500; + + vm.prank(admin); + PerksRegistry.setPerformanceLeewayData( + curveId, + keyPivots, + performanceLeeways + ); + + ( + uint256[] memory keyPivotsOut, + uint256[] memory performanceLeewaysOut + ) = PerksRegistry.getPerformanceLeewayData(curveId); + + assertEq(keyPivotsOut.length, keyPivots.length); + + assertEq(performanceLeewaysOut.length, performanceLeeways.length); + for (uint256 i = 0; i < performanceLeewaysOut.length; ++i) { + assertEq(performanceLeewaysOut[i], performanceLeeways[i]); + } + } + + function test_getPerformanceLeewayData_RevertWhen_no_data() public { + uint256 curveId = 0; + vm.expectRevert(ICSPerksRegistry.NoData.selector); + ( + uint256[] memory keyPivotsOut, + uint256[] memory performanceLeewaysOut + ) = PerksRegistry.getPerformanceLeewayData(curveId); + } +} + +contract CSPerksRegistryPriorityQueueLimitTest is CSPerksRegistryBaseTest { + function setUp() public virtual override { + super.setUp(); + _enableInitializers(address(PerksRegistry)); + PerksRegistry.initialize(admin); + } + + function test_setPriorityQueueLimit_set_valid_data() public { + uint256 curveId = 1; + uint256 limit = 20; + + vm.expectEmit(true, true, true, true, address(PerksRegistry)); + emit ICSPerksRegistry.PriorityQueueLimitSet(curveId, limit); + vm.prank(admin); + PerksRegistry.setPriorityQueueLimit(curveId, limit); + } + + function test_setPriorityQueueLimit_RevertWhen_not_admin() public { + uint256 curveId = 1; + uint256 limit = 20; + + bytes32 role = PerksRegistry.DEFAULT_ADMIN_ROLE(); + expectRoleRevert(stranger, role); + vm.prank(stranger); + PerksRegistry.setPriorityQueueLimit(curveId, limit); + } + + function test_getPerformanceLeewayData_usual_data() public { + uint256 curveId = 1; + uint256 limit = 20; + + vm.prank(admin); + PerksRegistry.setPriorityQueueLimit(curveId, limit); + + uint256 limitOut = PerksRegistry.getPriorityQueueLimit(curveId); + + assertEq(limitOut, limit); + } + + function test_getPerformanceLeewayData_default_return() public { + uint256 curveId = 0; + uint256 limitOut = PerksRegistry.getPriorityQueueLimit(curveId); + + assertEq(limitOut, 0); + } +}