## Glossary `TW` – triggerable withdrawals; `Partial deposits` – sending less than the maximum effective balance (2048 Eth, as if we send less than 32 validator will not be active, on keys 0x01 will always send 32 Eth) for a validator; `Top-up` – top up balance of validator on some value; `Partial withdrawals` – an operation to withdraw only part of a validator’s balance via EIP-7002; `New Allocation Strategy (AS)` – new algorithm to distribute stake across modules based on target share (TODO: add other characteristics..) and across operators of one module based on it's validator efficiency, fee, balance and so on. Module allocation strategy can be implemented as part of SR or as separate contract, this document doesn't force solution for AS. Allocation for operators can be a separate contract `Valmart`. `Valmart` - is a contract that determines which operator of module to deposit or from which operator to exit validators based on list of consumptions. ## Rebalancing Currently, the deposit flow doesn't allow rebalancing within the protocol, and rebalancing happens occasionally via withdrawals and fresh deposits. In SRv3, we want deposits and withdrawals to work coherently to ensure proper stake distribution based on operator weights (e.g., module load, validator efficiency, etc.). The target distribution will be managed by the New allocation strategy, that is subject of different document (TODO: add link). The new Allocation Strategy contract will be used in both deposits and withdrawals flows. ### Deposits Deposits should be well-balanced across modules and operators. The Staking Router (SR), leveraging a new Allocation Strategy, will determine how much ETH to allocate to each module. Deposits algorithm should rely on validator balances rather than the number of keys, and must support keys with both credential types: 0x01 and 0x02. Although initial deposits will not exceed 32 ETH, the SR will support topping up validator balances to reach the maximum effective balance. The choice of key type (0x01 or 0x02) will be determined at the module level, but each module will be required to support only one type of key to not complicate logic. Deposits will occur via two distinct flows: - Initial deposit of 32 ETH to activate new validator keys; - Top-up deposits for keys that have already been deposited (activated?). #### Key activation ![ADR for SR v3.0 - Frame 1](https://hackmd.io/_uploads/rJXU1grLle.jpg) 1. Guardians will run the Council Daemon, check vetted keys, and look for frontruns. We will need to add verification for both types of credentials (modifying event fetching, frontrun and signature checking); they will send signed messages to a queue monitored by the Depositor Bot; 2. Once a quorum of messages is received, the Depositor Bot calls depositBufferedEther on the DepositSecurityContract if the Lido buffer has ETH available; 3. `DepositSecurityContract`, after performing checks, calls the Lido.deposit method for the staking module; 4. `Lido.deposit` checks the new Allocation Strategy. The strategy considers module balances, target shares, and other significant factors, and returns the amount of ETH to be deposited in the module; 5. Within `Lido.deposit`, it calls `StakingRouter.deposit` with the ETH amount to allocate to the module; 6. `StakingRouter.deposit` calls `IStakingModule(stakingModuleAddress).obtainDepositKeys` with the ETH amount to retrieve vetted, not earlier deposited keys the module wants to deposit. `obtainDepositKeys` should return a batch of keys and an array of signatures as it currently do. The Staking Router will store the module type and expose a view method `getKeyType(stakingModuleId)` that returns the key type: either `0x01` or `0x02`. The vetting mechanism in the Council Daemon should be updated to verify that the deposit signature corresponds to a deposit message with withdrawal credentials matching the module's key type. TODO: Add some contract interfaces, add code TODO: there was some places for optimization, like move reading maxDepositsCount (future maxDepositAmount) from DSM to method in SR that will return deposit amount ``` uint256 depositsCount = Math256.min( _maxDepositsCount, stakingRouter.getStakingModuleMaxDepositsCount(_stakingModuleId, getDepositableEther()) ); ``` #### Partial deposits via proofs ![ADR for SR v3.0 - Frame 1 (4)](https://hackmd.io/_uploads/SJJ36lSLeg.jpg) To not give complicate current logic of creation of validator via `depositBufferedEth` it is suggested to separate top up in different method `depositPartial`. Creation of validator logic contain checks that we don't need to do for top-ups like change of nonce of module, deposit root change. Also to not reconsider security assumptions for valdiators creation flow. It is also will give flaxibility for top-up particular validator. For top-up validators we need to check: - validator created on CL and has lido withdrawal credential - it's type is `0x02` - it doesnt reach effective balance yet - want to allocate more Eth to operator It is suggested to make this checks offchain and bring to DSM proof. Proof will take risk from trusted actors (guardians) making top-up permitionless operation (TODO: is it permisionless or we need to cover with role? ). Make top up more precise operation, bot will check CL effective balance and bring precize amount we need. To not create new tooling it is suggested implement this part as Depositor Bot module. Depositor bot can be run permisionless by everyone. 1. `Bot` get operator stake distribution from `Valmart` for module, decide which validators of operator to top up, check CL, form proofs and call DSM `depositPartial` to top up modules valdiators. ``` { uint256 keyIndex, uint256 operatorId, bytes pubkey, uint256 topupAmount, uint256 curentBalance, bytes32[] proof } ``` 2. `DSM` check proofs (`CLVerifier` part of `DSM` check validator belongs to operator of module, after top-up balance of validator will not exceed it's max effective balance, balance correct) and proceed to Lido with top-up values. Lido check amount allowed for distribution for module based on New allocation strategy. Will be used buffered Eth, direct deposits to module and check limits for max deposit amount per block. Revert if checks didn't pass. 3. After all checks Lido pass partial deposit values to SR. SR proceed to Staking Module via `verifyPartialDeposit` method. Module check belonging keys to operator and that top ups fit operator target share based on Valmart. 4. After Module checks passed SR will top-up keys balances. Weak side of this algrithm is that top-ups dependent from availability of off-chain service. So maybe we will need reconsider limits to not create new keys when we have to much validators with not effective balance. #### Alternative solution without proofs Make `depositsBUfferedEther` common for new deposits and top-ups. This solution require storage for ### Withdrawals Withdrawals are the second pillar of rebalancing. To support stake reallocation, we will add a "delta" to the withdrawal value. 'delta' will be driven by the Allocation Strategy. This validator exit batch will support both withdrawals and redeposits for rebalancing. There are several options of implementation: - Full withdrawals via voluntary exits, tw; - Partial withdrawals without voluntary exits, tw if we need to withdraw full balance; - Or a combined approach using X validators for partial withdrawals while preserving volunteer exits, tw. <!-- Covering demand with volunteer exits is free. Don't require a lot of changes; Less development hours than other options. But we may withdraw more ETH than needed — in some cases, up to ~2000 ETH. It is not fast. No guarantee: we might need to use TW (triggerable withdrawals), which is not free (has a fee). Algorithm based on partial withdrawals will provide faster coverage of withdrawal demand (has its own queue, separate from voluntary exits), guaranteed exit, No risk of withdrawing more ETH than needed. But it is not free and also it complicate development. --> Voluntary exits: - Free (no additional cost). - Requires fewer development hours. - But may withdraw more ETH than needed (e.g., ~2000 ETH). - Slower and has no guarantees — TW may be needed (which incurs fees). Partial withdrawals: - Faster demand coverage (own queue, not tied to volunteer exits). - Guaranteed exit. - No over-withdrawal. - Not free and adds development complexity. At this point, we prefer voluntary exits as a simpler method that supports both validator types and enables rebalancing. In the next iteration, we may explore a combined approach using X% of validators for partial withdrawals. This will require Vebo oracle changes. On-chain we will predefine the content of the oracle’s intent report for partial withdrawals on a validator. We will also support a partial withdrawals method guarded by role. #### Oracle part The VEBO oracle is responsible for selecting validators to cover withdrawal demand. As the first step, VEBO will provide the required ETH amount to the New Allocation Strategy contract, which will return a priority list of modules to be used for withdrawals. // module 1 - x eth, module 2 - y eth - cортируем по убыванию. 1 в списке тот, которые дисбалансирует стейк больше других <!-- Risk: speed of withdrawals. configuration for module, how much we can withdraw from module. Confiration for delta --> - If the selected module is a legacy module, the oracle will apply a modified version of the existing predicates: it will prioritize operators based on the balance of their validators, rather than the number of keys. - If the selected module is a new Allocation Strategy module, it will also suggest which operators to prioritize. The oracle will then choose validators from this operator set based on factors such as validator age. The consolidation queue, partial withdrawals validators - dont use; выбор валидатора - черный ящик; <!-- - - if it is source , dont use; if it target too; so, or the partial withdrawals queue. --> The VEBO report should be updated to include the requested withdrawal amount (in ETH) and, for each exit entry, the balance of the operator’s validator: ``` /// MSB <------------------------------------------------------------------- LSB /// | 3 bytes | 5 bytes | 8 bytes | 48 bytes | /// | moduleId | nodeOpId | validatorIndex | validatorPubkey | balance | ``` This structure also enables future support for partial deposits via VEBO. #### Onchain part - Exit requests in the VEBO contract are limited by the ExitLimitUtils library. This library currently uses a maxExitLimits value per a fixed time period. Instead, it should use balance-based logic: maxExitLimits * 32 ETH. - Oracle sanity checker: The values such as exitedValidatorsPerDayLimit and maxValidatorExitRequestsPerReport should be based on total balance, rather than the number of validators. As technical debt cleanup, we also propose a new VEBO architecture: Split VEBO into two contracts: - Oracle contract for reporting. - Validator Exit Bus contract. TODO: Validator Exit Bus contract will use message bus Draft VEBO update scheme: https://miro.com/app/board/uXjVInc8cwA=/ Validator Exit Bus Oracle contract: - Supports new report format with validator balances. - Sanity checks based on balance instead number of validators. - Oracle sends exit request hash and data to the VEB contract. Validator Exit Bus contract: Two flows: - Full withdrawals via voluntary exits with next possibility to exit via 7002 (as it implemented in TW release) - Trusted partial withdrawal flow. - Uses balance-based limit checks before processing. - maxValidatorsPerReport replaced with balance-based limit. - VEB emits the voluntary exit event and stores the hash in VEB to trigger EIP-7002 exits later. Risk: loose events #### New trust assumption - The oracle could misreport validator balances — e.g., claiming 32 ETH while the actual balance is 2048 ETH. This could lead to reward loss, but not loss of stake. #### TWG `triggerFullWithdrawals` is limited by ExitLimitUtils, which should use balance instead of the number of exits. The main method must be modified to support balance-based checks.