Try โ€‚โ€‰HackMD

Lido on Ethereum. Protocol accounting with enabled withdrawals

The document outlines accounting principles and changes accomodated in Lido V2 with the withdrawals 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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

Transient balance handling was included with the adoption of LIP-1.

post-Merge update

The Merge event added another protocol income source โ€” execution layer rewards (block proposer accrues priority fee and MEV). Validators participating in the Lido protocol specify these rewards be sent to a separate vault contract. 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.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

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 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):

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

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).
  2. 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.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

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().

Pre-withdrawals

Pre-merge rewards were defined as:

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:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

Post-merge update

A Merge-ready version of the rewards estimation:

rewards = (beacon balance new - beacon balance old) - (appeared validators x 32 ETH) 
+ withdrawn from execution layer rewards vault

The updated example:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

Post-withdrawals

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)

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More โ†’

Commission (fee)

Comission distribution happens by minting additional shares, see distributeFee

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.

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.

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:
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)
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