# VEBO Improvements ![vebo2](https://hackmd.io/_uploads/HkDlClzA6.jpg) ## 🎯 TL;DR It is proposed to modify the algorithm for exiting validators in the Validator Exit Bus Oracle (VEBO). From the staking modules' side, it is suggested to add the ability to signal VEBO about the need to exit validators from an operator regardless of the demand in the Withdrawal Queue (WQ). From the staking router's side, it is proposed to consider the module's share when prioritizing validators for exit. It is assumed that the reader is familiar with the current design of VEBO: - [https://docs.lido.fi/contracts/validators-exit-bus-oracle](https://docs.lido.fi/contracts/validators-exit-bus-oracle) - [https://docs.lido.fi/guides/oracle-spec/validator-exit-bus](https://docs.lido.fi/guides/oracle-spec/validator-exit-bus) ## 🌟 Motivation ### Boosted Exit Requests The current VEBO design assumes that validators can only be requested to exit if there are existing user withdrawal requests. While the protocol has tools to prioritize validator exit requests from specific operators, it does not have tools that allow the protocol to request an operator to exit validators regardless of the demand in WQ. Exit requests without taking into account user withdrawal requests is important for upcoming permisionless modules in order to signal VEBO about the need to exit validator when operator's bond decreases. It's also needed for offboarding of operators or reducing their share in the pool in a situation when there is insufficient user withdrawal requests. This motivates to consider adding a special mode to the protocol to exit validators from the operator without taking into account the demand in WQ. ### Target Share The current design of the staking router uses the `targetShare` parameter of each module when distributing stakes among them. However, this parameter is not considered in the algorithm for choosing which validators request to exit. This could lead to a situation where one of the modules reaches its target share, after which a large number of validators are exited from other modules, leading to an increase in the original module's share above the target share. Since a module's share can be chosen based on security considerations, it is proposed to consider a mechanism to take it into account not only during stake allocation, but also during validator requests to exit. ## 💅 Proposed Design ### Boosted Exit Requests The current VEBO design presupposes a request for validator exits only in the presence of user withdrawal requests in the WQ contract. Modules can signal VEBO about the priority of exiting validators from a particular node operator using the `targetLimit` mechanism. It is proposed to allow modules to signal VEBO about the necessity of sending a request to exit validators at a specific operator without being tied to demand in WQ. For this, it is suggested to modify the current `targetLimit` instrument by adding an additional boosted exit mode, namely, the parameter `bool isTargetLimitActive` is proposed to be replaced with `uint8 targetLimitMode` taking one of the following values: - `0` – Disabled - `1` – Limited stake, smooth exit mode - `2` – Limited stake, boosted exit mode Let's consider the target limit modes in more detail: **Disabled.** This mode implies no limitation. The operator is not restricted in receiving new stakes and does not have additional priorities when choosing validators for exit. **Smooth exit mode.** The operator has a limit on the number of active validators. As long as the number of active validators of the operator does not exceed the `targetLimit`, the operator receives stakes under general conditions. If this value is reached, the operator stops receiving new stakes (should be implemented at the module level). If the number of active keys of the operator exceeds the `targetLimit`, then such an operator's validators are prioritized for exit in the amount of targeted validators to exit. **Boosted exit mode.** Similar to smooth mode, but does not consider demand in WQ. The operator's validators in the amount of targeted validators to exit are prioritized for exit and requested without considering demand in WQ. Adding a new mode does not affect the overall limit on validator exits per report. If it is necessary to accelerated exit a large number of validators, this will be done over several reports. The proposed design suggests the following changes in the `IStakingModule` interface: ```solidity interface IStakingModule { function getNodeOperatorSummary(uint256 _nodeOperatorId) external view returns ( uint8 targetLimitMode, // former isTargetLimitActive uint256 targetValidatorsCount, uint256 stuckValidatorsCount, uint256 refundedValidatorsCount, uint256 stuckPenaltyEndTimestamp, uint256 totalExitedValidators, uint256 totalDepositedValidators, uint256 depositableValidatorsCount ); function updateTargetValidatorsLimits( uint256 _nodeOperatorId, uint8 _targetLimitMode, // former _isTargetLimitActive uint256 _targetLimit ) external; } ``` The change in the `updateTargetValidatorsLimits` method does not break backward compatibility with the EasyTrack factory `UpdateTargetValidatorLimits` which is used to set limits in the Simple DVT module. Nonetheless, it is proposed to update it, supporting changes in the `IStakingModule` interface. ```solidity interface IUpdateTargetValidatorLimits { struct TargetValidatorsLimit { uint256 nodeOperatorId; uint8 targetLimitMode; // former isTargetLimitActive uint256 targetLimit; } } ``` The change from `bool isTargetLimitActive` to `uint8 targetLimitMode` affects the response from the view method `getNodeOperatorSummary`, which may be used in external integrations and offchain tooling. Tests show that backward compatibility remains: [https://github.com/lidofinance/sr-1.5-compatibility-tests](https://github.com/lidofinance/sr-1.5-compatibility-tests). All modes other than disabled are interpreted by the decoder based on the old interface as the enabled `targetLimit` mode. Changes to the off-chain part are described in the [Exit Order](#Exit-Order) section. ### Target Share When validators are exited from one module, its share decreases, while the shares of other modules increase. This can lead to a situation where a module's share significantly exceeds its target share. <img src="https://hackmd.io/_uploads/Sy-UBFdC6.png" style="width:580px"/> The illustration shows the exit of validators from module `A`, leading to a redistribution of shares between modules: the share of module `A` decreases, while the share of module `B` increases. The most direct and simplest solution could be to consider the target share when prioritizing validators for exit. However, this solution has a problem – the protocol's TVL has some volatility, and operators in different modules have varying costs for rejoining validators. These differences can be expressed in the node operator itself (solo staker or large company), in the automation of processes, technology features (vanilla validator or DVT), etc. TVL volatility, combined with varying rejoining costs, leads to the problem that the protocol may periodically rotate validators in modules with high rejoining costs – requesting exits and then making deposits after some time. Given the described problem and the drawbacks of the direct solution, it is proposed to represent the target share of a module as a range of values: `stakeShareLimit` and `priorityExitShareThreshold`, where `stakeShareLimit <= priorityExitShareThreshold`. The lower value `stakeShareLimit` represents the maximum share that can be allocated to a module when distributing stakes among modules. This parameter is nothing other than the current `targetShare`. Nevertheless, it is suggested to rename it, as its current name does not fully reflect its essence. The higher value `priorityExitShareThreshold` represents the module's share threshold, upon crossing which, exits of validators from the module will be prioritized. <img src="https://hackmd.io/_uploads/Hk3NrFORa.png" style="width:580px"/> These two values allow the module to be in one of three states at any given time: #### <span style="color:#00A3FF">Module has not reached share limit</span> `currentShare < stakeShareLimit`. The proposed design makes no changes for this state. Everything remains as it is: - The staking router **prioritizes stake allocation** to modules in this state - Validators in a module with this state **do not have any extra priority for exit** #### <span style="color:#36E6A6">Module has reached stake share limit</span> `stakeShareLimit <= currentShare <= priorityExitShareThreshold`. The proposed design makes no changes for this state. Everything remains as it is: - The staking router **does not allocate stake** to modules in this state - Validators in a module with this state **do not have any extra priority for exit** #### <span style="color:#FF8E76">Module exceeds priority exit threshold</span> `priorityExitShareThreshold < currentShare`. It is the state that is affected by the proposed changes. - The staking router **does not allocate stake** to modules in this state - Validators in a module with this state have an **increased exit priority** The proposed design suggests the following changes in the Staking Router contract interface: ```solidity interface IStakingRouter { struct StakingModule { uint24 id; address stakingModuleAddress; uint16 stakingModuleFee; uint16 treasuryFee; uint16 stakeShareLimit; // rename targetShare parameter uint8 status; string name; uint64 lastDepositAt; uint256 lastDepositBlock; uint256 exitedValidatorsCount; uint16 priorityExitShareThreshold; // new parameter } event StakingModuleStakeShareLimitSet(uint256 indexed stakingModuleId, uint256 stakeShareLimit, address setBy); event StakingModulePriorityExitShareThresholdSet(uint256 indexed stakingModuleId, uint256 priorityExitShareThreshold, address setBy); function updateStakingModule( uint256 _stakingModuleId, uint256 _stakeShareLimit, // rename _targetShare argument uint256 _priorityExitShareThreshold, // new argument uint256 _stakingModuleFee, uint256 _treasuryFee ) external; function addStakingModule( string calldata _name, address _stakingModuleAddress, uint256 _stakeShareLimit, // rename _targetShare argument uint256 _priorityExitShareThreshold, // new argument uint256 _stakingModuleFee, uint256 _treasuryFee ) external; } ``` :::spoiler Backward compatibility <br> The change to the `StakingModule` struct affects the response from some view methods, which may be used in external integrations and offchain tooling: - getStakingModule - getStakingModules - getStakingModuleDigests - getAllStakingModuleDigests Tests show that backward compatibility remains for both offchain tools and possible onchain integrations: [https://github.com/lidofinance/sr-1.5-compatibility-tests](https://github.com/lidofinance/sr-1.5-compatibility-tests). The modified response is correctly decoded using standard Solidity tools and the ethers library. New bytes in the response are ignored. ::: Changes to the off-chain part are described in the [Exit Order](#Exit-Order) section. ### Exit Order It is proposed to implement two stages of selecting validators for exit: 1. Covering Demand in WQ 2. Boosted Exits #### Stage 1. Covering Demand in WQ This stage is from the current design of VEBO. To determine which validators to request for exit, the Offchain Oracle selects one entry from the sorted list of exitable Lido validators until the demand in WQ is covered by the exiting validators and future rewards, or until the limit per report is reached. Validators are sorted by predicates in the following sequence: 1. Validator whose operator with the lowest number of delayed validators 2. _Validator whose operator with the highest number of targeted validators to boosted exit_ 3. _Validator whose operator with the highest number of targeted validators to smooth exit_ 4. _Validator whose module with the highest deviation from the module share_ 5. Validator whose operator with the highest stake weight 6. Validator whose operator with the highest number of validators 7. Validator with the lowest index The proposed changes affect predicates 2, 3 and 4 (italicized), the others remain unchanged. Let's examine the changing predicates in detail: **Boosted target limit predicate.** If `targetLimitMode` is set to boosted exits, the result will be the number of targeted validators to exit; in other cases, the predicate returns 0. The higher the return value, the higher the priority of the validator to exit. ```python def predicate_boosted_target_limit(): if operator.target_limit_mode == TARGET_LIMIT_MODE.BOOSTED_EXITS: return max(operator.predictable_validators_count - operator.target_limit, 0) return 0 ``` **Smooth target limit predicate.** If `targetLimitMode` is set to smooth limit, the result will be the number of targeted validators to exit; in other cases, the predicate returns 0. The higher the return value, the higher the priority of the validator to exit. ```python def predicate_smooth_target_limit(): if operator.target_limit_mode == TARGET_LIMIT_MODE.SMOOTH_EXITS: return max(operator.predictable_validators_count - operator.target_limit, 0) return 0 ``` **Module share predicate.** The predicate prioritizes the exit of validators from modules where there are more validators than the exit threshold. The higher the number of predicted validators over the exit threshold assigned to a module, the more prioritized the validators in that module are for exit. Predicted validators represent the number of active validators that will be in the module after all effects are applied: all requested validators are exited, deposits that were made recently turned into active validators. ```python def predicate_module_share(): module_max_validators = total_validators * priority_exit_share_threshold if module_predicted_validators > module_max_validators: # the higher is the number of validators in the module over the exit threshold, the higher is the exit priority return module_max_validators - module_predicted_validators return 0 ``` After each iteration of selecting a validator, the state of operators and validators mutates – validators previously sent for exit are marked, after which the array of validators is resorted. After the stage ends, the state is passed to the next stage – Boosted Exits, thus validators requested at an earlier stage are considered in the subsequent one. :::info - The target limit predicate is now split into two: boosted and smooth - Validators of operators with `targetLimitMode` set to boosted exits have a higher priority for exit than operators with the mode set to smooth limit - The new module share predicate prioritizes exits from modules that have more validators than the exit threshold allows ::: #### Stage 2. Boosted Exits The second stage presupposes the exit of validators requiring exit regardless of the demand in WQ. The state of operators and validators after the first step is filtered, leaving only validators of operators having `targetLimitMode` set to boosted exits. Validators are sorted by the number of targeted validators to exit. After each iteration of selecting a validator, the state of operators and validators mutates – validators previously sent for exit are marked, after which the array of validators is resorted. The selection of validators continues as long as there are elements in the array or until the total number of exiting operators per report reach the limit. There is only one sorting predicate on this stage: 1. _Validator whose operator with the highest number of targeted validators to boosted exit_ 2. _Validator with the lowest index_ Let's consider various examples below. <span style="color:#FF8E76">Coral color</span> indicates validators requested due to the enabled boosted target limit of the operator, <span style="color:#B8B8B8">grey color</span> indicates validators requested in general order. <img src="https://hackmd.io/_uploads/BkkztYuRT.png" style="width:580px;margin-top:24px;margin-bottom:12px"/> In the example above, only the first stage of the algorithm works. Demand in WQ is partially covered by <span style="color:#FF8E76">boosted exits</span> and partially by <span style="color:#B8B8B8">other validators in general order</span>. The second stage returns 0 validators to exit because all validators of operators in boosted exit status have already been requested by this time. <img src="https://hackmd.io/_uploads/ry-etKuC6.png" style="width:580px;margin-top:24px;margin-bottom:12px"/> In the example above, both stages of the algorithm work. Demand in WQ is fully covered by <span style="color:#FF8E76">boosted exits</span>, after which the second stage of the algorithm continues to <span style="color:#FF8E76">exit validators</span> until the targeted validators to boosted exit equals 0. <img src="https://hackmd.io/_uploads/HkAsuKOR6.png" style="width:580px;margin-top:24px;margin-bottom:12px"/> In the example above, the operator with the boosted exits mode enabled has delayed validators. As a result, the protocol switched to exiting <span style="color:#B8B8B8">validators from other operators</span> to cover the demand in WQ. After covering the demand, the second stage of the algorithm requested the next <span style="color:#FF8E76">validators to exit</span> from the operator with the boosted exits mode enabled. :::info - The predicate uses the state from the previous stage to ensure that all previously requested validators are accounted for - The selection of validators occurs only from among targeted validators to boosted exit - If an operator with the boosted exits mode enabled delays the exit, the demand in WQ will be covered by other operators ::: ## 🙈 Out of Scope ### Exit Specific Validators The current solution does not cover the scenario of requesting the exit of specific validators of an operator. Validators are requested in the sequence from the smaller index to the larger index, i.e. in the order in which they were deposited. However, there may be a situation in which it is necessary to request validators to be exited out of the order in which they were deposited, for example if the validator's private key has been lost or the DAO suspects that the validator's private key may have been compromised. The exit of specific validators is addressed in another research and is covered by the Priority Exit Bus (PEB) functionality, which allows requesting validators to exit in priority order and without respect to demand in the WQ. ## 💖 Acknowledgements The document is partially based on the works of Raman S and KRogLA: - [Forced Target Limits](https://hackmd.io/@lido/HyOsz8Oap) - [VEBO modify exit order](https://hackmd.io/@F4ever/rkTV5g3Fa)