--- title: Reward distribution in curated-based modules tags: Oracle status: --- # Reward distribution in curated-based modules <img src="https://hackmd.io/_uploads/ryTPt0OWA.jpg" alt="Picture" width="400" height="400" style="display: block; margin: 0 auto" /> ## 🎯 TL;DR This document presents an approach for implementing a permissionless method for rewards distribution in Curated and Simple DVT modules. * Reward distribution will be decoupled from the Accounting Oracle report and executed separately using the permissionless `distributeReward` method in each staking module. * A bot for the curated module will be designed to trigger the permissionless method for reward distribution after each Accounting Report is processed. ## 𓀀 Motivation Currently, for the Curated and Simple DVT modules, the finalization hook initiates reward distribution as part of the last transaction in the third phase. However, for the CSM module, rewards are distributed by a performance oracle. The situation where some modules distribute rewards during the third phase of the oracle report, while others use alternative mechanisms, complicates the accounting report and could potentially become a source of bugs in the future. Additionally, this approach increases complexity as it necessitates consideration of gas consumption for the distribution of rewards. - The reward distribution process might exceed the block gas limit if conducted for multiple Staking Modules as part of the final transaction in the third phase. - To ensure there is enough gas for reward distribution, we would need to make the algorithm for splitting the third-phase report into transactions more complex. This involves allocating less data to the final transaction of the third phase to reserve gas for reward distribution. ### 🛑 Technical Constraints The reward distribution for a single node operator in curated-based modules consumes 29,500 gas. For the full module with 200 operators, the total gas consumption amounts to 5.9 million. :::spoiler Calculation method During the first wave of SimpleDVT operator onboarding, 12 new operators were added. Gas consumption was analyzed for the `submitReportExtraDataEmpty` transaction in the third phase, which does not contain updates to exited or stuck validator statistics and only performs reward distribution. - [36 operators - 1_265_000 gas](https://etherscan.io/tx/0x9d7d4d2cfe3a7f18c3403f4b9227d0d41e659703a7ff8ceb589669048c694661) - [48 operators - 1_619_000 gas](https://etherscan.io/tx/0xfa615ea2bac3d72f17810eb14837cf28edcaee06b4de6191b04215504c7a60c9) ``` // Gas consumption for distributing reward to 12 operatos 1_619_000 - 1_265_000 = 354_000 // Gas consumption for distributing rewar to 1 operator 354_000 / 12 = 29_500 // Gas consumption for 200 operators // 200 - maximum operator count in curated-based modules 29_500 * 200 = 5_900_000 ``` ::: <br /> Considering the plans for SimpleDVT operator onboarding and a possible slight increase in the number of node operators for the curated module, we can estimate that reward distribution for 200 operators in the SimpleDVT module and 50 operators in the Curated module (250 operators in total) will require 7.5 million gas. ![image](https://hackmd.io/_uploads/ryi27Adz0.png) ###### *You can read more about the gas consumption limit for third-phase transactions in the [multi-transactional third phase](https://hackmd.io/@lido/rkMr1EmJC) document.* A very large transaction could potentially take longer to process or even stall, as validators might delay including it due to its size and the commitment of a large portion of a block's gas limit. Introducing additional SimpleDVT modules, each consuming up to 5.9 million gas for reward distribution, risks reaching the maximum Ethereum block limit. ## 📋 Proposed Solution The proposed design suggests that reward distribution should be decoupled from the finalization hook and instead be executed separately from the Accounting Oracle report, using permissionless `distributeReward` method for the each staking module. ![image](https://hackmd.io/_uploads/rkpQvzab0.png) The `distributeReward` method can be called at any time after the third phase of the oracle report is completed and before the start of the second phase processing of the next oracle report. This limitation exists because the total module reward is transferred to the module during the second phase of the oracle report, but distribution among node operators can only begin after the validators' statistics per node operator have been submitted to the module during the third phase of the oracle report. ![image](https://hackmd.io/_uploads/H1Vz0Za-C.png) It's required to develop a bot that will trigger `distributeReward` permissionless method after each Accounting Report. Proposed design suggest that bot will periodically check target modules state and will trigger reward distribution once oracle report completed, depend on the report state (positive, negative, failed) and the moment when bot trigger reward distribution several cases possible. ![image](https://hackmd.io/_uploads/HJTkQGp-R.png) Summary: Reward can be distributed at any time after the completion of the third phase of the oracle report and before the start of the second phase processing of the next oracle report. ::: spoiler What if reward are not distributed? Currently, it is also possible for situations to arise where rewards are not distributed for a couple of days. In such cases rewards accumulated and we distribute rewards based on the latest report; previous reports are not retained or considered in the distribution process. In the worst-case scenario, if the number of active validators changes significantly for some operators, it might result in inaccurate rewards distribution. However, the occurrence where rewards are not distributed on the report day is exceptional and should not happen under normal operation of the oracle. Such instances are treated as incidents and are addressed by the ValSet team. It's worth noting that during the previous years, there was only one instance when rewards were not distributed on the report day. In summary, while the fundamental approach to reward distribution among node operators within a module remains unchanged, we are planning a minor adjustment to the timing of these distributions. Currently, rewards are distributed as part of the Third Phase report. Going forward, we intend to separate this process from the Third Phase, opting to distribute reward in a separate transactions following the Third Phase. ::: ### Implementation details #### Curated-based modules Once the delivery of third-phase report updates is complete, the Accounting Oracle triggers the Finalization hook (calling the Staking Router's `onValidatorsCountsByNodeOperatorReportingFinished`). Within this method, for each staking module, we call `onExitedAndStuckValidatorsCountsUpdated`. This signals that all Node Operator updates have been successfully delivered and the Accounting Oracle report is finalized. Currently, rewards distribution in curated-based staking modules occurs within the `onExitedAndStuckValidatorsCountsUpdated` method. The proposed solution is to update this method to mark the module as ready for reward distribution instead of distributing the reward directly. The actual reward distribution will subsequently start within the distributeReward method. It's worth mentioning the existing `onRewardsMinted` method, which is used by the Staking Router during the second phase of the Accounting Oracle report to notify each Staking Module that the total module reward has been transferred to the module. In this method, we block reward distribution until the third phase report is finished (as per `onExitedAndStuckValidatorsCountsUpdated` above), ensuring that rewards can be distributed among node operators. `NodeOperatorRegistry.sol` code **example**: ```solidity= // Enum to represent the state of the reward distribution process enum RewardDistributionState { TransferredToModule, // New reward portion minted and transferred to the module ReadyForDistribution, // Operators' statistics updated, reward ready for distribution Distributed // Reward distributed among operators } // bytes32 internal constant REWARD_DISTRIBUTION_STATE = keccak256("lido.NodeOperatorsRegistry.rewardDistributionState"); bytes32 internal constant REWARD_DISTRIBUTION_STATE = 0x4ddbb0dcdc5f7692e494c15a7fca1f9eb65f31da0b5ce1c3381f6a1a1fd579b6; /// @dev Get the current reward distribution state, anyone can monitor this state /// and distribute reward (call distributeReward method) among operators when it's `ReadyForDistribution` function getRewardDistributionState() public view returns (RewardDistributionState) { uint256 state = REWARD_DISTRIBUTION_STATE.getStorageUint256(); return RewardDistributionState(state); } /// @notice Called by StakingRouter after it finishes updating exited and stuck validators /// counts for this module's node operators. /// /// Guaranteed to be called after an oracle report is applied, regardless of whether any node /// operator in this module has actually received any updated counts as a result of the report /// but given that the total number of exited validators returned from getStakingModuleSummary /// is the same as StakingRouter expects based on the total count received from the oracle. function onExitedAndStuckValidatorsCountsUpdated() external { _auth(STAKING_ROUTER_ROLE); _updateRewardDistributionState(RewardDistributionState.ReadyForDistribution); } /// @notice Permissionless method for distributing all accumulated module rewards among node operators /// based on the latest accounting report. /// /// @dev Rewards can be distributed after all necessary data required to distribute rewards among operators /// has been delivered, including exited and stuck keys. /// /// The reward distribution lifecycle: /// /// 1. TransferredToModule: Rewards are transferred to the module during an oracle main report. /// 2. ReadyForDistribution: All necessary data required to distribute rewards among operators has been delivered. /// 3. Distributed: Rewards have been successfully distributed. /// /// The function can only be called when the state is ReadyForDistribution. /// /// @dev Rewards can be distributed after node operators' statistics are updated until the next reward /// is transferred to the module during the next oracle frame. function distributeReward() external { require(getRewardDistributionState() == RewardDistributionState.ReadyForDistribution, "DISTRIBUTION_NOT_READY"); _updateRewardDistributionState(RewardDistributionState.Distributed); _distributeRewards(); } /// @notice Called by StakingRouter to signal that stETH rewards were minted for this module. function onRewardsMinted(uint256 /* _totalShares */) external { _auth(STAKING_ROUTER_ROLE); _updateRewardDistributionState(RewardDistributionState.TransferredToModule); } function _updateRewardDistributionState(RewardDistributionState _state) internal { REWARD_DISTRIBUTION_STATE.setStorageUint256(uint256(_state)); emit RewardDistributionStateChanged(_state); } ``` #### CSM module For the CSM module reward will be distributed by a dedicated performance oracle. Proposed design will not affect CSM module, the new `distributeReward` method required only for Curated-based modules. #### Empty third phase No addition changes are required in the case when the Accounting Oracle third phase report is empty. The AccountingOracle's `submitReportExtraDataEmpty` is called, which validates the report state and calls the finalization hook (Staking Router `onValidatorsCountsByNodeOperatorReportingFinished`). After that the new Reward bot can trigger reward distribution. ## 📦 Alternative Solution As an alternative solution, a claimable reward approach was considered. There is an existing [prototype](https://github.com/krogla/claimable-rewards-distributor/blob/main/contracts/test/OperatorRewardsMock.sol) written by [KRogLA](https://github.com/krogla) that contains a RewardsDistributor library for claimable reward approach. Each curated-based module tracks node operators claims and allows each node operator to claim the total accumulated rewards they have earned at any given moment. The flow can be observed in this [Excel preview](https://docs.google.com/spreadsheets/d/1Yxf-sz8ZSkjtPwI7LteXTfI_cXEZiTGjUmxRoUktPBw/edit#gid=0) . The advantage of this approach is its decentralized nature. Node operators gain control over when they receive rewards, and the cost of reward transfers becomes their responsibility. However, the implementation will require significantly more time as it necessitates extensive changes to the on-chain contract of curated-based modules. The mechanism for penalizing stuck validators requires further elaboration, and it will be necessary to handle the accumulation of division residues and cases where node operators, for some reason, do not claim their rewards. As a result, the implementation of this approach will take much more time. That is why a simpler approach with a bot for rewards distribution was preferred.