diff --git a/contracts/0.8.9/BeaconChainDepositor.sol b/contracts/0.8.9/BeaconChainDepositor.sol index 73629c5c5..26cd9b2ca 100644 --- a/contracts/0.8.9/BeaconChainDepositor.sol +++ b/contracts/0.8.9/BeaconChainDepositor.sol @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Lido 2 +// SPDX-FileCopyrightText: 2022 Lido // SPDX-License-Identifier: GPL-3.0 // See contracts/COMPILERS.md diff --git a/contracts/0.8.9/StakingRouter.sol b/contracts/0.8.9/StakingRouter.sol index 903c12a33..7ad277046 100644 --- a/contracts/0.8.9/StakingRouter.sol +++ b/contracts/0.8.9/StakingRouter.sol @@ -22,10 +22,10 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe /// @dev events event StakingModuleAdded(uint24 indexed stakingModuleId, address stakingModule, string name, address createdBy); - event StakingModuleTargetShareSet(uint24 indexed stakingModuleId, uint16 targetShare); - event StakingModuleFeesSet(uint24 indexed stakingModuleId, uint16 treasuryFee, uint16 moduleFee); + event StakingModuleTargetShareSet(uint24 indexed stakingModuleId, uint16 targetShare, address setBy); + event StakingModuleFeesSet(uint24 indexed stakingModuleId, uint16 stakingModuleFee, uint16 treasuryFee, address setBy); event StakingModuleStatusSet(uint24 indexed stakingModuleId, StakingModuleStatus status, address setBy); - event WithdrawalCredentialsSet(bytes32 withdrawalCredentials); + event WithdrawalCredentialsSet(bytes32 withdrawalCredentials, address setBy); event ContractVersionSet(uint256 version); /** * Emitted when the StakingRouter received ETH @@ -45,7 +45,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe struct StakingModuleCache { address stakingModuleAddress; - uint16 moduleFee; + uint16 stakingModuleFee; uint16 treasuryFee; uint16 targetShare; StakingModuleStatus status; @@ -54,9 +54,9 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } bytes32 public constant MANAGE_WITHDRAWAL_CREDENTIALS_ROLE = keccak256("MANAGE_WITHDRAWAL_CREDENTIALS_ROLE"); - bytes32 public constant MODULE_PAUSE_ROLE = keccak256("MODULE_PAUSE_ROLE"); - bytes32 public constant MODULE_RESUME_ROLE = keccak256("MODULE_RESUME_ROLE"); - bytes32 public constant MODULE_MANAGE_ROLE = keccak256("MODULE_MANAGE_ROLE"); + bytes32 public constant STAKING_MODULE_PAUSE_ROLE = keccak256("STAKING_MODULE_PAUSE_ROLE"); + bytes32 public constant STAKING_MODULE_RESUME_ROLE = keccak256("STAKING_MODULE_RESUME_ROLE"); + bytes32 public constant STAKING_MODULE_MANAGE_ROLE = keccak256("STAKING_MODULE_MANAGE_ROLE"); /// Version of the initialized contract data /// NB: Contract versioning starts from 1. @@ -81,7 +81,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe /// index 0 means a value is not in the set. bytes32 internal constant STAKING_MODULE_INDICES_MAPPING_POSITION = keccak256("lido.StakingRouter.stakingModuleIndicesOneBased"); - uint256 internal constant FEE_PRECISION_POINTS = 10 ** 20; // 100 * 10 ** 18 + uint256 public constant FEE_PRECISION_POINTS = 10 ** 20; // 100 * 10 ** 18 uint256 public constant TOTAL_BASIS_POINTS = 10000; constructor(address _depositContract) BeaconChainDepositor(_depositContract) { @@ -93,6 +93,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe /** * @dev proxy initialization * @param _admin Lido DAO Aragon agent contract address + * @param _lido Lido address * @param _withdrawalCredentials Lido withdrawal vault contract address */ function initialize(address _admin, address _lido, bytes32 _withdrawalCredentials) external { @@ -105,7 +106,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe LIDO_POSITION.setStorageAddress(_lido); WITHDRAWAL_CREDENTIALS_POSITION.setStorageBytes32(_withdrawalCredentials); - emit WithdrawalCredentialsSet(_withdrawalCredentials); + emit WithdrawalCredentialsSet(_withdrawalCredentials, msg.sender); } /// @dev prohibit direct transfer to contract @@ -121,22 +122,22 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } /** - * @notice register a new module - * @param _name name of module + * @notice register a new staking module + * @param _name name of staking module * @param _stakingModuleAddress target percent of total keys in protocol, in BP * @param _targetShare target total stake share - * @param _moduleFee fee of the module taken from the consensus layer rewards + * @param _stakingModuleFee fee of the staking module taken from the consensus layer rewards * @param _treasuryFee treasury fee */ - function addModule( + function addStakingModule( string calldata _name, address _stakingModuleAddress, uint16 _targetShare, - uint16 _moduleFee, + uint16 _stakingModuleFee, uint16 _treasuryFee - ) external onlyRole(MODULE_MANAGE_ROLE) { + ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { if (_targetShare > TOTAL_BASIS_POINTS) revert ErrorValueOver100Percent("_targetShare"); - if (_moduleFee + _treasuryFee > TOTAL_BASIS_POINTS) revert ErrorValueOver100Percent("_moduleFee + _treasuryFee"); + if (_stakingModuleFee + _treasuryFee > TOTAL_BASIS_POINTS) revert ErrorValueOver100Percent("_stakingModuleFee + _treasuryFee"); uint256 newStakingModuleIndex = getStakingModulesCount(); StakingModule storage newStakingModule = _getStakingModuleByIndex(newStakingModuleIndex); @@ -146,8 +147,8 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe newStakingModule.name = _name; newStakingModule.stakingModuleAddress = _stakingModuleAddress; newStakingModule.targetShare = _targetShare; + newStakingModule.stakingModuleFee = _stakingModuleFee; newStakingModule.treasuryFee = _treasuryFee; - newStakingModule.moduleFee = _moduleFee; /// @dev since `enum` is `uint8` by nature, so the `status` is stored as `uint8` to avoid possible problems when upgrading. /// But for human readability, we use `enum` as function parameter type. /// More about conversion in the docs https://docs.soliditylang.org/en/v0.8.17/types.html#enums @@ -158,34 +159,45 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe STAKING_MODULES_COUNT_POSITION.setStorageUint256(newStakingModuleIndex + 1); emit StakingModuleAdded(newStakingModuleId, _stakingModuleAddress, _name, msg.sender); - emit StakingModuleTargetShareSet(newStakingModuleId, _targetShare); - emit StakingModuleFeesSet(newStakingModuleId, _treasuryFee, _moduleFee); + emit StakingModuleTargetShareSet(newStakingModuleId, _targetShare, msg.sender); + emit StakingModuleFeesSet(newStakingModuleId, _stakingModuleFee, _treasuryFee, msg.sender); } - + + /** + * @notice update staking module params + * @param _stakingModuleId staking module id + * @param _targetShare target total stake share + * @param _stakingModuleFee fee of the staking module taken from the consensus layer rewards + * @param _treasuryFee treasury fee + */ function updateStakingModule( uint24 _stakingModuleId, uint16 _targetShare, - uint16 _moduleFee, + uint16 _stakingModuleFee, uint16 _treasuryFee - ) external onlyRole(MODULE_MANAGE_ROLE) { + ) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { if (_targetShare > TOTAL_BASIS_POINTS) revert ErrorValueOver100Percent("_targetShare"); - if (_moduleFee + _treasuryFee > TOTAL_BASIS_POINTS) revert ErrorValueOver100Percent("_moduleFee + _treasuryFee"); + if (_stakingModuleFee + _treasuryFee > TOTAL_BASIS_POINTS) revert ErrorValueOver100Percent("_stakingModuleFee + _treasuryFee"); uint256 stakingModuleIndex = _getStakingModuleIndexById(_stakingModuleId); StakingModule storage stakingModule = _getStakingModuleByIndex(stakingModuleIndex); stakingModule.targetShare = _targetShare; stakingModule.treasuryFee = _treasuryFee; - stakingModule.moduleFee = _moduleFee; + stakingModule.stakingModuleFee = _stakingModuleFee; - emit StakingModuleTargetShareSet(_stakingModuleId, _targetShare); - emit StakingModuleFeesSet(_stakingModuleId, _treasuryFee, _moduleFee); + emit StakingModuleTargetShareSet(_stakingModuleId, _targetShare, msg.sender); + emit StakingModuleFeesSet(_stakingModuleId, _stakingModuleFee, _treasuryFee, msg.sender); } + + /** + * @notice Returns all registred staking modules + */ function getStakingModules() external view returns (StakingModule[] memory res) { - uint256 modulesCount = getStakingModulesCount(); - res = new StakingModule[](modulesCount); - for (uint256 i; i < modulesCount; ) { + uint256 stakingModulesCount = getStakingModulesCount(); + res = new StakingModule[](stakingModulesCount); + for (uint256 i; i < stakingModulesCount; ) { res[i] = _getStakingModuleByIndex(i); unchecked { ++i; @@ -214,24 +226,27 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe return _getStakingModuleByIndex(_stakingModuleIdndex); } + /** + * @dev Returns status of staking module + */ function getStakingModuleStatus(uint24 _stakingModuleId) public view returns (StakingModuleStatus) { return StakingModuleStatus(_getStakingModuleById(_stakingModuleId).status); } /** - * @notice set the module status flag for participation in further deposits and/or reward distribution + * @notice set the staking module status flag for participation in further deposits and/or reward distribution */ - function setStakingModuleStatus(uint24 _stakingModuleId, StakingModuleStatus _status) external onlyRole(MODULE_MANAGE_ROLE) { + function setStakingModuleStatus(uint24 _stakingModuleId, StakingModuleStatus _status) external onlyRole(STAKING_MODULE_MANAGE_ROLE) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); stakingModule.status = uint8(_status); emit StakingModuleStatusSet(_stakingModuleId, _status, msg.sender); } /** - * @notice pause deposits for module + * @notice pause deposits for staking module * @param _stakingModuleId id of the staking module to be paused */ - function pauseStakingModule(uint24 _stakingModuleId) external onlyRole(MODULE_PAUSE_ROLE) { + function pauseStakingModule(uint24 _stakingModuleId) external onlyRole(STAKING_MODULE_PAUSE_ROLE) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); StakingModuleStatus _prevStatus = StakingModuleStatus(stakingModule.status); if (_prevStatus != StakingModuleStatus.Active) revert ErrorStakingModuleNotActive(); @@ -240,10 +255,10 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } /** - * @notice resume deposits for module + * @notice resume deposits for staking module * @param _stakingModuleId id of the staking module to be unpaused */ - function resumeStakingModule(uint24 _stakingModuleId) external onlyRole(MODULE_RESUME_ROLE) { + function resumeStakingModule(uint24 _stakingModuleId) external onlyRole(STAKING_MODULE_RESUME_ROLE) { StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); StakingModuleStatus _prevStatus = StakingModuleStatus(stakingModule.status); if (_prevStatus != StakingModuleStatus.DepositsPaused) revert ErrorStakingModuleNotPaused(); @@ -268,8 +283,8 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } function getStakingModuleLastDepositBlock(uint24 _stakingModuleId) external view returns (uint256) { - StakingModule storage module = _getStakingModuleById(_stakingModuleId); - return module.lastDepositBlock; + StakingModule storage stakingModule = _getStakingModuleById(_stakingModuleId); + return stakingModule.lastDepositBlock; } function getStakingModuleActiveKeysCount(uint24 _stakingModuleId) external view returns (uint256 activeKeysCount) { @@ -277,7 +292,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } /** - * @dev calculate max count of depositable module keys based on the current Staking Router balance and buffered Ether amoutn + * @dev calculate max count of depositable staking module keys based on the current Staking Router balance and buffered Ether amoutn * * @param _stakingModuleId id of the staking module to be deposited * @return max depositable keys count @@ -288,9 +303,9 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } /** - * @dev calculate max count of depositable module keys based on the total expected number of deposits + * @dev calculate max count of depositable staking module keys based on the total expected number of deposits * - * @param _stakingModuleIndex module index + * @param _stakingModuleIndex staking module index * @param _keysToAllocate total number of deposits to be made * @return max depositable keys count */ @@ -298,57 +313,57 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe uint256 _stakingModuleIndex, uint256 _keysToAllocate ) internal view returns (uint256) { - (, uint256[] memory newKeysAllocation, StakingModuleCache[] memory modulesCache) = _getKeysAllocation(_keysToAllocate); - return newKeysAllocation[_stakingModuleIndex] - modulesCache[_stakingModuleIndex].activeKeysCount; + (, uint256[] memory newKeysAllocation, StakingModuleCache[] memory stakingModuleCache) = _getKeysAllocation(_keysToAllocate); + return newKeysAllocation[_stakingModuleIndex] - stakingModuleCache[_stakingModuleIndex].activeKeysCount; } /** * @notice return shares table * * @return recipients recipients list - * @return moduleFees fee of each recipient - * @return totalFee total fee to mint for each module and treasury + * @return stakingModuleFees fee of each recipient + * @return totalFee total fee to mint for each staking module and treasury */ function getStakingRewardsDistribution() external view - returns (address[] memory recipients, uint96[] memory moduleFees, uint96 totalFee, uint256 precisionPoints) + returns (address[] memory recipients, uint96[] memory stakingModuleFees, uint96 totalFee, uint256 precisionPoints) { - (uint256 totalActiveKeys, StakingModuleCache[] memory modulesCache) = _loadStakingModulesCache(); - uint256 modulesCount = modulesCache.length; + (uint256 totalActiveKeys, StakingModuleCache[] memory stakingModuleCache) = _loadStakingModulesCache(); + uint256 stakingModulesCount = stakingModuleCache.length; - /// @dev return empty response if there are no modules or active keys yet - if (modulesCount == 0 || totalActiveKeys == 0) { + /// @dev return empty response if there are no staking modules or active keys yet + if (stakingModulesCount == 0 || totalActiveKeys == 0) { return (new address[](0), new uint96[](0), 0, FEE_PRECISION_POINTS); } precisionPoints = FEE_PRECISION_POINTS; - recipients = new address[](modulesCount); - moduleFees = new uint96[](modulesCount); - - uint256 rewardedModulesCount = 0; - uint256 moduleKeysShare; - uint96 moduleFee; - - for (uint256 i; i < modulesCount; ) { - /// @dev skip modules which have no active keys - if (modulesCache[i].activeKeysCount > 0) { - moduleKeysShare = ((modulesCache[i].activeKeysCount * precisionPoints) / totalActiveKeys); - - recipients[i] = address(modulesCache[i].stakingModuleAddress); - moduleFee = uint96((moduleKeysShare * modulesCache[i].moduleFee) / TOTAL_BASIS_POINTS); - /// @dev if the module has the `Stopped` status for some reason, then the module's - /// rewards go to the treasure, so that the DAO has ability to manage them - /// (e.g. to compensate the module in case of an error, etc.) - if (modulesCache[i].status != StakingModuleStatus.Stopped) { - moduleFees[i] = moduleFee; + recipients = new address[](stakingModulesCount); + stakingModuleFees = new uint96[](stakingModulesCount); + + uint256 rewardedStakingModulesCount = 0; + uint256 stakingModuleKeysShare; + uint96 stakingModuleFee; + + for (uint256 i; i < stakingModulesCount; ) { + /// @dev skip staking modules which have no active keys + if (stakingModuleCache[i].activeKeysCount > 0) { + stakingModuleKeysShare = ((stakingModuleCache[i].activeKeysCount * precisionPoints) / totalActiveKeys); + + recipients[i] = address(stakingModuleCache[i].stakingModuleAddress); + stakingModuleFee = uint96((stakingModuleKeysShare * stakingModuleCache[i].stakingModuleFee) / TOTAL_BASIS_POINTS); + /// @dev if the staking module has the `Stopped` status for some reason, then + /// the staking module's rewards go to the treasure, so that the DAO has ability + /// to manage them (e.g. to compensate thestaking module in case of an error, etc.) + if (stakingModuleCache[i].status != StakingModuleStatus.Stopped) { + stakingModuleFees[i] = stakingModuleFee; } - // else keep moduleFees[i] = 0, but increase totalFee + // else keep stakingModuleFees[i] = 0, but increase totalFee - totalFee += (uint96((moduleKeysShare * modulesCache[i].treasuryFee) / TOTAL_BASIS_POINTS) + moduleFee); + totalFee += (uint96((stakingModuleKeysShare * stakingModuleCache[i].treasuryFee) / TOTAL_BASIS_POINTS) + stakingModuleFee); unchecked { - rewardedModulesCount++; + rewardedStakingModulesCount++; } } unchecked { @@ -360,11 +375,11 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe if (totalFee >= precisionPoints) revert ErrorValueOver100Percent("totalFee"); /// @dev shrink arrays - if (rewardedModulesCount < modulesCount) { - uint256 trim = modulesCount - rewardedModulesCount; + if (rewardedStakingModulesCount < stakingModulesCount) { + uint256 trim = stakingModulesCount - rewardedStakingModulesCount; assembly { mstore(recipients, sub(mload(recipients), trim)) - mstore(moduleFees, sub(mload(moduleFees), trim)) + mstore(stakingModuleFees, sub(mload(stakingModuleFees), trim)) } } } @@ -378,7 +393,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe * @dev Invokes a deposit call to the official Deposit contract * @param _maxDepositsCount max deposits count * @param _stakingModuleId id of the staking module to be deposited - * @param _depositCalldata module calldata + * @param _depositCalldata staking module calldata */ function deposit( uint256 _maxDepositsCount, @@ -443,7 +458,7 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe //trim keys with old WC _trimUnusedKeys(); - emit WithdrawalCredentialsSet(_withdrawalCredentials); + emit WithdrawalCredentialsSet(_withdrawalCredentials, msg.sender); } /** @@ -454,8 +469,8 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } function _trimUnusedKeys() internal { - uint256 modulesCount = getStakingModulesCount(); - for (uint256 i; i < modulesCount; ) { + uint256 stakingModulesCount = getStakingModulesCount(); + for (uint256 i; i < stakingModulesCount; ) { IStakingModule(_getStakingModuleAddressByIndex(i)).invalidateReadyToDepositKeys(); unchecked { ++i; @@ -466,26 +481,26 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe function _readStakingModuleCache(uint256 _stakingModuleIndex) internal view returns (StakingModuleCache memory stakingModuleCache) { StakingModule storage stakingModuleData = _getStakingModuleByIndex(_stakingModuleIndex); stakingModuleCache.stakingModuleAddress = stakingModuleData.stakingModuleAddress; - stakingModuleCache.moduleFee = stakingModuleData.moduleFee; + stakingModuleCache.stakingModuleFee = stakingModuleData.stakingModuleFee; stakingModuleCache.treasuryFee = stakingModuleData.treasuryFee; stakingModuleCache.targetShare = stakingModuleData.targetShare; stakingModuleCache.status = StakingModuleStatus(stakingModuleData.status); } /** - * @dev load all modules list + * @dev load all staking modules list * @notice used for reward distribution - * @return totalActiveKeys for not stopped modules - * @return modulesCache array of StakingModuleCache struct + * @return totalActiveKeys for not stopped staking modules + * @return stakingModuleCache array of StakingModuleCache struct */ - function _loadStakingModulesCache() internal view returns (uint256 totalActiveKeys, StakingModuleCache[] memory modulesCache) { - uint256 modulesCount = getStakingModulesCount(); - modulesCache = new StakingModuleCache[](modulesCount); - for (uint256 i; i < modulesCount; ) { - modulesCache[i] = _readStakingModuleCache(i); - (, modulesCache[i].activeKeysCount, modulesCache[i].availableKeysCount) = IStakingModule(modulesCache[i].stakingModuleAddress) + function _loadStakingModulesCache() internal view returns (uint256 totalActiveKeys, StakingModuleCache[] memory stakingModuleCache) { + uint256 stakingModulesCount = getStakingModulesCount(); + stakingModuleCache = new StakingModuleCache[](stakingModulesCount); + for (uint256 i; i < stakingModulesCount; ) { + stakingModuleCache[i] = _readStakingModuleCache(i); + (, stakingModuleCache[i].activeKeysCount, stakingModuleCache[i].availableKeysCount) = IStakingModule(stakingModuleCache[i].stakingModuleAddress) .getValidatorsKeysStats(); - totalActiveKeys += modulesCache[i].activeKeysCount; + totalActiveKeys += stakingModuleCache[i].activeKeysCount; unchecked { ++i; } @@ -493,24 +508,24 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe } /** - * @dev load active modules list + * @dev load active staking modules list * @notice used for deposits allocation - * @return totalActiveKeys for active modules - * @return modulesCache array of StakingModuleCache struct + * @return totalActiveKeys for active staking modules + * @return stakingModuleCache array of StakingModuleCache struct */ - function _loadActiveStakingModulesCache() internal view returns (uint256 totalActiveKeys, StakingModuleCache[] memory modulesCache) { - uint256 modulesCount = getStakingModulesCount(); - modulesCache = new StakingModuleCache[](modulesCount); + function _loadActiveStakingModulesCache() internal view returns (uint256 totalActiveKeys, StakingModuleCache[] memory stakingModuleCache) { + uint256 stakingModulesCount = getStakingModulesCount(); + stakingModuleCache = new StakingModuleCache[](stakingModulesCount); - for (uint256 i; i < modulesCount; ) { - modulesCache[i] = _readStakingModuleCache(i); + for (uint256 i; i < stakingModulesCount; ) { + stakingModuleCache[i] = _readStakingModuleCache(i); - /// @dev account only keys from active modules - if (modulesCache[i].status == StakingModuleStatus.Active) { - (, modulesCache[i].activeKeysCount, modulesCache[i].availableKeysCount) = IStakingModule( - modulesCache[i].stakingModuleAddress + /// @dev account only keys from active staking modules + if (stakingModuleCache[i].status == StakingModuleStatus.Active) { + (, stakingModuleCache[i].activeKeysCount, stakingModuleCache[i].availableKeysCount) = IStakingModule( + stakingModuleCache[i].stakingModuleAddress ).getValidatorsKeysStats(); - totalActiveKeys += modulesCache[i].activeKeysCount; + totalActiveKeys += stakingModuleCache[i].activeKeysCount; } unchecked { ++i; @@ -520,24 +535,24 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe function _getKeysAllocation( uint256 _keysToAllocate - ) internal view returns (uint256 allocated, uint256[] memory allocations, StakingModuleCache[] memory modulesCache) { + ) internal view returns (uint256 allocated, uint256[] memory allocations, StakingModuleCache[] memory stakingModuleCache) { // calculate total used keys for operators uint256 totalActiveKeys; - (totalActiveKeys, modulesCache) = _loadActiveStakingModulesCache(); + (totalActiveKeys, stakingModuleCache) = _loadActiveStakingModulesCache(); - uint256 modulesCount = modulesCache.length; - allocations = new uint256[](modulesCount); - if (modulesCount > 0) { + uint256 stakingModulesCount = stakingModuleCache.length; + allocations = new uint256[](stakingModulesCount); + if (stakingModulesCount > 0) { /// @dev new estimated active keys count totalActiveKeys += _keysToAllocate; - uint256[] memory capacities = new uint256[](modulesCount); + uint256[] memory capacities = new uint256[](stakingModulesCount); uint256 targetKeys; - for (uint256 i; i < modulesCount; ) { - allocations[i] = modulesCache[i].activeKeysCount; - targetKeys = (modulesCache[i].targetShare * totalActiveKeys) / TOTAL_BASIS_POINTS; - capacities[i] = Math.min(targetKeys, modulesCache[i].activeKeysCount + modulesCache[i].availableKeysCount); + for (uint256 i; i < stakingModulesCount; ) { + allocations[i] = stakingModuleCache[i].activeKeysCount; + targetKeys = (stakingModuleCache[i].targetShare * totalActiveKeys) / TOTAL_BASIS_POINTS; + capacities[i] = Math.min(targetKeys, stakingModuleCache[i].activeKeysCount + stakingModuleCache[i].availableKeysCount); unchecked { ++i; } @@ -589,6 +604,11 @@ contract StakingRouter is IStakingRouter, AccessControlEnumerable, BeaconChainDe emit ContractVersionSet(version); } + /// @notice Return the initialized version of this contract starting from 0 + function getVersion() external view returns (uint256) { + return CONTRACT_VERSION_POSITION.getStorageUint256(); + } + function _getStorageStakingModulesMapping(bytes32 position) internal pure returns (mapping(uint256 => StakingModule) storage result) { assembly { result.slot := position diff --git a/contracts/0.8.9/interfaces/IStakingRouter.sol b/contracts/0.8.9/interfaces/IStakingRouter.sol index dc0a2e549..e5a6971d4 100644 --- a/contracts/0.8.9/interfaces/IStakingRouter.sol +++ b/contracts/0.8.9/interfaces/IStakingRouter.sol @@ -9,7 +9,7 @@ interface IStakingRouter { function getStakingRewardsDistribution() external view - returns (address[] memory recipients, uint96[] memory moduleFees, uint96 totalFeee, uint256 precisionPoints); + returns (address[] memory recipients, uint96[] memory stakingModuleFees, uint96 totalFeee, uint256 precisionPoints); function deposit(uint256 maxDepositsCount, uint24 stakingModuleId, bytes calldata depositCalldata) external payable returns (uint256); @@ -32,37 +32,37 @@ interface IStakingRouter { } struct StakingModule { - /// @notice unique id of the module + /// @notice unique id of the staking module uint24 id; - /// @notice address of module + /// @notice address of staking module address stakingModuleAddress; - /// @notice rewarf fee of the module - uint16 moduleFee; + /// @notice rewarf fee of the staking module + uint16 stakingModuleFee; /// @notice treasury fee uint16 treasuryFee; /// @notice target percent of total keys in protocol, in BP uint16 targetShare; - /// @notice module status if module can not accept the deposits or can participate in further reward distribution + /// @notice staking module status if staking module can not accept the deposits or can participate in further reward distribution uint8 status; - /// @notice name of module + /// @notice name of staking module string name; - /// @notice block.timestamp of the last deposit of the module + /// @notice block.timestamp of the last deposit of the staking module uint64 lastDepositAt; - /// @notice block.number of the last deposit of the module + /// @notice block.number of the last deposit of the staking module uint256 lastDepositBlock; } function getStakingModules() external view returns (StakingModule[] memory res); - function addModule( + function addStakingModule( string memory _name, address _stakingModuleAddress, uint16 _targetShare, - uint16 _moduleFee, + uint16 _stakingModuleFee, uint16 _treasuryFee ) external; - function updateStakingModule(uint24 _stakingModuleId, uint16 _targetShare, uint16 _moduleFee, uint16 _treasuryFee) external; + function updateStakingModule(uint24 _stakingModuleId, uint16 _targetShare, uint16 _stakingModuleFee, uint16 _treasuryFee) external; function getStakingModule(uint24 _stakingModuleId) external view returns (StakingModule memory); diff --git a/contracts/0.8.9/test_helpers/StakingModuleMock.sol b/contracts/0.8.9/test_helpers/StakingModuleMock.sol index 56f1e48d6..69948e6e6 100644 --- a/contracts/0.8.9/test_helpers/StakingModuleMock.sol +++ b/contracts/0.8.9/test_helpers/StakingModuleMock.sol @@ -10,6 +10,7 @@ import {IStakingModule} from "../interfaces/IStakingModule.sol"; contract StakingModuleMock is IStakingModule { uint256 private _activeKeysCount; uint256 private _availableKeysCount; + uint256 private _keysNonce; function getActiveKeysCount() public view returns (uint256) { return _activeKeysCount; @@ -42,7 +43,13 @@ contract StakingModuleMock is IStakingModule { readyToDepositValidatorsKeysCount = _availableKeysCount; } - function getValidatorsKeysNonce() external view returns (uint256) {} + function getValidatorsKeysNonce() external view returns (uint256) { + return _keysNonce; + } + + function setValidatorsKeysNonce(uint256 _newKeysNonce) external { + _keysNonce = _newKeysNonce; + } function getNodeOperatorsCount() external view returns (uint256) {} @@ -62,7 +69,9 @@ contract StakingModuleMock is IStakingModule { function updateExitedValidatorsKeysCount(uint256 _nodeOperatorId, uint256 _exitedValidatorsCount) external {} - function invalidateReadyToDepositKeys() external {} + function invalidateReadyToDepositKeys() external { + _availableKeysCount = _activeKeysCount; + } function requestValidatorsKeysForDeposits(uint256 _keysCount, bytes calldata _calldata) external diff --git a/contracts/0.8.9/test_helpers/StakingRouterMock.sol b/contracts/0.8.9/test_helpers/StakingRouterMock.sol index 50dd710d0..24982b5aa 100644 --- a/contracts/0.8.9/test_helpers/StakingRouterMock.sol +++ b/contracts/0.8.9/test_helpers/StakingRouterMock.sol @@ -12,5 +12,9 @@ contract StakingRouterMock is StakingRouter { // unlock impl _setContractVersion(0); } + + function getStakingModuleIndexById(uint24 _stakingModuleId) external view returns (uint256) { + return _getStakingModuleIndexById(_stakingModuleId); + } } \ No newline at end of file diff --git a/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol b/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol index 81f8e50cc..5f5b57ca5 100644 --- a/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol +++ b/contracts/0.8.9/test_helpers/StakingRouterMockForDepositSecurityModule.sol @@ -17,7 +17,7 @@ contract StakingRouterMockForDepositSecurityModule is IStakingRouter { function getStakingModules() external view returns (StakingModule[] memory res) {} - function addModule( + function addStakingModule( string memory _name, address _stakingModuleAddress, uint16 _targetShare, diff --git a/test/0.4.24/lido.rewards-distribution.test.js b/test/0.4.24/lido.rewards-distribution.test.js index 801eef67a..6a64b55a4 100644 --- a/test/0.4.24/lido.rewards-distribution.test.js +++ b/test/0.4.24/lido.rewards-distribution.test.js @@ -100,25 +100,39 @@ contract('Lido', ([appManager, voting, user2]) => { await stakingRouter.initialize(appManager, app.address, wc) // Set up the staking router permissions. - const MODULE_MANAGE_ROLE = await stakingRouter.MODULE_MANAGE_ROLE() + const STAKING_MODULE_MANAGE_ROLE = await stakingRouter.STAKING_MODULE_MANAGE_ROLE() - await stakingRouter.grantRole(MODULE_MANAGE_ROLE, voting, { from: appManager }) + await stakingRouter.grantRole(STAKING_MODULE_MANAGE_ROLE, voting, { from: appManager }) await app.setStakingRouter(stakingRouter.address, { from: voting }) soloModule = await ModuleSolo.new(app.address, { from: appManager }) - await stakingRouter.addModule('Curated', curatedModule.address, cfgCurated.targetShare, cfgCurated.moduleFee, cfgCurated.treasuryFee, { - from: voting - }) + await stakingRouter.addStakingModule( + 'Curated', + curatedModule.address, + cfgCurated.targetShare, + cfgCurated.moduleFee, + cfgCurated.treasuryFee, + { + from: voting + } + ) await curatedModule.increaseTotalSigningKeysCount(500_000, { from: appManager }) await curatedModule.increaseDepositedSigningKeysCount(499_950, { from: appManager }) await curatedModule.increaseVettedSigningKeysCount(499_950, { from: appManager }) - await stakingRouter.addModule('Solo', soloModule.address, cfgCommunity.targetShare, cfgCommunity.moduleFee, cfgCommunity.treasuryFee, { - from: voting - }) + await stakingRouter.addStakingModule( + 'Solo', + soloModule.address, + cfgCommunity.targetShare, + cfgCommunity.moduleFee, + cfgCommunity.treasuryFee, + { + from: voting + } + ) await soloModule.setTotalKeys(100, { from: appManager }) await soloModule.setTotalUsedKeys(10, { from: appManager }) await soloModule.setTotalStoppedKeys(0, { from: appManager }) @@ -126,8 +140,8 @@ contract('Lido', ([appManager, voting, user2]) => { it('Rewards distribution fills treasury', async () => { const beaconBalance = ETH(1) - const { moduleFees, totalFee, precisionPoints } = await stakingRouter.getStakingRewardsDistribution() - const treasuryShare = moduleFees.reduce((total, share) => total.sub(share), totalFee) + const { stakingModuleFees, totalFee, precisionPoints } = await stakingRouter.getStakingRewardsDistribution() + const treasuryShare = stakingModuleFees.reduce((total, share) => total.sub(share), totalFee) const treasuryRewards = bn(beaconBalance).mul(treasuryShare).div(precisionPoints) await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(32) }) @@ -141,7 +155,7 @@ contract('Lido', ([appManager, voting, user2]) => { it('Rewards distribution fills modules', async () => { const beaconBalance = ETH(1) - const { recipients, moduleFees, precisionPoints } = await stakingRouter.getStakingRewardsDistribution() + const { recipients, stakingModuleFees, precisionPoints } = await stakingRouter.getStakingRewardsDistribution() await app.submit(ZERO_ADDRESS, { from: user2, value: ETH(32) }) @@ -154,7 +168,7 @@ contract('Lido', ([appManager, voting, user2]) => { for (let i = 0; i < recipients.length; i++) { const moduleBalanceAfter = await app.balanceOf(recipients[i]) - const moduleRewards = bn(beaconBalance).mul(moduleFees[i]).div(precisionPoints) + const moduleRewards = bn(beaconBalance).mul(stakingModuleFees[i]).div(precisionPoints) assert(moduleBalanceAfter.gt(moduleBalanceBefore[i])) assertBn(fixRound(moduleBalanceBefore[i].add(moduleRewards)), fixRound(moduleBalanceAfter)) } diff --git a/test/0.4.24/lido.test.js b/test/0.4.24/lido.test.js index 4853792cc..903a50004 100644 --- a/test/0.4.24/lido.test.js +++ b/test/0.4.24/lido.test.js @@ -1,5 +1,4 @@ const { hash } = require('eth-ens-namehash') -const { assert } = require('chai') const { newDao, newApp } = require('./helpers/dao') const { getInstalledApp } = require('@aragon/contract-helpers-test/src/aragon-os') const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') @@ -7,6 +6,7 @@ const { ZERO_ADDRESS, bn, getEventAt } = require('@aragon/contract-helpers-test' const { BN } = require('bn.js') const { formatEther } = require('ethers/lib/utils') const { getEthBalance, formatStEth, formatBN, hexConcat, pad, ETH, tokens } = require('../helpers/utils') +const { assert } = require('../helpers/assert') const nodeOperators = require('../helpers/node-operators') const NodeOperatorsRegistry = artifacts.require('NodeOperatorsRegistry') @@ -79,7 +79,7 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor, t stakingRouter = await StakingRouter.new(depositContract.address) await stakingRouter.initialize(appManager, app.address, ZERO_ADDRESS) await stakingRouter.grantRole(await stakingRouter.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), voting, { from: appManager }) - await stakingRouter.grantRole(await stakingRouter.MODULE_MANAGE_ROLE(), voting, { from: appManager }) + await stakingRouter.grantRole(await stakingRouter.STAKING_MODULE_MANAGE_ROLE(), voting, { from: appManager }) // BeaconChainDepositor beaconChainDepositor = await BeaconChainDepositorMock.new(depositContract.address) @@ -130,7 +130,7 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor, t // Initialize the app's proxy. await app.initialize(oracle.address, treasury, stakingRouter.address, depositor) - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Curated', operators.address, 10_000, // 100 % _targetShare @@ -348,37 +348,37 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor, t let module1 = await stakingRouter.getStakingModule(curated.id) assertBn(module1.targetShare, 10000) - assertBn(module1.moduleFee, 500) + assertBn(module1.stakingModuleFee, 500) assertBn(module1.treasuryFee, 500) - // stakingModuleId, targetShare, moduleFee, treasuryFee + // stakingModuleId, targetShare, stakingModuleFee, treasuryFee await stakingRouter.updateStakingModule(module1.id, module1.targetShare, 300, 700, { from: voting }) module1 = await stakingRouter.getStakingModule(curated.id) await assertRevert( stakingRouter.updateStakingModule(module1.id, module1.targetShare, 300, 700, { from: user1 }), - `AccessControl: account ${user1.toLowerCase()} is missing role ${await stakingRouter.MODULE_MANAGE_ROLE()}` + `AccessControl: account ${user1.toLowerCase()} is missing role ${await stakingRouter.STAKING_MODULE_MANAGE_ROLE()}` ) await assertRevert( stakingRouter.updateStakingModule(module1.id, module1.targetShare, 300, 700, { from: nobody }), - `AccessControl: account ${nobody.toLowerCase()} is missing role ${await stakingRouter.MODULE_MANAGE_ROLE()}` + `AccessControl: account ${nobody.toLowerCase()} is missing role ${await stakingRouter.STAKING_MODULE_MANAGE_ROLE()}` ) - await assertRevert( + await assert.revertsWithCustomError( stakingRouter.updateStakingModule(module1.id, 10001, 300, 700, { from: voting }), - `ed with custom error 'ErrorValueOver100Percent("_targetShare")` + 'ErrorValueOver100Percent("_targetShare")' ) - await assertRevert( + await assert.revertsWithCustomError( stakingRouter.updateStakingModule(module1.id, 10000, 10001, 700, { from: voting }), - `ed with custom error 'ErrorValueOver100Percent("_moduleFee + _treasuryFee")` + 'ErrorValueOver100Percent("_stakingModuleFee + _treasuryFee")' ) - await assertRevert( + await assert.revertsWithCustomError( stakingRouter.updateStakingModule(module1.id, 10000, 300, 10001, { from: voting }), - `ed with custom error 'ErrorValueOver100Percent("_moduleFee + _treasuryFee")` + 'ErrorValueOver100Percent("_stakingModuleFee + _treasuryFee")' ) // distribution fee calculates on active keys in modules @@ -516,9 +516,9 @@ contract('Lido', ([appManager, voting, user1, user2, user3, nobody, depositor, t await operators.addSigningKeys(0, 1, pad('0x010203', 48), pad('0x01', 96), { from: voting }) // can not deposit with unset withdrawalCredentials - await assertRevert( + await assert.revertsWithCustomError( app.methods['deposit(uint256,uint24,bytes)'](MAX_DEPOSITS, CURATED_MODULE_ID, CALLDATA, { from: depositor }), - `ed with custom error 'ErrorEmptyWithdrawalsCredentials()` + 'ErrorEmptyWithdrawalsCredentials()' ) // set withdrawalCredentials with keys, because they were trimmed await stakingRouter.setWithdrawalCredentials(pad('0x0202', 32), { from: voting }) diff --git a/test/0.8.9/staking-router-deposits-allocation.test.js b/test/0.8.9/staking-router-deposits-allocation.test.js index 488dff88f..6e57d25f3 100644 --- a/test/0.8.9/staking-router-deposits-allocation.test.js +++ b/test/0.8.9/staking-router-deposits-allocation.test.js @@ -8,13 +8,13 @@ const DepositContractMock = artifacts.require('DepositContractMock.sol') contract('StakingRouter', (accounts) => { let evmSnapshotId let depositContract, stakingRouter - let curatedStakingModuleMock, soloStakingModuleMock, dvtStakingModuleMock + let curatedStakingModuleMock, soloStakingModuleMock const [deployer, lido, admin] = accounts before(async () => { depositContract = await DepositContractMock.new({ from: deployer }) stakingRouter = await StakingRouter.new(depositContract.address, { from: deployer }) - ;[curatedStakingModuleMock, soloStakingModuleMock, dvtStakingModuleMock] = await Promise.all([ + ;[curatedStakingModuleMock, soloStakingModuleMock] = await Promise.all([ StakingModuleMock.new({ from: deployer }), StakingModuleMock.new({ from: deployer }), StakingModuleMock.new({ from: deployer }) @@ -24,15 +24,15 @@ contract('StakingRouter', (accounts) => { await stakingRouter.initialize(admin, lido, wc, { from: deployer }) // Set up the staking router permissions. - const [MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, MODULE_PAUSE_ROLE, MODULE_MANAGE_ROLE] = await Promise.all([ + const [MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, STAKING_MODULE_PAUSE_ROLE, STAKING_MODULE_MANAGE_ROLE] = await Promise.all([ stakingRouter.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), - stakingRouter.MODULE_PAUSE_ROLE(), - stakingRouter.MODULE_MANAGE_ROLE() + stakingRouter.STAKING_MODULE_PAUSE_ROLE(), + stakingRouter.STAKING_MODULE_MANAGE_ROLE() ]) await stakingRouter.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, admin, { from: admin }) - await stakingRouter.grantRole(MODULE_PAUSE_ROLE, admin, { from: admin }) - await stakingRouter.grantRole(MODULE_MANAGE_ROLE, admin, { from: admin }) + await stakingRouter.grantRole(STAKING_MODULE_PAUSE_ROLE, admin, { from: admin }) + await stakingRouter.grantRole(STAKING_MODULE_MANAGE_ROLE, admin, { from: admin }) evmSnapshotId = await hre.ethers.provider.send('evm_snapshot', []) }) @@ -44,7 +44,7 @@ contract('StakingRouter', (accounts) => { describe('One staking module', () => { beforeEach(async () => { - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Curated', curatedStakingModuleMock.address, 10_000, // target share 100 % @@ -90,7 +90,7 @@ contract('StakingRouter', (accounts) => { describe('Two staking modules', () => { beforeEach(async () => { - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Curated', curatedStakingModuleMock.address, 10_000, // 100 % _targetShare @@ -98,7 +98,7 @@ contract('StakingRouter', (accounts) => { 5_000, // 50 % _treasuryFee { from: admin } ) - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Solo', soloStakingModuleMock.address, 200, // 2 % _targetShare @@ -134,7 +134,7 @@ contract('StakingRouter', (accounts) => { describe('Make deposit', () => { beforeEach(async () => { - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Curated', curatedStakingModuleMock.address, 10_000, // 100 % _targetShare @@ -142,7 +142,7 @@ contract('StakingRouter', (accounts) => { 5_000, // 50 % _treasuryFee { from: admin } ) - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Solo', soloStakingModuleMock.address, 200, // 2 % _targetShare diff --git a/test/0.8.9/staking-router-deposits.test.js b/test/0.8.9/staking-router-deposits.test.js index fedb1f55e..38cf4a13d 100644 --- a/test/0.8.9/staking-router-deposits.test.js +++ b/test/0.8.9/staking-router-deposits.test.js @@ -2,6 +2,7 @@ const hre = require('hardhat') const { assertBn, assertRevert, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') const { newDao, newApp } = require('../0.4.24/helpers/dao') const { ETH, genKeys } = require('../helpers/utils') +const { assert } = require('../helpers/assert') const LidoMock = artifacts.require('LidoMock.sol') const LidoOracleMock = artifacts.require('OracleMock.sol') @@ -100,15 +101,15 @@ contract('StakingRouter', (accounts) => { await stakingRouter.initialize(admin, lido.address, wc, { from: deployer }) // Set up the staking router permissions. - const [MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, MODULE_PAUSE_ROLE, MODULE_MANAGE_ROLE] = await Promise.all([ + const [MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, STAKING_MODULE_PAUSE_ROLE, STAKING_MODULE_MANAGE_ROLE] = await Promise.all([ stakingRouter.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), - stakingRouter.MODULE_PAUSE_ROLE(), - stakingRouter.MODULE_MANAGE_ROLE() + stakingRouter.STAKING_MODULE_PAUSE_ROLE(), + stakingRouter.STAKING_MODULE_MANAGE_ROLE() ]) await stakingRouter.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, voting, { from: admin }) - await stakingRouter.grantRole(MODULE_PAUSE_ROLE, voting, { from: admin }) - await stakingRouter.grantRole(MODULE_MANAGE_ROLE, voting, { from: admin }) + await stakingRouter.grantRole(STAKING_MODULE_PAUSE_ROLE, voting, { from: admin }) + await stakingRouter.grantRole(STAKING_MODULE_MANAGE_ROLE, voting, { from: admin }) await lido.initialize(oracle.address, treasury, stakingRouter.address, depositSecurityModule.address) @@ -122,7 +123,7 @@ contract('StakingRouter', (accounts) => { describe('Make deposit', () => { beforeEach(async () => { - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Curated', operators.address, 10_000, // 100 % _targetShare @@ -130,7 +131,7 @@ contract('StakingRouter', (accounts) => { 5_000, // 50 % _treasuryFee { from: voting } ) - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Community', soloStakingModuleMock.address, 200, // 2 % _targetShare @@ -153,9 +154,9 @@ contract('StakingRouter', (accounts) => { // assertRevert(stakingRouter.deposit(maxDepositsCount, curated.id, '0x', {'from': voting }), 'APP_AUTH_DSM_FAILED') - assertRevert( + assert.revertsWithCustomError( lido.deposit(maxDepositsCount, curated.id, '0x', { from: depositSecurityModule.address }), - "ed with custom error 'ErrorZeroMaxSigningKeysCount()" + 'ErrorZeroMaxSigningKeysCount()' ) }) diff --git a/test/0.8.9/staking-router.test.js b/test/0.8.9/staking-router.test.js new file mode 100644 index 000000000..ded0c777b --- /dev/null +++ b/test/0.8.9/staking-router.test.js @@ -0,0 +1,623 @@ +const hre = require('hardhat') +const { MaxUint256 } = require('@ethersproject/constants') +const { utils } = require('web3') +const { BN } = require('bn.js') +const { assert } = require('../helpers/assert') +const { EvmSnapshot } = require('../helpers/blockchain') +const { artifacts } = require('hardhat') + +const DepositContractMock = artifacts.require('DepositContractMock') +const StakingRouterMock = artifacts.require('StakingRouterMock.sol') +const StakingRouter = artifacts.require('StakingRouter.sol') +const StakingModuleMock = artifacts.require('StakingModuleMock.sol') + +const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' +const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000' +const MANAGE_WITHDRAWAL_CREDENTIALS_ROLE = utils.soliditySha3('MANAGE_WITHDRAWAL_CREDENTIALS_ROLE') +const STAKING_MODULE_PAUSE_ROLE = utils.soliditySha3('STAKING_MODULE_PAUSE_ROLE') +const STAKING_MODULE_RESUME_ROLE = utils.soliditySha3('STAKING_MODULE_RESUME_ROLE') +const STAKING_MODULE_MANAGE_ROLE = utils.soliditySha3('STAKING_MODULE_MANAGE_ROLE') + +const StakingModuleStatus = { + Active: 0, // deposits and rewards allowed + DepositsPaused: 1, // deposits NOT allowed, rewards allowed + Stopped: 2 // deposits and rewards NOT allowed +} + +contract('StakingRouter', (accounts) => { + let depositContract, app + const [deployer, lido, admin, appManager, stranger] = accounts + const wc = '0x'.padEnd(66, '1234') + const snapshot = new EvmSnapshot(hre.ethers.provider) + + describe('setup env', async () => { + before(async () => { + depositContract = await DepositContractMock.new({ from: deployer }) + app = await StakingRouterMock.new(depositContract.address, { from: deployer }) + }) + + it('init fails on wrong input', async () => { + await assert.revertsWithCustomError(app.initialize(ZERO_ADDRESS, lido, wc, { from: deployer }), 'ErrorZeroAddress("_admin")') + await assert.revertsWithCustomError(app.initialize(admin, ZERO_ADDRESS, wc, { from: deployer }), 'ErrorZeroAddress("_lido")') + }) + + it('initialized correctly', async () => { + const tx = await app.initialize(admin, lido, wc, { from: deployer }) + + assert.equals(await app.getVersion(), 1) + assert.equals(await app.getWithdrawalCredentials(), wc) + assert.equals(await app.getLido(), lido) + assert.equals(await app.getStakingModulesCount(), 0) + + assert.equals(await app.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 1) + assert.equals(await app.hasRole(DEFAULT_ADMIN_ROLE, admin), true) + + assert.equals(tx.logs.length, 3) + + await assert.emits(tx, 'ContractVersionSet', { version: 1 }) + await assert.emits(tx, 'RoleGranted', { role: DEFAULT_ADMIN_ROLE, account: admin, sender: deployer }) + await assert.emits(tx, 'WithdrawalCredentialsSet', { withdrawalCredentials: wc }) + }) + + it('second initialize reverts', async () => { + await assert.revertsWithCustomError(app.initialize(admin, lido, wc, { from: deployer }), 'ErrorBaseVersion()') + }) + + it('stranger is not allowed to grant roles', async () => { + await assert.reverts( + app.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: stranger }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}` + ) + }) + + it('grant role MANAGE_WITHDRAWAL_CREDENTIALS_ROLE', async () => { + const tx = await app.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: admin }) + assert.equals(await app.getRoleMemberCount(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE), 1) + assert.equals(await app.hasRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, account: appManager, sender: admin }) + }) + + it('grant role STAKING_MODULE_PAUSE_ROLE', async () => { + const tx = await app.grantRole(STAKING_MODULE_PAUSE_ROLE, appManager, { from: admin }) + assert.equals(await app.getRoleMemberCount(STAKING_MODULE_PAUSE_ROLE), 1) + assert.equals(await app.hasRole(STAKING_MODULE_PAUSE_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_PAUSE_ROLE, account: appManager, sender: admin }) + }) + + it('grant role STAKING_MODULE_RESUME_ROLE', async () => { + const tx = await app.grantRole(STAKING_MODULE_RESUME_ROLE, appManager, { from: admin }) + assert.equals(await app.getRoleMemberCount(STAKING_MODULE_RESUME_ROLE), 1) + assert.equals(await app.hasRole(STAKING_MODULE_RESUME_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_RESUME_ROLE, account: appManager, sender: admin }) + }) + + it('grant role STAKING_MODULE_MANAGE_ROLE', async () => { + const tx = await app.grantRole(STAKING_MODULE_MANAGE_ROLE, appManager, { from: admin }) + assert.equals(await app.getRoleMemberCount(STAKING_MODULE_MANAGE_ROLE), 1) + assert.equals(await app.hasRole(STAKING_MODULE_MANAGE_ROLE, appManager), true) + + assert.equals(tx.logs.length, 1) + await assert.emits(tx, 'RoleGranted', { role: STAKING_MODULE_MANAGE_ROLE, account: appManager, sender: admin }) + }) + + it('public constants', async () => { + assert.equals(await app.FEE_PRECISION_POINTS(), new BN('100000000000000000000')) + assert.equals(await app.TOTAL_BASIS_POINTS(), 10000) + assert.equals(await app.DEPOSIT_CONTRACT(), depositContract.address) + assert.equals(await app.DEFAULT_ADMIN_ROLE(), DEFAULT_ADMIN_ROLE) + assert.equals(await app.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), MANAGE_WITHDRAWAL_CREDENTIALS_ROLE) + assert.equals(await app.STAKING_MODULE_PAUSE_ROLE(), STAKING_MODULE_PAUSE_ROLE) + assert.equals(await app.STAKING_MODULE_RESUME_ROLE(), STAKING_MODULE_RESUME_ROLE) + assert.equals(await app.STAKING_MODULE_MANAGE_ROLE(), STAKING_MODULE_MANAGE_ROLE) + }) + + it('getKeysAllocation', async () => { + const keysAllocation = await app.getKeysAllocation(1000) + + assert.equals(keysAllocation.allocated, 0) + assert.equals(keysAllocation.allocations, []) + }) + }) + + describe('implementation', async () => { + let stakingRouterImplementation + + before(async () => { + await snapshot.add() + stakingRouterImplementation = await StakingRouter.new(depositContract.address, { from: deployer }) + }) + + after(async () => { + await snapshot.revert() + }) + + it('contract version is max uint256', async () => { + assert.equals(await stakingRouterImplementation.getVersion(), MaxUint256) + }) + + it('initialize reverts on implementation', async () => { + await assert.revertsWithCustomError(stakingRouterImplementation.initialize(admin, lido, wc, { from: deployer }), `ErrorBaseVersion()`) + }) + + it('has no granted roles', async () => { + assert.equals(await stakingRouterImplementation.getRoleMemberCount(DEFAULT_ADMIN_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_PAUSE_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_RESUME_ROLE), 0) + assert.equals(await stakingRouterImplementation.getRoleMemberCount(STAKING_MODULE_MANAGE_ROLE), 0) + }) + + it('state is empty', async () => { + assert.equals(await stakingRouterImplementation.getWithdrawalCredentials(), ZERO_BYTES32) + assert.equals(await stakingRouterImplementation.getLido(), ZERO_ADDRESS) + assert.equals(await stakingRouterImplementation.getStakingModulesCount(), 0) + }) + + it('deposit fails', async () => { + await assert.reverts(stakingRouterImplementation.deposit(100, 0, '0x00', { from: stranger }), `APP_AUTH_LIDO_FAILED`) + }) + }) + + describe('staking router', async () => { + let stakingModule + before(async () => { + await snapshot.add() + + stakingModule = await StakingModuleMock.new({ from: deployer }) + + await app.addStakingModule('Test module', stakingModule.address, 100, 1000, 2000, { + from: appManager + }) + + await stakingModule.setAvailableKeysCount(100, { from: deployer }) + + assert.equals(await stakingModule.getAvailableKeysCount(), 100) + }) + + after(async () => { + await snapshot.revert() + }) + + it('set withdrawal credentials does not allowed without role', async () => { + const newWC = '0x'.padEnd(66, '5678') + await assert.reverts( + app.setWithdrawalCredentials(newWC, { from: stranger }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${MANAGE_WITHDRAWAL_CREDENTIALS_ROLE}` + ) + }) + + it('set withdrawal credentials', async () => { + const newWC = '0x'.padEnd(66, '5678') + const tx = await app.setWithdrawalCredentials(newWC, { from: appManager }) + + await assert.emits(tx, 'WithdrawalCredentialsSet', { withdrawalCredentials: newWC }) + + assert.equals(await stakingModule.getAvailableKeysCount(), 0) + }) + + it('direct transfer fails', async () => { + const value = 100 + await assert.revertsWithCustomError(app.sendTransaction({ value, from: deployer }), `ErrorDirectETHTransfer()`) + }) + + it('getStakingModuleKeysOpIndex', async () => { + await stakingModule.setValidatorsKeysNonce(100, { from: deployer }) + + assert.equals(await app.getStakingModuleKeysOpIndex(1), 100) + }) + + it('getStakingModuleActiveKeysCount', async () => { + await stakingModule.setActiveKeysCount(200, { from: deployer }) + + assert.equals(await app.getStakingModuleActiveKeysCount(1), 200) + }) + + it('getStakingRewardsDistribution', async () => { + const anotherStakingModule = await StakingModuleMock.new({ from: deployer }) + + await app.addStakingModule('Test module 2', anotherStakingModule.address, 100, 1000, 2000, { + from: appManager + }) + + await app.getStakingRewardsDistribution() + }) + + it('getStakingModuleIndexById zero index fail', async () => { + await assert.reverts(app.getStakingModuleIndexById(0), 'UNREGISTERED_STAKING_MODULE') + }) + }) + + describe('manage staking modules', async () => { + let stakingModule1, stakingModule2 + + const stakingModulesParams = [ + { + name: 'Test module 1', + targetShare: 1000, + stakingModuleFee: 2000, + treasuryFee: 200, + expectedModuleId: 1, + address: null + }, + { + name: 'Test module 1', + targetShare: 1000, + stakingModuleFee: 2000, + treasuryFee: 200, + expectedModuleId: 2, + address: null + } + ] + + before(async () => { + await snapshot.add() + + stakingModule1 = await StakingModuleMock.new({ from: deployer }) + stakingModule2 = await StakingModuleMock.new({ from: deployer }) + + stakingModulesParams[0].address = stakingModule1.address + stakingModulesParams[1].address = stakingModule2.address + }) + + after(async () => { + await snapshot.revert() + }) + + it('addStakingModule call is not allowed from stranger', async () => { + await assert.reverts( + app.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { from: stranger } + ), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` + ) + }) + + it('addStakingModule fails on share > 100%', async () => { + await assert.revertsWithCustomError( + app.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + 10001, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { from: appManager } + ), + `ErrorValueOver100Percent("_targetShare")` + ) + }) + + it('addStakingModule fails on fees > 100%', async () => { + await assert.revertsWithCustomError( + app.addStakingModule(stakingModulesParams[0].name, stakingModule1.address, stakingModulesParams[0].targetShare, 5000, 5001, { + from: appManager + }), + `ErrorValueOver100Percent("_stakingModuleFee + _treasuryFee")` + ) + }) + + it('add staking module', async () => { + const tx = await app.addStakingModule( + stakingModulesParams[0].name, + stakingModule1.address, + stakingModulesParams[0].targetShare, + stakingModulesParams[0].stakingModuleFee, + stakingModulesParams[0].treasuryFee, + { + from: appManager + } + ) + assert.equals(tx.logs.length, 3) + await assert.emits(tx, 'StakingModuleAdded', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + stakingModule: stakingModule1.address, + name: stakingModulesParams[0].name, + createdBy: appManager + }) + await assert.emits(tx, 'StakingModuleTargetShareSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + targetShare: stakingModulesParams[0].targetShare, + setBy: appManager + }) + await assert.emits(tx, 'StakingModuleFeesSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + stakingModuleFee: stakingModulesParams[0].stakingModuleFee, + treasuryFee: stakingModulesParams[0].treasuryFee, + setBy: appManager + }) + + assert.equals(await app.getStakingModulesCount(), 1) + assert.equals(await app.getStakingModuleStatus(stakingModulesParams[0].expectedModuleId), StakingModuleStatus.Active) + assert.equals(await app.getStakingModuleIsStopped(stakingModulesParams[0].expectedModuleId), false) + assert.equals(await app.getStakingModuleIsDepositsPaused(stakingModulesParams[0].expectedModuleId), false) + assert.equals(await app.getStakingModuleIsActive(stakingModulesParams[0].expectedModuleId), true) + + const module = await app.getStakingModule(stakingModulesParams[0].expectedModuleId) + + assert.equals(await app.getStakingModuleByIndex(stakingModulesParams[0].expectedModuleId - 1), module) + + assert.equals(module.name, stakingModulesParams[0].name) + assert.equals(module.stakingModuleAddress, stakingModule1.address) + assert.equals(module.stakingModuleFee, stakingModulesParams[0].stakingModuleFee) + assert.equals(module.treasuryFee, stakingModulesParams[0].treasuryFee) + assert.equals(module.targetShare, stakingModulesParams[0].targetShare) + assert.equals(module.status, StakingModuleStatus.Active) + assert.equals(module.lastDepositAt, 0) + assert.equals(module.lastDepositBlock, 0) + }) + + it('add another staking module', async () => { + const tx = await app.addStakingModule( + stakingModulesParams[1].name, + stakingModule2.address, + stakingModulesParams[1].targetShare, + stakingModulesParams[1].stakingModuleFee, + stakingModulesParams[1].treasuryFee, + { + from: appManager + } + ) + + assert.equals(tx.logs.length, 3) + await assert.emits(tx, 'StakingModuleAdded', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + stakingModule: stakingModule2.address, + name: stakingModulesParams[1].name, + createdBy: appManager + }) + await assert.emits(tx, 'StakingModuleTargetShareSet', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + targetShare: stakingModulesParams[1].targetShare, + setBy: appManager + }) + await assert.emits(tx, 'StakingModuleFeesSet', { + stakingModuleId: stakingModulesParams[1].expectedModuleId, + stakingModuleFee: stakingModulesParams[1].stakingModuleFee, + treasuryFee: stakingModulesParams[1].treasuryFee, + setBy: appManager + }) + + assert.equals(await app.getStakingModulesCount(), 2) + assert.equals(await app.getStakingModuleStatus(stakingModulesParams[1].expectedModuleId), StakingModuleStatus.Active) + assert.equals(await app.getStakingModuleIsStopped(stakingModulesParams[1].expectedModuleId), false) + assert.equals(await app.getStakingModuleIsDepositsPaused(stakingModulesParams[1].expectedModuleId), false) + assert.equals(await app.getStakingModuleIsActive(stakingModulesParams[1].expectedModuleId), true) + + const module = await app.getStakingModule(stakingModulesParams[1].expectedModuleId) + + assert.equals(await app.getStakingModuleByIndex(stakingModulesParams[1].expectedModuleId - 1), module) + + assert.equals(module.name, stakingModulesParams[1].name) + assert.equals(module.stakingModuleAddress, stakingModule2.address) + assert.equals(module.stakingModuleFee, stakingModulesParams[1].stakingModuleFee) + assert.equals(module.treasuryFee, stakingModulesParams[1].treasuryFee) + assert.equals(module.targetShare, stakingModulesParams[1].targetShare) + assert.equals(module.status, StakingModuleStatus.Active) + assert.equals(module.lastDepositAt, 0) + assert.equals(module.lastDepositBlock, 0) + }) + + it('get staking modules list', async () => { + const stakingModules = await app.getStakingModules() + + for (let i = 0; i < 2; i++) { + assert.equals(stakingModules[i].name, stakingModulesParams[i].name) + assert.equals(stakingModules[i].stakingModuleAddress, stakingModulesParams[i].address) + assert.equals(stakingModules[i].stakingModuleFee, stakingModulesParams[i].stakingModuleFee) + assert.equals(stakingModules[i].treasuryFee, stakingModulesParams[i].treasuryFee) + assert.equals(stakingModules[i].targetShare, stakingModulesParams[i].targetShare) + assert.equals(stakingModules[i].status, StakingModuleStatus.Active) + assert.equals(stakingModules[i].lastDepositAt, 0) + assert.equals(stakingModules[i].lastDepositBlock, 0) + } + }) + + it('update staking module does not allowed without role', async () => { + await assert.reverts( + app.updateStakingModule( + stakingModulesParams[0].expectedModuleId, + stakingModulesParams[0].targetShare + 1, + stakingModulesParams[0].stakingModuleFee + 1, + stakingModulesParams[0].treasuryFee + 1, + { + from: stranger + } + ), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` + ) + }) + + it('update staking module fails on target share > 100%', async () => { + await assert.revertsWithCustomError( + app.updateStakingModule( + stakingModulesParams[0].expectedModuleId, + 10001, + stakingModulesParams[0].stakingModuleFee + 1, + stakingModulesParams[0].treasuryFee + 1, + { + from: appManager + } + ), + `ErrorValueOver100Percent("_targetShare")` + ) + }) + + it('update staking module fails on fees > 100%', async () => { + await assert.revertsWithCustomError( + app.updateStakingModule(stakingModulesParams[0].expectedModuleId, stakingModulesParams[0].targetShare + 1, 5000, 5001, { + from: appManager + }), + `ErrorValueOver100Percent("_stakingModuleFee + _treasuryFee")` + ) + }) + + it('update staking module', async () => { + const stakingModuleNewParams = { + id: stakingModulesParams[0].expectedModuleId, + targetShare: stakingModulesParams[0].targetShare + 1, + stakingModuleFee: stakingModulesParams[0].stakingModuleFee + 1, + treasuryFee: stakingModulesParams[0].treasuryFee + 1 + } + + const tx = await app.updateStakingModule( + stakingModuleNewParams.id, + stakingModuleNewParams.targetShare, + stakingModuleNewParams.stakingModuleFee, + stakingModuleNewParams.treasuryFee, + { + from: appManager + } + ) + + assert.equals(tx.logs.length, 2) + + await assert.emits(tx, 'StakingModuleTargetShareSet', { + stakingModuleId: stakingModuleNewParams.id, + targetShare: stakingModuleNewParams.targetShare, + setBy: appManager + }) + await assert.emits(tx, 'StakingModuleFeesSet', { + stakingModuleId: stakingModuleNewParams.id, + stakingModuleFee: stakingModuleNewParams.stakingModuleFee, + treasuryFee: stakingModuleNewParams.treasuryFee, + setBy: appManager + }) + }) + + it('set staking module status does not allowed without role', async () => { + await assert.reverts( + app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: stranger + }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_MANAGE_ROLE}` + ) + }) + + it('set staking module status', async () => { + const tx = await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: appManager + }) + + await assert.emits(tx, 'StakingModuleStatusSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + status: StakingModuleStatus.Stopped, + setBy: appManager + }) + }) + + it('pause staking module does not allowed without role', async () => { + await assert.reverts( + app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: stranger + }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_PAUSE_ROLE}` + ) + }) + + it('pause staking module does not allowed at not active staking module', async () => { + await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: appManager + }) + await assert.revertsWithCustomError( + app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager + }), + `ErrorStakingModuleNotActive()` + ) + await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.DepositsPaused, { + from: appManager + }) + await assert.revertsWithCustomError( + app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager + }), + `ErrorStakingModuleNotActive()` + ) + }) + + it('pause staking module', async () => { + await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { + from: appManager + }) + const tx = await app.pauseStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager + }) + + await assert.emits(tx, 'StakingModuleStatusSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + status: StakingModuleStatus.DepositsPaused, + setBy: appManager + }) + }) + + it('deposit fails', async () => { + await assert.reverts( + app.deposit(100, stakingModulesParams[0].expectedModuleId, '0x00', { value: 100, from: lido }), + 'STAKING_MODULE_NOT_ACTIVE' + ) + }) + + it('getKeysAllocation', async () => { + const keysAllocation = await app.getKeysAllocation(1000) + + assert.equals(keysAllocation.allocated, 0) + assert.equals(keysAllocation.allocations, [0, 0]) + }) + + it('resume staking module does not allowed without role', async () => { + await assert.reverts( + app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: stranger + }), + `AccessControl: account ${stranger.toLowerCase()} is missing role ${STAKING_MODULE_RESUME_ROLE}` + ) + }) + + it('resume staking module does not allowed at not paused staking module', async () => { + await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Stopped, { + from: appManager + }) + await assert.revertsWithCustomError( + app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager + }), + `ErrorStakingModuleNotPaused()` + ) + await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.Active, { + from: appManager + }) + await assert.revertsWithCustomError( + app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager + }), + `ErrorStakingModuleNotPaused()` + ) + }) + + it('resume staking module', async () => { + await app.setStakingModuleStatus(stakingModulesParams[0].expectedModuleId, StakingModuleStatus.DepositsPaused, { + from: appManager + }) + const tx = await app.resumeStakingModule(stakingModulesParams[0].expectedModuleId, { + from: appManager + }) + + await assert.emits(tx, 'StakingModuleStatusSet', { + stakingModuleId: stakingModulesParams[0].expectedModuleId, + status: StakingModuleStatus.Active, + setBy: appManager + }) + }) + }) +}) diff --git a/test/deposit.test.js b/test/deposit.test.js index bd4469936..4ee4b4c3e 100644 --- a/test/deposit.test.js +++ b/test/deposit.test.js @@ -83,8 +83,8 @@ contract('Lido with official deposit contract', ([appManager, voting, user1, use stakingRouter = await StakingRouter.new(depositContract.address, { from: appManager }) await stakingRouter.initialize(appManager, app.address, ZERO_ADDRESS) await stakingRouter.grantRole(await stakingRouter.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), voting, { from: appManager }) - await stakingRouter.grantRole(await stakingRouter.MODULE_MANAGE_ROLE(), voting, { from: appManager }) - await stakingRouter.addModule( + await stakingRouter.grantRole(await stakingRouter.STAKING_MODULE_MANAGE_ROLE(), voting, { from: appManager }) + await stakingRouter.addStakingModule( 'Curated', operators.address, 10_000, // 100 % _targetShare diff --git a/test/helpers/assert.js b/test/helpers/assert.js new file mode 100644 index 000000000..2699dbf93 --- /dev/null +++ b/test/helpers/assert.js @@ -0,0 +1,60 @@ +const chai = require('chai') +const { getEvents, isBn } = require('@aragon/contract-helpers-test') +const { assertRevert } = require('@aragon/contract-helpers-test/src/asserts') +const { toChecksumAddress } = require('ethereumjs-util') +const { isAddress } = require('ethers/lib/utils') + +// chai.use(require('chai-match')) + +chai.util.addMethod(chai.assert, 'emits', function (receipt, eventName, args = {}, options = {}) { + const event = getEvent(receipt, eventName, args, options.abi) + this.isTrue(event !== undefined, `Event ${eventName} with args ${JSON.stringify(args)} wasn't found`) +}) + +chai.util.addMethod(chai.assert, 'notEmits', function (receipt, eventName, args = {}, options = {}) { + const { abi } = options + const event = getEvent(receipt, eventName, args, abi) + this.isUndefined(event, `Expected that event "${eventName}" with args ${args} wouldn't be emitted, but it was.`) +}) + +chai.util.addMethod(chai.assert, 'reverts', async function (receipt, reason) { + await assertRevert(receipt, reason) +}) + +chai.util.addMethod(chai.assert, 'equals', function (actual, expected, errorMsg = '') { + this.equal(actual.toString(), expected.toString(), `${errorMsg} expected ${expected.toString()} to equal ${actual.toString()}`) +}) + +chai.util.addMethod(chai.assert, 'revertsWithCustomError', async function (receipt, reason) { + try { + await receipt + } catch (error) { + chai.expect(error.message).to.equal(`VM Exception while processing transaction: reverted with custom error '${reason}'`) + return + } + throw new Error(`Transaction has been executed without revert. Expected revert reason ${reason}`) +}) + +function getEvent(receipt, eventName, args, abi) { + return getEvents(receipt, eventName, { decodeForAbi: abi }).find((e) => + // find the first index where every event argument matches the expected one + Object.entries(args).every( + ([argName, argValue]) => e.args[argName] !== undefined && normalizeArg(e.args[argName]) === normalizeArg(argValue) + ) + ) +} + +function normalizeArg(arg) { + if (isBn(arg) || Number.isFinite(arg)) { + return arg.toString() + } else if (isAddress(arg)) { + return toChecksumAddress(arg) + } else if (arg && arg.address) { + // Web3.js or Truffle contract instance + return toChecksumAddress(arg.address) + } + + return arg +} + +module.exports = { assert: chai.assert } diff --git a/test/scenario/helpers/deploy.js b/test/scenario/helpers/deploy.js index 5289c7e20..faf97354b 100644 --- a/test/scenario/helpers/deploy.js +++ b/test/scenario/helpers/deploy.js @@ -30,12 +30,11 @@ async function deployDaoAndPool(appManager, voting) { const treasury = web3.eth.accounts.create() - const [{ dao, acl }, oracleMock, depositContractMock, poolBase, nodeOperatorsRegistryBase] = await Promise.all([ + const [{ dao, acl }, oracleMock, depositContractMock, poolBase] = await Promise.all([ newDao(appManager), OracleMock.new(), DepositContractMock.new(), - Lido.new(), - NodeOperatorsRegistry.new() + Lido.new() ]) const stakingRouter = await StakingRouter.new(depositContractMock.address) @@ -90,31 +89,26 @@ async function deployDaoAndPool(appManager, voting) { acl.createPermission(voting, pool.address, MANAGE_PROTOCOL_CONTRACTS_ROLE, appManager, { from: appManager }) ]) - const nodeOperatorsRegistry = await setupNodeOperatorsRegistry( - dao, - acl, - voting, - token, - nodeOperatorsRegistryBase, - appManager, - stakingRouter.address - ) + const nodeOperatorsRegistry = await setupNodeOperatorsRegistry(dao, acl, voting, token, appManager, stakingRouter.address) const wc = '0x'.padEnd(66, '1234') await stakingRouter.initialize(appManager, pool.address, wc, { from: appManager }) // Set up the staking router permissions. - const [MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, MODULE_PAUSE_ROLE, MODULE_MANAGE_ROLE] = await Promise.all([ + const [MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, STAKING_MODULE_PAUSE_ROLE, STAKING_MODULE_MANAGE_ROLE] = await Promise.all([ stakingRouter.MANAGE_WITHDRAWAL_CREDENTIALS_ROLE(), - stakingRouter.MODULE_PAUSE_ROLE(), - stakingRouter.MODULE_MANAGE_ROLE() + stakingRouter.STAKING_MODULE_PAUSE_ROLE(), + stakingRouter.STAKING_MODULE_MANAGE_ROLE() ]) - await stakingRouter.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, voting, { from: appManager }) - await stakingRouter.grantRole(MODULE_PAUSE_ROLE, voting, { from: appManager }) - await stakingRouter.grantRole(MODULE_MANAGE_ROLE, voting, { from: appManager }) + await stakingRouter.grantRole(STAKING_MODULE_PAUSE_ROLE, voting, { from: appManager }) + await stakingRouter.grantRole(STAKING_MODULE_MANAGE_ROLE, voting, { from: appManager }) + + await stakingRouter.grantRole(MANAGE_WITHDRAWAL_CREDENTIALS_ROLE, appManager, { from: appManager }) + await stakingRouter.grantRole(STAKING_MODULE_PAUSE_ROLE, appManager, { from: appManager }) + await stakingRouter.grantRole(STAKING_MODULE_MANAGE_ROLE, appManager, { from: appManager }) - await stakingRouter.addModule( + await stakingRouter.addStakingModule( 'Curated', nodeOperatorsRegistry.address, 10_000, // 100 % _targetShare @@ -148,8 +142,10 @@ async function deployDaoAndPool(appManager, voting) { } } -async function setupNodeOperatorsRegistry(dao, acl, voting, token, nodeOperatorsRegistryBase, appManager, stakingRouterAddress) { - const nodeOperatorsRegistryProxyAddress = await newApp(dao, 'node-operators-registry', nodeOperatorsRegistryBase.address, appManager) +async function setupNodeOperatorsRegistry(dao, acl, voting, token, appManager, stakingRouterAddress) { + const nodeOperatorsRegistryBase = await NodeOperatorsRegistry.new() + const name = 'node-operators-registry-' + Math.random().toString(36).slice(2, 6) + const nodeOperatorsRegistryProxyAddress = await newApp(dao, name, nodeOperatorsRegistryBase.address, appManager) const nodeOperatorsRegistry = await NodeOperatorsRegistry.at(nodeOperatorsRegistryProxyAddress) // Initialize the node operators registry and the pool diff --git a/test/scenario/lido_rewards_distribution_math.js b/test/scenario/lido_rewards_distribution_math.js index 08d206bbb..0fdfd4517 100644 --- a/test/scenario/lido_rewards_distribution_math.js +++ b/test/scenario/lido_rewards_distribution_math.js @@ -3,8 +3,8 @@ const { BN } = require('bn.js') const { assertBn, assertEvent } = require('@aragon/contract-helpers-test/src/asserts') const { getEventArgument, ZERO_ADDRESS } = require('@aragon/contract-helpers-test') -const { pad, ETH } = require('../helpers/utils') -const { deployDaoAndPool } = require('./helpers/deploy') +const { pad, ETH, hexConcat } = require('../helpers/utils') +const { deployDaoAndPool, setupNodeOperatorsRegistry } = require('./helpers/deploy') const { DSMAttestMessage, DSMPauseMessage } = require('../0.8.9/helpers/signatures') const INodeOperatorsRegistry = artifacts.require('INodeOperatorsRegistry') @@ -16,11 +16,14 @@ const tenKBN = new BN(10000) // Total max fee is 10% const totalFeePoints = 0.1 * 10000 -// Of this 1%, 30% goes to the treasury -const treasuryFeePoints = 0.3 * 10000 // 50% goes to node operators const nodeOperatorsFeePoints = 0.5 * 10000 +const StakingModuleStatus = { + Active: 0, // deposits and rewards allowed + DepositsPaused: 1, // deposits NOT allowed, rewards allowed + Stopped: 2 // deposits and rewards NOT allowed +} contract('Lido: rewards distribution math', (addresses) => { const [ // the root account which deployed the DAO @@ -32,15 +35,13 @@ contract('Lido: rewards distribution math', (addresses) => { operator_2, // users who deposit Ether to the pool user1, - user2, - user3, // unrelated address nobody ] = addresses let pool, nodeOperatorsRegistry, token - let stakingRouter - let oracleMock + let stakingRouter, dao, acl + let oracleMock, anotherCuratedModule let treasuryAddr, guardians let depositSecurityModule, depositRoot @@ -83,6 +84,9 @@ contract('Lido: rewards distribution math', (addresses) => { before(async () => { const deployed = await deployDaoAndPool(appManager, voting) + dao = deployed.dao + acl = deployed.acl + // contracts/StETH.sol token = deployed.pool @@ -249,6 +253,149 @@ contract('Lido: rewards distribution math', (addresses) => { assertBn(nodeOperatorsRegistryTokenDelta, nodeOperatorsFeeToMint, 'nodeOperatorsRegistry balance = fee') }) + it(`add another staking module`, async () => { + anotherCuratedModule = await setupNodeOperatorsRegistry(dao, acl, voting, token, appManager, stakingRouter.address) + await stakingRouter.addStakingModule( + 'Curated limited', + anotherCuratedModule.address, + 5_000, // 50 % _targetShare + 100, // 1 % _moduleFee + 100, // 1 % _treasuryFee + { from: voting } + ) + + const modulesList = await stakingRouter.getStakingModules() + + assert(modulesList.length, 2, 'module added') + + const operator = { + name: 'operator', + address: operator_2, + validators: [...Array(10).keys()].map((i) => ({ + key: pad('0xaa01' + i.toString(16), 48), + sig: pad('0x' + i.toString(16), 96) + })) + } + const validatorsCount = 10 + await anotherCuratedModule.addNodeOperator(operator.name, operator.address, { from: voting }) + await anotherCuratedModule.addSigningKeysOperatorBH( + 0, + validatorsCount, + hexConcat(...operator.validators.map((v) => v.key)), + hexConcat(...operator.validators.map((v) => v.sig)), + { + from: operator.address + } + ) + await anotherCuratedModule.setNodeOperatorStakingLimit(0, validatorsCount, { from: voting }) + assertBn(await anotherCuratedModule.getUnusedSigningKeyCount(0), validatorsCount, 'operator of module has 10 unused keys') + }) + + it(`deposit to new module`, async () => { + const depositAmount = ETH(32) + await pool.submit(ZERO_ADDRESS, { value: depositAmount, from: user1 }) + + const [_, newCurated] = await stakingRouter.getStakingModules() + + await nodeOperatorsRegistry.setNodeOperatorStakingLimit(0, 0, { from: voting }) + + const block = await web3.eth.getBlock('latest') + const keysOpIndex = await anotherCuratedModule.getKeysOpIndex() + + DSMAttestMessage.setMessagePrefix(await depositSecurityModule.ATTEST_MESSAGE_PREFIX()) + DSMPauseMessage.setMessagePrefix(await depositSecurityModule.PAUSE_MESSAGE_PREFIX()) + + const validAttestMessage = new DSMAttestMessage(block.number, block.hash, depositRoot, newCurated.id, keysOpIndex) + + const signatures = [ + validAttestMessage.sign(guardians.privateKeys[guardians.addresses[0]]), + validAttestMessage.sign(guardians.privateKeys[guardians.addresses[1]]) + ] + + const user1BalanceBefore = await token.balanceOf(user1) + const user1SharesBefore = await token.sharesOf(user1) + const totalSupplyBefore = await token.totalSupply() + + assertBn(await anotherCuratedModule.getUnusedSigningKeyCount(0), 10, 'operator of module has 10 unused keys') + await depositSecurityModule.depositBufferedEther(block.number, block.hash, depositRoot, newCurated.id, keysOpIndex, '0x', signatures) + assertBn(await anotherCuratedModule.getUnusedSigningKeyCount(0), 9, 'operator of module has 9 unused keys') + + assertBn(await token.balanceOf(user1), user1BalanceBefore, 'user1 balance is equal first reported value + their buffered deposit value') + assertBn(await token.sharesOf(user1), user1SharesBefore, 'user1 shares are equal to the first deposit') + assertBn(await token.totalSupply(), totalSupplyBefore, 'token total supply') + assertBn(await token.getBufferedEther(), ETH(2), '') + }) + + it(`rewards distribution`, async () => { + const totalPooledEtherBefore = await token.getTotalPooledEther() + const bufferedBefore = await token.getBufferedEther() + const newBeaconBalance = totalPooledEtherBefore.sub(bufferedBefore).mul(new BN(2)) + + const firstModuleSharesBefore = await token.sharesOf(nodeOperatorsRegistry.address) + const secondModuleSharesBefore = await token.sharesOf(anotherCuratedModule.address) + const treasurySharesBefore = await await token.sharesOf(treasuryAddr) + + await reportBeacon(2, newBeaconBalance) + + assertBn(await token.totalSupply(), newBeaconBalance.add(bufferedBefore), 'token total supply') + + const rewardsToDistribute = await token.getSharesByPooledEth(newBeaconBalance.add(bufferedBefore).sub(totalPooledEtherBefore)) + + const expectedRewardsDistribution = { + firstModule: rewardsToDistribute.div(new BN(40)), + secondModule: rewardsToDistribute.div(new BN(200)), + treasury: rewardsToDistribute + .div(new BN(40)) + .add(rewardsToDistribute.div(new BN(200))) + .add(new BN(2)) + } + + const firstModuleSharesAfter = await token.sharesOf(nodeOperatorsRegistry.address) + const secondModuleSharesAfter = await token.sharesOf(anotherCuratedModule.address) + const treasurySharesAfter = await await token.sharesOf(treasuryAddr) + + assertBn(firstModuleSharesAfter, firstModuleSharesBefore.add(expectedRewardsDistribution.firstModule), 'first module balance') + assertBn(secondModuleSharesAfter, secondModuleSharesBefore.add(expectedRewardsDistribution.secondModule), 'second module balance') + assertBn(treasurySharesAfter, treasurySharesBefore.add(expectedRewardsDistribution.treasury), 'treasury balance') + }) + + it(`module rewards should recieved by treasury if module stopped`, async () => { + const [firstModule] = await stakingRouter.getStakingModules() + const totalPooledEtherBefore = await token.getTotalPooledEther() + const bufferedBefore = await token.getBufferedEther() + const newBeaconBalance = totalPooledEtherBefore.sub(bufferedBefore).mul(new BN(2)) + + await stakingRouter.setStakingModuleStatus(firstModule.id, StakingModuleStatus.Stopped, { from: appManager }) + + const firstModuleSharesBefore = await token.sharesOf(nodeOperatorsRegistry.address) + const secondModuleSharesBefore = await token.sharesOf(anotherCuratedModule.address) + const treasurySharesBefore = await await token.sharesOf(treasuryAddr) + + await reportBeacon(2, newBeaconBalance) + + assertBn(await token.totalSupply(), newBeaconBalance.add(bufferedBefore), 'token total supply') + + const rewardsToDistribute = await token.getSharesByPooledEth(newBeaconBalance.add(bufferedBefore).sub(totalPooledEtherBefore)) + + const expectedRewardsDistribution = { + firstModule: new BN(0), + secondModule: rewardsToDistribute.div(new BN(200)), + treasury: rewardsToDistribute + .div(new BN(40)) + .add(rewardsToDistribute.div(new BN(200))) + .add(new BN(2)) + .add(rewardsToDistribute.div(new BN(40))) + } + + const firstModuleSharesAfter = await token.sharesOf(nodeOperatorsRegistry.address) + const secondModuleSharesAfter = await token.sharesOf(anotherCuratedModule.address) + const treasurySharesAfter = await token.sharesOf(treasuryAddr) + + assertBn(firstModuleSharesAfter, firstModuleSharesBefore.add(expectedRewardsDistribution.firstModule), 'first module balance') + assertBn(secondModuleSharesAfter, secondModuleSharesBefore.add(expectedRewardsDistribution.secondModule), 'second module balance') + assertBn(treasurySharesAfter, treasurySharesBefore.add(expectedRewardsDistribution.treasury), 'treasury balance') + }) + // test multiple staking modules erward distribution async function getAwaitedFeesSharesTokensDeltas(profitAmount, prevTotalShares, validatorsCount) { const totalPooledEther = await pool.getTotalPooledEther()