--- tags: withdrawals, explainer author: Eugene Mamin title: Lido on Ethereum. Protocol accounting with enabled withdrawals --- # Lido on Ethereum. Protocol accounting with enabled withdrawals The document outlines accounting principles and changes accomodated in [Lido V2](https://blog.lido.fi/lido-v2-launch/) with the [withdrawals](https://consensys.io/shanghai-capella-upgrade) support (post Shanghai/Capella hardfork) for the Lido on Ethereum protocol. ## Historical context ### Protocol total value locked calculation. Pre-withdrawals Lido protocol total value locked (TVL) is designed to be represented with `stETH` token total supply (ERC20-compatible, fungible). ##### pre-Merge state The main component of `stETH` token total supply is the balance of Lido-participating validators on the Beacon Chain (now it's Consensus Layer) side. The complete total supply also includes the following additions: - buffered ether amount (to be deposited on Beacon Chain) - transient ether balance (have a deposited validator, but not activated yet) `stETH total supply` = `beacon balance` + `buffered balance` + `transient balance` ![](https://hackmd.io/_uploads/rkSz0E8Os.jpg) >Transient balance handling was included with the adoption of [LIP-1](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-1.md). ##### post-Merge update The Merge event added another protocol income source — [execution layer rewards](https://docs.lido.fi/#execution-layer) (block proposer accrues priority fee and MEV). Validators participating in the Lido protocol specify these rewards be sent to a separate [vault contract](https://docs.lido.fi/contracts/lido-execution-layer-rewards-vault). The core Lido contract withdraws the accrued funds as part of each oracle report appending to the `buffered balance`. This way, the formula above stays valid: `ExecutionLayerRewardsVault` is included into TVL implicitly by Oracle-guided "back-staking", see [LIP-12](https://research.lido.fi/t/lip-12-on-chain-part-of-the-rewards-distribution-after-the-merge/1625). ![](https://hackmd.io/_uploads/HJVNCEI_j.jpg) ## Current approach for Lido V2 ### Beacon balance 'splitting' With enabled withdrawals, `beacon balances` can be split according to the following rules: - if a validator has more than 32 ETH (i.e., `MAX_EFFECTIVE_BALANCE`), extra amounts are to be skimmed (partially withdrawn) to `withdrawal credentials` address (if `0x01` type is used) - once a validator exited, all balance would be withdrawn to `withdrawals credentials` (if `0x01`) Withdrawals represented via [special operations](https://eips.ethereum.org/EIPS/eip-4895) are included into a block execution payload. Thus, tracking the withdrawn funds when TVL updates is essential, especially considering consequent oracle reports. As a simple example, suppose there is only one validator (`v1`) and no buffered/transient ether: `stETH total supply` = `v1 balance` + `withdrawals vault balance` The simplified single Lido validator lifecycle case is presented below (comparing two Ethereum network underlying mechanics in action: pre-/post- withdrawals): ![](https://hackmd.io/_uploads/r1eRcBIdo.jpg) Design points to update TVL calculation: 1. Oracle **MUST** provide reports of `beacon balance` and `withdrawals vault balance` at the same (i.e., synchronized) point of time to track validators' balances updates and prevent double-accounting. It is a crucial point of the described withdrawals-enabled accounting approach due to the asynchronous and autonomous nature of withdrawals on Ethereum: the Lido protocol can't use the balance of `withdrawals vault` at the arbitrary moment because it's coupled with `beacon balance` (which is inaccessible from the Execution Layer side without oracle). 3. If on each report Oracle withdraws `withdrawals vault balance` and appends the funds to `buffered balance`, than `withdrawals vault balance` can be safely omitted in the TVL formula (similar to `ExecutionLayerRewardsVault` approach). ### `stETH` withdrawal fulfillment Withdrawal requests get accumulated via `WithdrawalQueue` by locking `stETH` on balance. Then, the following actions are performed as part of the Oracle report: - withdraw & append execution layer rewards to `buffered balance` - withdraw & append `withdrawals vault balance` to `buffered balance` - transfer some funds from `buffered balance` to `WithdrawalsQueue balance` - fulfill a bunch of unfinalized yet Lido withdrawals - burns underlying `stETH` shares for the fulfilled withdrawal requests The last three steps lower TVL since `WithdrawalQueue balance` is not included in the `stETH` total supply. However, by shares burning, user balances get preserved via `stETH` rebasing mechanics. The scheme below outlines the Lido on Ethereum protocol design with enabled withdrawals. ![](https://hackmd.io/_uploads/ByoBANL_i.jpg) ## Appendixes The following appendixes provide additional details on rewards and commissions. ### Rewards calculation As part of each oracle report, the protocol needs to calculate rewards and distribute comissions (protocol fee). Rewards calculation happens inside [`Lido.handleOracleReport()`](https://github.com/lidofinance/lido-dao/blob/master/contracts/0.4.24/Lido.sol#L467). #### Pre-withdrawals Pre-merge rewards were defined as: ```rust appeared validators = (beacon validators new - beacon validators old) rewards = (beacon balance new - beacon balance old) - (appeared validators x 32 ETH) ``` An illustrative example is provided below: ![](https://hackmd.io/_uploads/SJWr9PIOi.jpg) ##### Post-merge update A Merge-ready version of the rewards estimation: ```rust rewards = (beacon balance new - beacon balance old) - (appeared validators x 32 ETH) + withdrawn from execution layer rewards vault ``` The updated example: ![](https://hackmd.io/_uploads/HJX8cvIus.jpg) #### Post-withdrawals ```rust rewards = (beacon balance new - beacon balance old) - (appeared validators x 32 ETH) + withdrawn from execution layer rewards vault + withdrawn from withdrawals vault ``` >NOTE: `withdrawn from withdrawals vault` **MUST** rely on `withdrawals vault balance` reported by Oracle (i.e., synchronized with `beacon balance`) ![](https://hackmd.io/_uploads/BJJp9D8uj.jpg) ### Commission (fee) Comission distribution happens by minting additional shares, see [`distributeFee`](https://github.com/lidofinance/lido-dao/blob/master/contracts/0.4.24/Lido.sol#L801) Fee distribution executes only if the beacon chain rewards (without the execution layer rewards) are positive. This criterion was introduced with the merge-ready Lido protocol upgrade and encourages validators to converge to a healhy state of profitable validation. ```rust if rewards - withdrawn from execution layer rewards vault > 0 { distributeFee(rewards) } ``` ### Limits Lido protocol mechanics (TVL calculation, staker balances tracking, withdrawals fulfillment) rely on the data, provided by the off-chain oracle running committee. To lower the attack surface, the protocol has on-chain security/sanity checks to prevent reporting malicious accounting data. Another known risk is that data can be correct itself, but the state transition is too drastic, which encourages short-term arbs lowering the rewards of the long-term protocol holders. For instance, `stETH` rebase must be limited to prevent oracle reports [sandwiching](https://github.com/lidofinance/lido-dao/issues/405). Terms: - `cap` — hard restriction, oracle tx reverts if the cap value exceeded - `limiter` — allows transferring up to the saturation amount, doesn't impose reverts #### Pre-withdrawals Current protocol limits: - APR sanity check **cap** per protocol TVL change: ```rust if (postReportTVL > preReportTVL) revert if ((postReportTVL - preReportTVL) > APR relative annual cap) else revert if ((preReportTVL - postReportTVL) > APR relative per-report cap) ``` - EL withdrawal **limiter** (2BP of TVL per report) - Coverage application **limiter** (4BP of TVL per report) #### Post-withdrawals It should be possible to restrict APR by: - max allowed APR change **cap** (with respect to period) for Consensus Layer rewards and penalties: - 10% increase (as an estimated APR) - 5% decrease (per signle report) ```rust if (newBeaconBalance > oldBeaconBalance) revert if ((newBeaconBalance - oldBeaconBalance) > APR relative annual cap) else revert if ((oldBeaconBalance - newBeaconBalance) > APR relative per-report cap) ``` - unified **limiter** preventing the single report arb - 27% increase (as APR) per single report >NOTE: The cap can't be applied for `withdrawals vault` balance to prevent protocol blocking when someone sends a massive ether amount. The unified limiter should follow these steps: 1) init limiter value with the chosen threshold of 27% 2) subtract the reported beacon APR change from the limiter value 3) withdraw no more than the limiter value from withdrawals vault, update the limiter 4) withdraw no more than the limiter value from el rewards vault, update the limiter 5) apply coverage using the remainder limiter value at maximum. Additional **caps**: - exited validators counter (can't exit more than 10 validators per epoch) - validators number change (can't activate more than 10 validators per epoch) - request id to finalize up to (check that block number is less than reported with the margin) - finalization price (check that the finalization price resembles APR) >NB: Amount of activations/exits scales with the amount of active validators and the limit is the active validator set divided by 64.000. > >Until 327680 active validators in the network, 4 validators can be activated per epoch. > >For every 65536 (=4 * 16384) active validator, the validator activation rate goes up by one. >The exactly same churn limit is applicable for exiting validators. > >10 validators per epoch requires 655360 active validators which translates to 2200 validators per day. > >There are 487656 active Ethereum validators at the moment of writing (7 validators per epoch). See: [Ethereum 2.0 Glossary // Validator Lifecycle](https://kb.beaconcha.in/glossary#2.-pending)