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:
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.
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.
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
– Disabled1
– Limited stake, smooth exit mode2
– Limited stake, boosted exit modeLet'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:
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.
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. 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 section.
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.
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.
These two values allow the module to be in one of three states at any given time:
currentShare < stakeShareLimit
. The proposed design makes no changes for this state. Everything remains as it is:
stakeShareLimit <= currentShare <= priorityExitShareThreshold
.
The proposed design makes no changes for this state. Everything remains as it is:
priorityExitShareThreshold < currentShare
. It is the state that is affected by the proposed changes.
The proposed design suggests the following changes in the Staking Router contract interface:
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;
}
The change to the StakingModule
struct affects the response from some view methods, which may be used in external integrations and offchain tooling:
Tests show that backward compatibility remains for both offchain tools and possible onchain integrations: 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 section.
It is proposed to implement two stages of selecting validators for exit:
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:
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.
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.
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.
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.
targetLimitMode
set to boosted exits have a higher priority for exit than operators with the mode set to smooth limitThe 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:
Let's consider various examples below. Coral color indicates validators requested due to the enabled boosted target limit of the operator, grey color indicates validators requested in general order.
In the example above, only the first stage of the algorithm works. Demand in WQ is partially covered by boosted exits and partially by other validators in general order. The second stage returns 0 validators to exit because all validators of operators in boosted exit status have already been requested by this time.
In the example above, both stages of the algorithm work. Demand in WQ is fully covered by boosted exits, after which the second stage of the algorithm continues to exit validators until the targeted validators to boosted exit equals 0.
In the example above, the operator with the boosted exits mode enabled has delayed validators. As a result, the protocol switched to exiting validators from other operators to cover the demand in WQ. After covering the demand, the second stage of the algorithm requested the next validators to exit from the operator with the boosted exits mode enabled.
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.
The document is partially based on the works of Raman S and KRogLA: