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/script/DeployBase.s.sol b/script/DeployBase.s.sol index 6c6aa6fd..cbf73ed3 100644 --- a/script/DeployBase.s.sol +++ b/script/DeployBase.s.sol @@ -191,7 +191,8 @@ abstract contract DeployBase is Script { ), pivotSlot: Slot.wrap( uint64(config.verifierSupportedEpoch * config.slotsPerEpoch) - ) + ), + admin: deployer }); accounting.initialize({ @@ -261,15 +262,17 @@ abstract contract DeployBase is Script { _avgPerfLeewayBP: config.avgPerfLeewayBP }); - address[] memory sealables = new address[](3); + address[] memory sealables = new address[](4); sealables[0] = address(csm); sealables[1] = address(accounting); sealables[2] = address(oracle); + sealables[3] = address(verifier); address gateSeal = _deployGateSeal(sealables); csm.grantRole(csm.PAUSE_ROLE(), gateSeal); oracle.grantRole(oracle.PAUSE_ROLE(), gateSeal); accounting.grantRole(accounting.PAUSE_ROLE(), gateSeal); + verifier.grantRole(verifier.PAUSE_ROLE(), gateSeal); accounting.grantRole( accounting.RESET_BOND_CURVE_ROLE(), config.setResetBondCurveAddress @@ -299,6 +302,12 @@ abstract contract DeployBase is Script { csm.grantRole(csm.DEFAULT_ADMIN_ROLE(), config.aragonAgent); csm.revokeRole(csm.DEFAULT_ADMIN_ROLE(), deployer); + verifier.grantRole( + verifier.DEFAULT_ADMIN_ROLE(), + config.aragonAgent + ); + verifier.revokeRole(verifier.DEFAULT_ADMIN_ROLE(), deployer); + accounting.grantRole( accounting.DEFAULT_ADMIN_ROLE(), config.aragonAgent diff --git a/script/DeployCSVerifierElectra.s.sol b/script/DeployCSVerifierElectra.s.sol index 254e87ef..3c046a23 100644 --- a/script/DeployCSVerifierElectra.s.sol +++ b/script/DeployCSVerifierElectra.s.sol @@ -24,6 +24,7 @@ struct Config { Slot firstSupportedSlot; Slot pivotSlot; uint64 slotsPerEpoch; + address admin; } // Check the constants below via `yarn run gindex`. @@ -63,7 +64,8 @@ abstract contract DeployCSVerifier is Script { gIHistoricalSummariesPrev: config.gIHistoricalSummariesPrev, gIHistoricalSummariesCurr: config.gIHistoricalSummariesCurr, firstSupportedSlot: config.firstSupportedSlot, - pivotSlot: config.pivotSlot + pivotSlot: config.pivotSlot, + admin: config.admin }); console.log("CSVerifier deployed at:", address(verifier)); } @@ -82,7 +84,8 @@ contract DeployCSVerifierHolesky is DeployCSVerifier { gIHistoricalSummariesPrev: HISTORICAL_SUMMARIES_DENEB, gIHistoricalSummariesCurr: HISTORICAL_SUMMARIES_ELECTRA, firstSupportedSlot: Slot.wrap(950272), // 269_568 * 32, @see https://github.com/eth-clients/mainnet/blob/main/metadata/config.yaml#L52 - pivotSlot: Slot.wrap(0) // TODO: Update with Electra slot. + pivotSlot: Slot.wrap(0), // TODO: Update with Electra slot. + admin: 0xE92329EC7ddB11D25e25b3c21eeBf11f15eB325d // Aragon Agent }); } } @@ -100,7 +103,8 @@ contract DeployCSVerifierMainnet is DeployCSVerifier { gIHistoricalSummariesPrev: HISTORICAL_SUMMARIES_DENEB, gIHistoricalSummariesCurr: HISTORICAL_SUMMARIES_ELECTRA, firstSupportedSlot: Slot.wrap(8626176), // 29_696 * 32, @see https://github.com/eth-clients/holesky/blob/main/metadata/config.yaml#L38 - pivotSlot: Slot.wrap(0) // TODO: Update with Electra slot. + pivotSlot: Slot.wrap(0), // TODO: Update with Electra slot. + admin: 0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c // Aragon Agent }); } } diff --git a/script/DeployImplementationsBase.s.sol b/script/DeployImplementationsBase.s.sol index ef932a5c..770315e8 100644 --- a/script/DeployImplementationsBase.s.sol +++ b/script/DeployImplementationsBase.s.sol @@ -22,6 +22,7 @@ import { Slot } from "../src/lib/Types.sol"; import { DeployBase } from "./DeployBase.s.sol"; abstract contract DeployImplementationsBase is DeployBase { + address gateSeal; address earlyAdoption; function _deploy() internal { @@ -90,9 +91,24 @@ abstract contract DeployImplementationsBase is DeployBase { ), pivotSlot: Slot.wrap( uint64(config.verifierSupportedEpoch * config.slotsPerEpoch) - ) + ), + admin: deployer }); + address[] memory sealables = new address[](4); + sealables[0] = address(csm); + sealables[1] = address(accounting); + sealables[2] = address(oracle); + sealables[3] = address(verifier); + gateSeal = _deployGateSeal(sealables); + + verifier.grantRole(verifier.PAUSE_ROLE(), address(gateSeal)); + verifier.grantRole( + verifier.DEFAULT_ADMIN_ROLE(), + config.aragonAgent + ); + verifier.revokeRole(verifier.DEFAULT_ADMIN_ROLE(), deployer); + JsonObj memory deployJson = Json.newObj(); deployJson.set("PermissionlessGate", address(permissionlessGate)); deployJson.set("VettedGate", address(vettedGate)); @@ -102,6 +118,7 @@ abstract contract DeployImplementationsBase is DeployBase { deployJson.set("CSFeeDistributorImpl", address(feeDistributorImpl)); deployJson.set("CSVerifier", address(verifier)); deployJson.set("HashConsensus", address(hashConsensus)); + deployJson.set("GateSeal", address(gateSeal)); deployJson.set("git-ref", gitRef); vm.writeJson( deployJson.str, diff --git a/script/fork-helpers/SimulateVote.s.sol b/script/fork-helpers/SimulateVote.s.sol index 96d1fcc8..3bbbe03c 100644 --- a/script/fork-helpers/SimulateVote.s.sol +++ b/script/fork-helpers/SimulateVote.s.sol @@ -8,6 +8,8 @@ import { DeploymentFixtures } from "test/helpers/Fixtures.sol"; import { IStakingRouter } from "../../src/interfaces/IStakingRouter.sol"; import { OssifiableProxy } from "../../src/lib/proxy/OssifiableProxy.sol"; import { CSModule } from "../../src/CSModule.sol"; +import { CSAccounting } from "../../src/CSAccounting.sol"; +import { CSFeeOracle } from "../../src/CSFeeOracle.sol"; import { IBurner } from "../../src/interfaces/IBurner.sol"; import { ForkHelpersCommon } from "./Common.sol"; @@ -100,6 +102,8 @@ contract SimulateVote is Script, DeploymentFixtures, ForkHelpersCommon { address admin = _prepareAdmin(deploymentConfig.csm); csm = CSModule(deploymentConfig.csm); + accounting = CSAccounting(deploymentConfig.accounting); + oracle = CSFeeOracle(deploymentConfig.oracle); vm.startBroadcast(admin); @@ -121,6 +125,22 @@ contract SimulateVote is Script, DeploymentFixtures, ForkHelpersCommon { csm.revokeRole(csm.VERIFIER_ROLE(), address(deploymentConfig.verifier)); csm.grantRole(csm.VERIFIER_ROLE(), address(upgradeConfig.verifier)); + csm.revokeRole(csm.PAUSE_ROLE(), address(deploymentConfig.gateSeal)); + accounting.revokeRole( + accounting.PAUSE_ROLE(), + address(deploymentConfig.gateSeal) + ); + oracle.revokeRole( + oracle.PAUSE_ROLE(), + address(deploymentConfig.gateSeal) + ); + + csm.grantRole(csm.PAUSE_ROLE(), address(upgradeConfig.gateSeal)); + accounting.grantRole( + accounting.PAUSE_ROLE(), + address(upgradeConfig.gateSeal) + ); + oracle.grantRole(oracle.PAUSE_ROLE(), address(upgradeConfig.gateSeal)); vm.stopBroadcast(); } } diff --git a/src/CSAccounting.sol b/src/CSAccounting.sol index 1b626abe..6ad9201f 100644 --- a/src/CSAccounting.sol +++ b/src/CSAccounting.sol @@ -467,6 +467,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/CSFeeDistributor.sol b/src/CSFeeDistributor.sol index c9a1b28f..f6043d7b 100644 --- a/src/CSFeeDistributor.sol +++ b/src/CSFeeDistributor.sol @@ -82,7 +82,7 @@ contract CSFeeDistributor is } STETH.transferShares(ACCOUNTING, sharesToDistribute); - emit FeeDistributed(nodeOperatorId, sharesToDistribute); + emit OperatorFeeDistributed(nodeOperatorId, sharesToDistribute); } /// @inheritdoc ICSFeeDistributor @@ -121,6 +121,8 @@ contract CSFeeDistributor is ); } + emit ModuleFeeDistributed(distributed); + // NOTE: Make sure off-chain tooling provides a distinct CID of a log even for empty reports, e.g. by mixing // in a frame identifier such as reference slot to a file. if (bytes(_logCid).length == 0) revert InvalidLogCID(); 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/CSVerifier.sol b/src/CSVerifier.sol index 4ce50582..5a3d3788 100644 --- a/src/CSVerifier.sol +++ b/src/CSVerifier.sol @@ -5,6 +5,8 @@ pragma solidity 0.8.24; import { ICSVerifier } from "./interfaces/ICSVerifier.sol"; import { ICSModule } from "./interfaces/ICSModule.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import { PausableUntil } from "./lib/utils/PausableUntil.sol"; import { BeaconBlockHeader, Slot, Validator, Withdrawal } from "./lib/Types.sol"; import { GIndex } from "./lib/GIndex.sol"; @@ -22,13 +24,16 @@ function gweiToWei(uint64 amount) pure returns (uint256) { return uint256(amount) * 1 gwei; } -contract CSVerifier is ICSVerifier { +contract CSVerifier is ICSVerifier, AccessControlEnumerable, PausableUntil { using { amountWei } for Withdrawal; using SSZ for BeaconBlockHeader; using SSZ for Withdrawal; using SSZ for Validator; + bytes32 public constant PAUSE_ROLE = keccak256("PAUSE_ROLE"); + bytes32 public constant RESUME_ROLE = keccak256("RESUME_ROLE"); + // See `BEACON_ROOTS_ADDRESS` constant in the EIP-4788. address public constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; @@ -77,10 +82,12 @@ contract CSVerifier is ICSVerifier { GIndex gIHistoricalSummariesPrev, GIndex gIHistoricalSummariesCurr, Slot firstSupportedSlot, - Slot pivotSlot + Slot pivotSlot, + address admin ) { if (withdrawalAddress == address(0)) revert ZeroWithdrawalAddress(); if (module == address(0)) revert ZeroModuleAddress(); + if (admin == address(0)) revert ZeroAdminAddress(); if (slotsPerEpoch == 0) revert InvalidChainConfig(); if (firstSupportedSlot > pivotSlot) revert InvalidPivotSlot(); @@ -101,6 +108,18 @@ contract CSVerifier is ICSVerifier { FIRST_SUPPORTED_SLOT = firstSupportedSlot; PIVOT_SLOT = pivotSlot; + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /// @inheritdoc ICSVerifier + function resume() external onlyRole(RESUME_ROLE) { + _resume(); + } + + /// @inheritdoc ICSVerifier + function pauseFor(uint256 duration) external onlyRole(PAUSE_ROLE) { + _pauseFor(duration); } /// @inheritdoc ICSVerifier @@ -109,7 +128,7 @@ contract CSVerifier is ICSVerifier { WithdrawalWitness calldata witness, uint256 nodeOperatorId, uint256 keyIndex - ) external { + ) external whenResumed { if (beaconBlock.header.slot < FIRST_SUPPORTED_SLOT) { revert UnsupportedSlot(beaconBlock.header.slot); } @@ -151,7 +170,7 @@ contract CSVerifier is ICSVerifier { WithdrawalWitness calldata witness, uint256 nodeOperatorId, uint256 keyIndex - ) external { + ) external whenResumed { if (beaconBlock.header.slot < FIRST_SUPPORTED_SLOT) { revert UnsupportedSlot(beaconBlock.header.slot); } diff --git a/src/interfaces/ICSAccounting.sol b/src/interfaces/ICSAccounting.sol index 371bd053..31d64516 100644 --- a/src/interfaces/ICSAccounting.sol +++ b/src/interfaces/ICSAccounting.sol @@ -161,6 +161,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/src/interfaces/ICSFeeDistributor.sol b/src/interfaces/ICSFeeDistributor.sol index 438db952..928373af 100644 --- a/src/interfaces/ICSFeeDistributor.sol +++ b/src/interfaces/ICSFeeDistributor.sol @@ -8,7 +8,10 @@ pragma solidity 0.8.24; interface ICSFeeDistributor is IAssetRecovererLib { /// @dev Emitted when fees are distributed - event FeeDistributed(uint256 indexed nodeOperatorId, uint256 shares); + event OperatorFeeDistributed( + uint256 indexed nodeOperatorId, + uint256 shares + ); /// @dev Emitted when distribution data is updated event DistributionDataUpdated( @@ -20,6 +23,9 @@ interface ICSFeeDistributor is IAssetRecovererLib { /// @dev Emitted when distribution log is updated event DistributionLogUpdated(string logCid); + /// @dev It logs how many shares were distributed in the latest report + event ModuleFeeDistributed(uint256 shares); + error ZeroAccountingAddress(); error ZeroStEthAddress(); error ZeroAdminAddress(); 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/src/interfaces/ICSVerifier.sol b/src/interfaces/ICSVerifier.sol index 8412d7c4..0fb8fcc9 100644 --- a/src/interfaces/ICSVerifier.sol +++ b/src/interfaces/ICSVerifier.sol @@ -61,8 +61,13 @@ interface ICSVerifier { error UnsupportedSlot(Slot slot); error ZeroModuleAddress(); error ZeroWithdrawalAddress(); + error ZeroAdminAddress(); error InvalidPivotSlot(); + function PAUSE_ROLE() external view returns (bytes32); + + function RESUME_ROLE() external view returns (bytes32); + function BEACON_ROOTS() external view returns (address); function SLOTS_PER_EPOCH() external view returns (uint64); @@ -87,6 +92,13 @@ interface ICSVerifier { function MODULE() external view returns (ICSModule); + /// @notice Pause write methods calls for `duration` seconds + /// @param duration Duration of the pause in seconds + function pauseFor(uint256 duration) external; + + /// @notice Resume write methods calls + function resume() external; + /// @notice Verify withdrawal proof and report withdrawal to the module for valid proofs /// @param beaconBlock Beacon block header /// @param witness Withdrawal witness against the `beaconBlock`'s state root. diff --git a/test/CSAccounting.t.sol b/test/CSAccounting.t.sol index 3af35b9a..31d186c8 100644 --- a/test/CSAccounting.t.sol +++ b/test/CSAccounting.t.sol @@ -2458,7 +2458,7 @@ contract CSAccountingClaimWstETHRewardsTest is } } -contract CSAccountingclaimRewardsUnstETHTest is +contract CSAccountingClaimRewardsUnstETHTest is CSAccountingClaimRewardsBaseTest { function test_default() public override assertInvariants { @@ -2869,6 +2869,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(); diff --git a/test/CSFeeDistributor.t.sol b/test/CSFeeDistributor.t.sol index 79037a95..34ab111f 100644 --- a/test/CSFeeDistributor.t.sol +++ b/test/CSFeeDistributor.t.sol @@ -161,7 +161,7 @@ contract CSFeeDistributorTest is CSFeeDistributorTestBase { ); vm.expectEmit(true, true, true, true, address(feeDistributor)); - emit ICSFeeDistributor.FeeDistributed(nodeOperatorId, shares); + emit ICSFeeDistributor.OperatorFeeDistributed(nodeOperatorId, shares); vm.prank(address(accounting)); feeDistributor.distributeFees({ @@ -419,6 +419,9 @@ contract CSFeeDistributorTest is CSFeeDistributorTestBase { treeCid ); + vm.expectEmit(true, true, true, true, address(feeDistributor)); + emit ICSFeeDistributor.ModuleFeeDistributed(shares); + vm.expectEmit(true, true, true, true, address(feeDistributor)); emit ICSFeeDistributor.DistributionLogUpdated(logCid); 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); + } +} diff --git a/test/CSVerifier.t.sol b/test/CSVerifier.t.sol index ead089fa..418af3b3 100644 --- a/test/CSVerifier.t.sol +++ b/test/CSVerifier.t.sol @@ -7,6 +7,7 @@ import { stdJson } from "forge-std/StdJson.sol"; import { ICSVerifier } from "../src/interfaces/ICSVerifier.sol"; import { ICSModule } from "../src/interfaces/ICSModule.sol"; +import { PausableUntil } from "../src/lib/utils/PausableUntil.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { CSVerifier } from "../src/CSVerifier.sol"; @@ -38,6 +39,11 @@ contract CSVerifierTestBase is Test, Utilities { CSVerifier public verifier; Stub public module; Slot public firstSupportedSlot; + address public admin; + address public stranger; + + bytes32 public pauseRole; + bytes32 public resumeRole; string internal fixturesPath = "./test/fixtures/CSVerifier/"; @@ -54,6 +60,7 @@ contract CSVerifierTestConstructor is CSVerifierTestBase { function setUp() public { module = new Stub(); firstSupportedSlot = Slot.wrap(100_500); + admin = nextAddress("ADMIN"); } function test_constructor() public { @@ -70,7 +77,8 @@ contract CSVerifierTestConstructor is CSVerifierTestBase { gIFirstValidatorPrev: pack(0x560000000000, 40), gIFirstValidatorCurr: pack(0x560000000001, 40), firstSupportedSlot: firstSupportedSlot, - pivotSlot: Slot.wrap(100_501) + pivotSlot: Slot.wrap(100_501), + admin: admin }); assertEq(address(verifier.WITHDRAWAL_ADDRESS()), withdrawalAddress); @@ -123,7 +131,8 @@ contract CSVerifierTestConstructor is CSVerifierTestBase { gIFirstValidatorPrev: pack(0x560000000000, 40), gIFirstValidatorCurr: pack(0x560000000000, 40), firstSupportedSlot: firstSupportedSlot, // Any value less than the slots from the fixtures. - pivotSlot: firstSupportedSlot + pivotSlot: firstSupportedSlot, + admin: admin }); } @@ -140,7 +149,8 @@ contract CSVerifierTestConstructor is CSVerifierTestBase { gIFirstValidatorPrev: pack(0x560000000000, 40), gIFirstValidatorCurr: pack(0x560000000000, 40), firstSupportedSlot: firstSupportedSlot, // Any value less than the slots from the fixtures. - pivotSlot: firstSupportedSlot + pivotSlot: firstSupportedSlot, + admin: admin }); } @@ -157,7 +167,26 @@ contract CSVerifierTestConstructor is CSVerifierTestBase { gIFirstValidatorPrev: pack(0x560000000000, 40), gIFirstValidatorCurr: pack(0x560000000000, 40), firstSupportedSlot: firstSupportedSlot, // Any value less than the slots from the fixtures. - pivotSlot: firstSupportedSlot + pivotSlot: firstSupportedSlot, + admin: admin + }); + } + + function test_constructor_RevertWhen_ZeroAdminAddress() public { + vm.expectRevert(ICSVerifier.ZeroAdminAddress.selector); + verifier = new CSVerifier({ + withdrawalAddress: nextAddress(), + module: address(module), + slotsPerEpoch: 32, + gIHistoricalSummariesPrev: pack(0x0, 0), // We don't care of the value for this test. + gIHistoricalSummariesCurr: pack(0x0, 0), // We don't care of the value for this test. + gIFirstWithdrawalPrev: pack(0xe1c0, 4), + gIFirstWithdrawalCurr: pack(0xe1c0, 4), + gIFirstValidatorPrev: pack(0x560000000000, 40), + gIFirstValidatorCurr: pack(0x560000000000, 40), + firstSupportedSlot: firstSupportedSlot, // Any value less than the slots from the fixtures. + pivotSlot: firstSupportedSlot, + admin: address(0) }); } } @@ -165,6 +194,7 @@ contract CSVerifierTestConstructor is CSVerifierTestBase { contract CSVerifierWithdrawalTest is CSVerifierTestBase { function setUp() public { module = new Stub(); + admin = nextAddress("ADMIN"); verifier = new CSVerifier({ withdrawalAddress: 0xb3E29C46Ee1745724417C0C51Eb2351A1C01cF36, @@ -177,8 +207,17 @@ contract CSVerifierWithdrawalTest is CSVerifierTestBase { gIFirstValidatorPrev: pack(0x560000000000, 40), gIFirstValidatorCurr: pack(0x560000000000, 40), firstSupportedSlot: Slot.wrap(100_500), // Any value less than the slots from the fixtures. - pivotSlot: Slot.wrap(100_500) + pivotSlot: Slot.wrap(100_500), + admin: admin }); + + pauseRole = verifier.PAUSE_ROLE(); + resumeRole = verifier.RESUME_ROLE(); + + vm.startPrank(admin); + verifier.grantRole(pauseRole, admin); + verifier.grantRole(resumeRole, admin); + vm.stopPrank(); } function test_processWithdrawalProof() public { @@ -378,6 +417,27 @@ contract CSVerifierWithdrawalTest is CSVerifierTestBase { ); } + function test_processWithdrawalProof_RevertWhenPaused() public { + WithdrawalFixture memory fixture = abi.decode( + _readFixture("withdrawal.json"), + (WithdrawalFixture) + ); + + _setMocksWithdrawal(fixture); + + vm.prank(admin); + verifier.pauseFor(100_500); + assertTrue(verifier.isPaused()); + + vm.expectRevert(PausableUntil.ResumedExpected.selector); + verifier.processWithdrawalProof( + fixture.beaconBlock, + fixture.witness, + 0, + 0 + ); + } + function _setMocksWithdrawal(WithdrawalFixture memory fixture) internal { vm.mockCall( verifier.BEACON_ROOTS(), @@ -398,3 +458,83 @@ contract CSVerifierWithdrawalTest is CSVerifierTestBase { ); } } + +contract CSVerifierPauseTest is CSVerifierTestBase { + function setUp() public { + module = new Stub(); + admin = nextAddress("ADMIN"); + stranger = nextAddress("STRANGER"); + + verifier = new CSVerifier({ + withdrawalAddress: 0xb3E29C46Ee1745724417C0C51Eb2351A1C01cF36, + module: address(module), + slotsPerEpoch: 32, + gIHistoricalSummariesPrev: pack(0x0, 0), // We don't care of the value for this test. + gIHistoricalSummariesCurr: pack(0x0, 0), // We don't care of the value for this test. + gIFirstWithdrawalPrev: pack(0xe1c0, 4), + gIFirstWithdrawalCurr: pack(0xe1c0, 4), + gIFirstValidatorPrev: pack(0x560000000000, 40), + gIFirstValidatorCurr: pack(0x560000000000, 40), + firstSupportedSlot: Slot.wrap(100_500), // Any value less than the slots from the fixtures. + pivotSlot: Slot.wrap(100_500), + admin: admin + }); + + pauseRole = verifier.PAUSE_ROLE(); + resumeRole = verifier.RESUME_ROLE(); + + vm.startPrank(admin); + verifier.grantRole(pauseRole, admin); + verifier.grantRole(resumeRole, admin); + vm.stopPrank(); + } + + function test_pause() public { + assertFalse(verifier.isPaused()); + vm.prank(admin); + verifier.pauseFor(100_500); + assertTrue(verifier.isPaused()); + } + + function test_pause_RevertWhenNoRole() public { + expectRoleRevert(stranger, pauseRole); + vm.prank(stranger); + verifier.pauseFor(100_500); + } + + function test_pause_RevertWhenPaused() public { + vm.prank(admin); + verifier.pauseFor(100_500); + assertTrue(verifier.isPaused()); + + vm.expectRevert(PausableUntil.ResumedExpected.selector); + vm.prank(admin); + verifier.pauseFor(100_500); + } + + function test_resume() public { + vm.prank(admin); + verifier.pauseFor(100_500); + assertTrue(verifier.isPaused()); + + vm.prank(admin); + verifier.resume(); + assertFalse(verifier.isPaused()); + } + + function test_resume_RevertWhenNoRole() public { + vm.prank(admin); + verifier.pauseFor(100_500); + assertTrue(verifier.isPaused()); + + expectRoleRevert(stranger, resumeRole); + vm.prank(stranger); + verifier.resume(); + } + + function test_resume_RevertWhenNotPaused() public { + vm.expectRevert(PausableUntil.PausedExpected.selector); + vm.prank(admin); + verifier.resume(); + } +} diff --git a/test/CSVerifierHistorical.t.sol b/test/CSVerifierHistorical.t.sol index 97920ecd..c309b832 100644 --- a/test/CSVerifierHistorical.t.sol +++ b/test/CSVerifierHistorical.t.sol @@ -7,7 +7,7 @@ import { stdJson } from "forge-std/StdJson.sol"; import { ICSVerifier } from "../src/interfaces/ICSVerifier.sol"; import { ICSModule } from "../src/interfaces/ICSModule.sol"; - +import { PausableUntil } from "../src/lib/utils/PausableUntil.sol"; import { GIndex } from "../src/lib/GIndex.sol"; import { CSVerifier } from "../src/CSVerifier.sol"; @@ -15,6 +15,7 @@ import { ICSVerifier } from "../src/interfaces/ICSVerifier.sol"; import { pack } from "../src/lib/GIndex.sol"; import { Slot } from "../src/lib/Types.sol"; +import { Utilities } from "./helpers/Utilities.sol"; import { Stub } from "./helpers/mocks/Stub.sol"; function dec(Slot self) pure returns (Slot slot) { @@ -25,7 +26,7 @@ function dec(Slot self) pure returns (Slot slot) { using { dec } for Slot; -contract CSVerifierHistoricalTest is Test { +contract CSVerifierHistoricalTest is Test, Utilities { using stdJson for string; struct HistoricalWithdrawalFixture { @@ -38,11 +39,16 @@ contract CSVerifierHistoricalTest is Test { CSVerifier public verifier; Stub public module; + address public admin; + + bytes32 public pauseRole; + bytes32 public resumeRole; HistoricalWithdrawalFixture public fixture; function setUp() public { module = new Stub(); + admin = nextAddress("ADMIN"); verifier = new CSVerifier({ withdrawalAddress: 0xb3E29C46Ee1745724417C0C51Eb2351A1C01cF36, module: address(module), @@ -54,8 +60,17 @@ contract CSVerifierHistoricalTest is Test { gIFirstValidatorPrev: pack(0x560000000000, 40), gIFirstValidatorCurr: pack(0x560000000000, 40), firstSupportedSlot: Slot.wrap(100_500), // Any value less than the slots from the fixtures. - pivotSlot: Slot.wrap(100_500) + pivotSlot: Slot.wrap(100_500), + admin: admin }); + + pauseRole = verifier.PAUSE_ROLE(); + resumeRole = verifier.RESUME_ROLE(); + + vm.startPrank(admin); + verifier.grantRole(pauseRole, admin); + verifier.grantRole(resumeRole, admin); + vm.stopPrank(); } function _get_fixture() internal { @@ -171,6 +186,25 @@ contract CSVerifierHistoricalTest is Test { ); } + function test_processWithdrawalProof_RevertWhenPaused() public { + _get_fixture(); + _setMocksWithdrawal(fixture); + + vm.prank(admin); + verifier.pauseFor(100_500); + assertTrue(verifier.isPaused()); + + vm.expectRevert(PausableUntil.ResumedExpected.selector); + // solhint-disable-next-line func-named-parameters + verifier.processHistoricalWithdrawalProof( + fixture.beaconBlock, + fixture.oldBlock, + fixture.witness, + 0, + 0 + ); + } + function _setMocksWithdrawal( HistoricalWithdrawalFixture memory _fixture ) internal { diff --git a/test/CSVerifierHistoricalCrossForks.t.sol b/test/CSVerifierHistoricalCrossForks.t.sol index b6f9b9f2..7c01aed6 100644 --- a/test/CSVerifierHistoricalCrossForks.t.sol +++ b/test/CSVerifierHistoricalCrossForks.t.sol @@ -29,9 +29,11 @@ contract CSVerifierBiForkTestConstructor is Test, Utilities { CSVerifier verifier; Stub module; + address public admin; function setUp() public { module = new Stub(); + admin = nextAddress("ADMIN"); } function test_constructor_HappyPath() public { @@ -46,7 +48,8 @@ contract CSVerifierBiForkTestConstructor is Test, Utilities { gIHistoricalSummariesPrev: pack(0x3b, 0), gIHistoricalSummariesCurr: pack(0x3b, 0), firstSupportedSlot: Slot.wrap(8_192), - pivotSlot: Slot.wrap(950_272) + pivotSlot: Slot.wrap(950_272), + admin: admin }); assertEq( @@ -102,7 +105,8 @@ contract CSVerifierBiForkTestConstructor is Test, Utilities { gIHistoricalSummariesPrev: pack(0x3b, 0), gIHistoricalSummariesCurr: pack(0x3b, 0), firstSupportedSlot: Slot.wrap(8_192), - pivotSlot: Slot.wrap(950_272) + pivotSlot: Slot.wrap(950_272), + admin: admin }); } @@ -119,7 +123,8 @@ contract CSVerifierBiForkTestConstructor is Test, Utilities { gIHistoricalSummariesPrev: pack(0x3b, 0), gIHistoricalSummariesCurr: pack(0x3b, 0), firstSupportedSlot: Slot.wrap(8_192), - pivotSlot: Slot.wrap(950_272) + pivotSlot: Slot.wrap(950_272), + admin: admin }); } @@ -136,7 +141,8 @@ contract CSVerifierBiForkTestConstructor is Test, Utilities { gIHistoricalSummariesPrev: pack(0x3b, 0), gIHistoricalSummariesCurr: pack(0x3b, 0), firstSupportedSlot: Slot.wrap(8_192), - pivotSlot: Slot.wrap(950_272) + pivotSlot: Slot.wrap(950_272), + admin: admin }); } @@ -153,12 +159,13 @@ contract CSVerifierBiForkTestConstructor is Test, Utilities { gIHistoricalSummariesPrev: pack(0x3b, 0), gIHistoricalSummariesCurr: pack(0x3b, 0), firstSupportedSlot: Slot.wrap(200), - pivotSlot: Slot.wrap(100) + pivotSlot: Slot.wrap(100), + admin: admin }); } } -contract CSVerifierBiForkHistoricalTest is Test { +contract CSVerifierBiForkHistoricalTest is Test, Utilities { using stdJson for string; struct HistoricalWithdrawalFixture { @@ -171,11 +178,13 @@ contract CSVerifierBiForkHistoricalTest is Test { CSVerifier public verifier; Stub public module; + address public admin; HistoricalWithdrawalFixture public fixture; function setUp() public { module = new Stub(); + admin = nextAddress("ADMIN"); verifier = new CSVerifier({ withdrawalAddress: 0xb3E29C46Ee1745724417C0C51Eb2351A1C01cF36, module: address(module), @@ -187,7 +196,8 @@ contract CSVerifierBiForkHistoricalTest is Test { gIHistoricalSummariesPrev: pack(0x3b, 0), gIHistoricalSummariesCurr: pack(0x3b, 0), firstSupportedSlot: Slot.wrap(8_192), - pivotSlot: Slot.wrap(950_272) + pivotSlot: Slot.wrap(950_272), + admin: admin }); } diff --git a/test/fork/deployment/PostDeployment.t.sol b/test/fork/deployment/PostDeployment.t.sol index c8f313e7..cdc5a35d 100644 --- a/test/fork/deployment/PostDeployment.t.sol +++ b/test/fork/deployment/PostDeployment.t.sol @@ -501,4 +501,10 @@ contract CSVerifierDeploymentTest is Test, Utilities, DeploymentFixtures { deployParams.verifierSupportedEpoch * deployParams.slotsPerEpoch ); } + + function test_roles() public { + assertTrue(verifier.hasRole(verifier.PAUSE_ROLE(), address(gateSeal))); + assertEq(verifier.getRoleMemberCount(verifier.PAUSE_ROLE()), 1); + assertEq(verifier.getRoleMemberCount(verifier.RESUME_ROLE()), 0); + } } diff --git a/test/fork/integration/GateSeal.t.sol b/test/fork/integration/GateSeal.t.sol index bbf445e8..677ff2ea 100644 --- a/test/fork/integration/GateSeal.t.sol +++ b/test/fork/integration/GateSeal.t.sol @@ -23,10 +23,11 @@ contract GateSealTest is Test, Utilities, DeploymentFixtures { } function test_sealAll() public { - address[] memory sealables = new address[](3); + address[] memory sealables = new address[](4); sealables[0] = address(csm); sealables[1] = address(accounting); sealables[2] = address(oracle); + sealables[3] = address(verifier); vm.prank(gateSeal.get_sealing_committee()); gateSeal.seal(sealables); @@ -34,6 +35,7 @@ contract GateSealTest is Test, Utilities, DeploymentFixtures { assertTrue(csm.isPaused()); assertTrue(accounting.isPaused()); assertTrue(oracle.isPaused()); + assertTrue(verifier.isPaused()); } function test_sealCSM() public { @@ -44,6 +46,8 @@ contract GateSealTest is Test, Utilities, DeploymentFixtures { assertTrue(csm.isPaused()); assertFalse(accounting.isPaused()); + assertFalse(oracle.isPaused()); + assertFalse(verifier.isPaused()); } function test_sealAccounting() public { @@ -54,6 +58,8 @@ contract GateSealTest is Test, Utilities, DeploymentFixtures { assertTrue(accounting.isPaused()); assertFalse(csm.isPaused()); + assertFalse(oracle.isPaused()); + assertFalse(verifier.isPaused()); } function test_sealOracle() public { @@ -63,7 +69,20 @@ contract GateSealTest is Test, Utilities, DeploymentFixtures { gateSeal.seal(sealables); assertTrue(oracle.isPaused()); + assertFalse(csm.isPaused()); assertFalse(accounting.isPaused()); + assertFalse(verifier.isPaused()); + } + + function test_sealVerifier() public { + address[] memory sealables = new address[](1); + sealables[0] = address(verifier); + vm.prank(gateSeal.get_sealing_committee()); + gateSeal.seal(sealables); + + assertTrue(verifier.isPaused()); assertFalse(csm.isPaused()); + assertFalse(accounting.isPaused()); + assertFalse(oracle.isPaused()); } } diff --git a/test/fork/invariant/Invariants.t.sol b/test/fork/invariant/Invariants.t.sol index f325127f..1e094cb0 100644 --- a/test/fork/invariant/Invariants.t.sol +++ b/test/fork/invariant/Invariants.t.sol @@ -217,3 +217,23 @@ contract HashConsensusInvariant is InvariantsBase { ); } } + +contract VerifierInvariant is InvariantsBase { + function test_roles() public { + assertEq( + verifier.getRoleMemberCount(verifier.PAUSE_ROLE()), + 1, + "pause" + ); + assertEq( + verifier.getRoleMember(verifier.PAUSE_ROLE(), 0), + address(gateSeal), + "pause address" + ); + assertEq( + verifier.getRoleMemberCount(verifier.RESUME_ROLE()), + 0, + "resume" + ); + } +} diff --git a/test/fork/voting/StatePostVote.t.sol b/test/fork/voting/StatePostVote.t.sol index d0743e1a..271d204c 100644 --- a/test/fork/voting/StatePostVote.t.sol +++ b/test/fork/voting/StatePostVote.t.sol @@ -248,4 +248,10 @@ contract ContractsStateTest is Test, Utilities, DeploymentFixtures { 0 ); } + + function test_verifier_roles() public { + assertTrue(verifier.hasRole(verifier.PAUSE_ROLE(), address(gateSeal))); + assertEq(verifier.getRoleMemberCount(verifier.PAUSE_ROLE()), 1); + assertEq(verifier.getRoleMemberCount(verifier.RESUME_ROLE()), 0); + } } diff --git a/test/helpers/Fixtures.sol b/test/helpers/Fixtures.sol index fb51ef2b..c6a62bd8 100644 --- a/test/helpers/Fixtures.sol +++ b/test/helpers/Fixtures.sol @@ -111,6 +111,7 @@ contract DeploymentHelpers is Test { address verifier; address earlyAdoption; address hashConsensus; + address gateSeal; } function envVars() public returns (Env memory) { @@ -211,6 +212,7 @@ contract DeploymentHelpers is Test { config, ".HashConsensus" ); + upgradeConfig.gateSeal = vm.parseJsonAddress(config, ".GateSeal"); } function parseDeployParams( @@ -281,6 +283,7 @@ contract DeploymentFixtures is StdCheats, DeploymentHelpers { vettedGate = VettedGate(upgradeConfig.vettedGate); verifier = CSVerifier(upgradeConfig.verifier); hashConsensus = HashConsensus(upgradeConfig.hashConsensus); + gateSeal = IGateSeal(upgradeConfig.gateSeal); } }