Skip to main content

Lido

Liquid staking pool and a related ERC-20 rebasing token (stETH)

What is Lido?

Lido is a liquid staking pool and the core contract that is responsible for:

  • accepting users' stake, buffering it and minting respective amounts of liquid token
  • do a proper accounting based on received oracle reports and the current state of the protocol
  • collecting withdrawals, priority fees and MEV from respective vaults into the buffer
  • applying fees and distributing rewards
  • passing buffered ether further to StakingRouter or WithdrawalQueueERC721

Also, Lido is an ERC-20 rebasing token, which represents staked ether, stETH. Tokens are minted upon ether submission and burned when redeemed. stETH holder balances are updated daily with oracle reports. It also implements the ERC-2612 permit and ERC-1271 signature validation extensions.

Other contracts are bound to the core and have the following responsibilities:

  • LidoLocator: protocol-wide address book which contains references to all meaningful parts of the Lido protocol on-chain
  • WithdrawalQueueERC721: a withdrawal requests FIFO queue and a respective NFT (unstETH)
  • StakingRouter: hub, which manages staking modules and distributes the stake among them
  • NodeOperatorsRegistry: original module, responsible for managing the curated set of node operators
  • OracleReportSanityChecker: helper for validation of oracle report parameters and smoothening token rebases
  • Burner: vault to contain stETH that ought to be burned on oracle report
  • WithdrawalVault: vault to collect partial and full withdrawals coming from the Beacon Chain
  • LidoExecutionLayerRewardsVault: vault to collect priority fees and MEV rewards coming from validators of the pool
  • DepositSecurityModule: protection from deposit frontrunning vulnerability
  • AccountingOracle: oracle committee, which gathers an accounting report for the protocol
  • EIP712StETH: ad-hoc helper to implement ERC-2612 permit for Solidity 0.4.24 Lido contract

Submit

Lido contract is a main entry point for stakers. To take part in the pool, a user can send some ETH to the contract address and the same amount of stETH tokens will be minted to the sender address. Submitted ether is accumulated in the buffer and can be passed further to WithdrawalQueueERC721 to fulfill withdrawal requests or to StakingRouter to deposit as a new validator stake.

To withdraw the underlying ETH back, a user may use the WithdrawalQueueERC721 contract or swap the token on the secondary market (it may be a cheaper and faster alternative).

Deposit

User-submitted ether is stored in the buffer and can be later used for withdrawals or passed further to StakingRouter to be used as validator's deposits. It happens asynchronously and uses DepositSecurityModule as a guard to prevent deposit frontrunning vulnerability.

Redeem

The token might be redeemed for ether through the protocol using the WithdrawalQueueERC721 contract leveraging staking withdrawals enabled with the Shanghai/Capella (aka "Shapella") Ethereum hardfork.

Rebase

When an oracle report occurs, the supply of the token is increased or decreased algorithmically, based on staking rewards (or slashing penalties) on the Beacon Chain, execution layer rewards (starting from the Merge Ethereum upgrade) or fulfilled withdrawal requests (starting from Lido V2). A rebase happens when oracle reports beacon stats.

The rebasing mechanism is implemented via the "shares" concept. Instead of storing a map with account balances, Lido stores which share of the total pool is owned by the account. The balance of an account is calculated as follows:

balanceOf(account) = shares[account] * totalPooledEther / totalShares
  • shares - map of user account shares. Every time a user deposits ether, it is converted to shares and added to the current user shares amount.

  • totalShares - the sum of shares of all accounts in the shares map

  • totalPooledEther - a sum of three types of ether owned by protocol:

    • buffered balance - ether stored on contract and hasn't been deposited or locked for withdrawals yet
    • transient balance - ether submitted to the official Deposit contract but not yet visible in the beacon state
    • beacon balance - the total amount of ether on validator accounts. This value is reported by oracles and makes the strongest impact on stETH total supply change

For example, assume that we have:

totalShares = 5
totalPooledEther = 10 ETH
sharesOf(Alice) -> 1
sharesOf(Bob) -> 4

Therefore:

balanceOf(Alice) -> 2 tokens which corresponds to 2 ETH
balanceOf(Bob) -> 8 tokens which corresponds to 8 ETH

On each rebase totalPooledEther normally increases, indicating that there were some rewards earned by validators, that ought to be distributed, so the user balance gets increased as well automatically, despite their shares remaining as they were.

totalPooledEther = 15 ETH
// user balance increased
balanceOf(Alice) -> 3 tokens which corresponds to 3 ETH now
balanceOf(Bob) -> 12 tokens which corresponds to 12 ETH now
// shares remain still
sharesOf(Alice) -> 1
sharesOf(Bob) -> 4
note

Since the balances of all token holders change when the amount of total pooled ether changes, this token cannot fully implement the ERC-20 standard: it only emits Transfer events upon explicit transfer between holders. In contrast, when the total amount of pooled ether increases, no Transfer events are generated: doing so would require emitting an event for each token holder and thus running an unbounded loop.

Oracle report

One of the cornerstones of the Lido protocol is the oracle report, that usually (but not guaranteed) once a day provides the protocol with the data that can't be easily accessed on-chain, but is required for precise accounting. It includes some Beacon chain stats as well as corresponding EL-side values that are valid on the reporting block and the decision data required to fulfill pending withdrawal requests.

  • Beacon chain stats:
    • the total number of validators managed by the pool
    • the total balance of validators managed by the pool
  • Historical EL values:
    • withdrawal vault balance
    • execution layer rewards vault balance
    • burner stETH shares balance
  • Withdrawal-related data
    • requests in the queue to be finalized
    • share rate to be used for finalization

Oracle report is processed in 9 simple steps:

  1. Memorize the pre-state that will be required for incremental updates of the protocol balance
  2. Validate the report data using OracleReportSanityChecker
  3. Calculate the amount of ether to be locked on WithdrawalQueueERC721 and move the respective amount of shares to be burnt to Burner
  4. Using OracleReportSanityChecker calculate the amounts of ether that can be withdrawn from LidoExecutionLayerRewardsVault and WithdrawalVault as well as the number of shares that can be burnt from Burner to avoid the rebase that can be easily frontrun.
  5. Collect the calculated amounts of ether from vaults and proceed with withdrawal requests finalization: send requested ether to WithdrawalQueue
  6. Burn the previously requested shares from Burner for withdrawals or coverage application
  7. Distribute rewards and protocol fees minting new stETH for the respective parties
  8. Complete token rebase by informing observers (emit an event and call the external receivers if any)
  9. Post-report sanity check for share rate provided with the report

So, the observable outcome of the report for the protocol is the following:

  • withdrawal requests in the queue are fulfilled
  • ether is collected from withdrawal and EL rewards vaults to the buffer
  • CL balance is updated according to the report
  • rewards are distributed among stakers, staking modules and protocol treasury

Standards

Contract implements the following Ethereum standards:

fallback

Sends funds to the pool and mints StETH tokens to the msg.sender address

function() payable
note

Allows users to submit their funds by sending it to the contract address

submit()

Sends funds to the pool with the optional _referral parameter and mints StETH tokens to the msg.sender address.

See https://lido.fi/referral for referral program details.

function submit(address _referral) payable returns (uint256)
ParameterTypeDescription
_referraladdressOptional referral address

Returns the number of StETH shares generated.

getBufferedEther()

Returns the amount of ether temporarily buffered on the contract's balance.

function getBufferedEther() view returns (uint256)
note

The buffered balance is kept on the contract from the moment the funds are received from a user until the moment they are sent to the official Deposit contract or WithdrawalsQueueERC721

isStakingPaused()

Returns staking state: whether it's paused or not.

function isStakingPaused() view returns (bool)
note

'staking' here means the ability to accept new submit requests

getCurrentStakeLimit()

Returns how much ether can be staked in the current block.

function getCurrentStakeLimit() view returns (uint256)
note

Special return values:

  • 2^256 - 1 if staking is unlimited;
  • 0 if staking is paused or if the limit is exhausted.

getStakeLimitFullInfo()

Returns full info about current stake limit parameters and state.

function getStakeLimitFullInfo() view returns (
bool isStakingPaused,
bool isStakingLimitSet,
uint256 currentStakeLimit,
uint256 maxStakeLimit,
uint256 maxStakeLimitGrowthBlocks,
uint256 prevStakeLimit,
uint256 prevStakeBlockNumber
)
NameTypeDescription
isStakingPausedboolStaking pause state (equivalent to return of isStakingPaused())
isStakingLimitSetboolWhether the stake limit is set or not
currentStakeLimituint256Current stake limit (equivalent to return of getCurrentStakeLimit())
maxStakeLimituint256Max stake limit
maxStakeLimitGrowthBlocksuint256Blocks needed to restore max stake limit from the fully exhausted state
prevStakeLimituint256Previously reached stake limit
prevStakeBlockNumberuint256Previously seen block number

deposit()

Deposit buffered ether to the StakingRouter's module with the id of _stakingModuleId.

Can be called only by DepositSecurityModule contract.

function deposit(uint256 _maxDeposits, uint256 _stakingModuleId, bytes _depositCalldata)
ParameterTypeDescription
_maxDepositsuint256Number of max deposit calls
_stakingModuleIduint256Id of the staking module to be deposited
_depositCalldatabytesmodule calldata

getDepositableEther()

Returns the amount of ether available to deposit.

function getDepositableEther() view returns (uint256)

canDeposit()

Returns true if depositing buffered ether to the consensus layer is allowed.

function canDeposit() view returns (bool)

handleOracleReport()

Updates accounting stats, collects EL rewards and distributes collected rewards if beacon balance increased, performs withdrawal requests finalization.

Can be called only by AccountingOracle contract.

function handleOracleReport(
uint256 _reportTimestamp,
uint256 _timeElapsed,
uint256 _clValidators,
uint256 _clBalance,
uint256 _withdrawalVaultBalance,
uint256 _elRewardsVaultBalance,
uint256 _sharesRequestedToBurn,
uint256[] _withdrawalFinalizationBatches,
uint256 _simulatedShareRate
) returns (uint256[4] postRebaseAmounts)
ParameterTypeDescription
_reportTimestampuint256The moment of the oracle report calculation
_timeElapseduint256Seconds elapsed since the previous report calculation
_clValidatorsuint256Number of Lido validators on Consensus Layer
_clBalanceuint256Sum of all Lido validators' balances on Consensus Layer
_withdrawalVaultBalanceuint256Withdrawal vault balance on Execution Layer
_elRewardsVaultBalanceuint256elRewards vault balance on Execution Layer
_sharesRequestedToBurnuint256shares requested to burn through Burner
_withdrawalFinalizationBatchesuint256[]the ascendingly-sorted array of withdrawal request IDs to finalize
_simulatedShareRateuint256share rate simulated by oracle when (1e27 precision)

Returns a fixed array of 4 values that represents changes made during the report

NameTypeDescription
postRebaseAmounts[0]uint256postTotalPooledEther amount of ether in the protocol
postRebaseAmounts[1]uint256postTotalShares amount of shares in the protocol
postRebaseAmounts[2]uint256withdrawals withdrawn from the withdrawals vault
postRebaseAmounts[3]uint256elRewards withdrawn from the execution layer rewards vault

getTotalPooledEther()

Returns the entire amount of ether controlled by the protocol

function getTotalPooledEther() view returns (uint256)
note

The sum of all ETH balances in the protocol, equals to the total supply of stETH.

getTotalELRewardsCollected()

Returns the total amount of execution layer rewards collected to Lido contract buffer.

function getTotalELRewardsCollected() view returns (uint256)

getBeaconStat()

Returns the tuple of key statistics related to the Beacon Chain.

function getBeaconStat() view returns (
uint256 depositedValidators,
uint256 beaconValidators,
uint256 beaconBalance
)
NameTypeDescription
depositedValidatorsuint256Number of the ever deposited Lido-participating validators
beaconValidatorsuint256Number of Lido's validators visible in the Beacon state, reported by oracles
beaconBalanceuint256Total amount of Beacon-side ether (sum of all the balances of Lido validators)
note

depositedValidators is always greater or equal to beaconValidators

|

receiveELRewards()

A payable function for execution layer rewards. Can be called only by the LidoExecutionLayerRewardsVault contract.

function receiveELRewards() payable

receiveWithdrawals()

A payable function for withdrawals acquisition. Can be called only by WithdrawalVault.

function receiveWithdrawals() payable

Protocol levers

stop()

Stop pool routine operations.

Can be called only by the bearer of PAUSE_ROLE

function stop()

resume()

Resume pool routine operations.

Can be called only by the bearer of RESUME_ROLE

function resume()

pauseStaking()

Stops accepting new ether to the protocol.

Can be called only by the bearer of STAKING_PAUSE_ROLE

function pauseStaking()
note

While accepting new ether is stopped, calls to the submit function, as well as to the default payable function, will revert.

resumeStaking()

Resumes accepting new ether to the protocol (if pauseStaking was called previously).

Can be called only by the bearer of STAKING_CONTROL_ROLE

function resumeStaking()
note

Staking could be rate-limited by imposing a limit on the stake amount at each moment in time, see setStakingLimit() and removeStakingLimit().

setStakingLimit()

Sets the staking rate limit.

Can be called only by the bearer of STAKING_CONTROL_ROLE

Limit explanation scheme:

    * ▲ Stake limit
* │..... ..... ........ ... .... ... Stake limit = max
* │ . . . . . . . . .
* │ . . . . . . . . .
* │ . . . . .
* │──────────────────────────────────────────────────> Time
* │ ^ ^ ^ ^^^ ^ ^ ^ ^^^ ^ Stake events
function setStakingLimit(uint256 _maxStakeLimit, uint256 _stakeLimitIncreasePerBlock)
ParameterTypeDescription
_maxStakeLimituint256Max stake limit value
_stakeLimitIncreasePerBlockuint256Stake limit increase per single block
note

Reverts if:

  • _maxStakeLimit == 0
  • _maxStakeLimit >= 2^96
  • _maxStakeLimit < _stakeLimitIncreasePerBlock
  • _maxStakeLimit / _stakeLimitIncreasePerBlock >= 2^32 (only if _stakeLimitIncreasePerBlock != 0)

removeStakingLimit()

Removes the staking rate limit.

Can be called only by the bearer of STAKING_CONTROL_ROLE

function removeStakingLimit()

unsafeChangeDepositedValidators()

Unsafely change deposited validators counter.

The method unsafely changes the deposited validator counter. Can be required when onboarding external validators to Lido (i.e., had deposited before and rotated their type-0x00 withdrawal credentials to Lido).

Can be called only by the bearer of UNSAFE_CHANGE_DEPOSITED_VALIDATORS_ROLE

ParameterTypeDescription
_newDepositedValidatorsuint256New value for the deposited validators counter
warning

The method might break the internal protocol state if applied incorrectly

name()

Returns the name of the token.

function name() view returns (string)
note

Always returns Liquid staked Ether 2.0.

symbol()

Returns the symbol of the token.

function symbol() view returns (string)
note

Always returns stETH.

decimals()

Returns the number of decimals for getting user representation of a token amount.

function decimals() view returns (uint8)
note

Always returns 18.

totalSupply()

Returns the number of tokens in existence.

function totalSupply() view returns (uint256)
note

Always equals to getTotalPooledEther() since the token amount is pegged to the total amount of ether controlled by the protocol.

balanceOf()

Returns the number of tokens owned by the _account

function balanceOf(address _account) view returns (uint256)
note

Balances are dynamic and equal the _account's share in the amount of total ether controlled by the protocol. See sharesOf.

allowance()

Returns the remaining number of tokens that _spender is allowed to spend on behalf of _owner through transferFrom(). This is zero by default.

function allowance(address _owner, address _spender) view returns (uint256)
ParameterTypeDescription
_owneraddressAddress of owner
_spenderaddressAddress of spender
note

This value changes when approve() or transferFrom() is called unless the allowance is infinite (2^256)

approve()

Sets _amount as the allowance of _spender over the caller's tokens

function approve(address _spender, uint256 _amount) returns (bool)
ParameterTypeDescription
_spenderaddressAddress of spender
_amountuint256Amount of tokens

Returns a boolean value indicating whether the operation succeeded.

note

Requirements:

  • _spender cannot be the zero address.
  • the contract must not be paused.

increaseAllowance()

Atomically increases the allowance granted to _spender by the caller by _addedValue.

This is an alternative to approve() that can be used as a mitigation for problems described here.

function increaseAllowance(address _spender, uint256 _addedValue) returns (bool)
ParameterTypeDescription
_senderaddressAddress of spender
_addedValueuint256Amount of tokens to increase allowance

Returns a boolean value indicating whether the operation succeeded

note

Requirements:

  • _spender address cannot be zero.
  • the contract must not be paused.

decreaseAllowance()

Atomically decreases the allowance granted to _spender by the caller by _subtractedValue.

This is an alternative to approve() that can be used as a mitigation for problems described here.

function decreaseAllowance(address _spender, uint256 _subtractedValue) returns (bool)
ParameterTypeDescription
_senderaddressAddress of spender
_subtractedValueuint256Amount of tokens to decrease allowance

Returns a boolean value indicating whether the operation succeeded.

note

Requirements:

  • _spender cannot be the zero address.
  • _spender must have an allowance for the caller of at least _subtractedValue.
  • the contract must not be paused.

transfer()

Moves _amount tokens from the caller's account to the _recipient account.

function transfer(address _recipient, uint256 _amount) returns (bool)
ParameterTypeDescription
_recipientaddressAddress of tokens recipient
_amountuint256Amount of tokens to transfer

Returns a boolean value indicating whether the operation succeeded.

note

Requirements:

  • _recipient cannot be the zero address or stETH contract itself.
  • the caller must have a balance of at least _amount.
  • the contract must not be paused.

transferFrom()

Moves _amount tokens from _sender to _recipient using the allowance mechanism. _amount is then deducted from the caller's allowance.

function transferFrom(
address _sender,
address _recipient,
uint256 _amount
) returns (bool)
ParameterTypeDescription
_senderaddressAddress of spender
_recipientaddressAddress of recipient
_amountuint256Amount of tokens

Returns a boolean value indicating whether the operation succeeded.

note

Requirements:

  • _sender cannot be the zero addresses.
  • _recipient cannot be the zero address or stETH contract itself.
  • _sender must have a balance of at least _amount.
  • the caller must have an allowance for _sender's tokens of at least _amount.
  • the contract must not be paused.

getTotalShares()

Returns the total amount of shares in existence.

function getTotalShares() view returns (uint256)

sharesOf()

Returns the number of shares owned by _account

function sharesOf(address _account) view returns (uint256)

getSharesByPooledEth()

Returns the number of shares that corresponds to _ethAmount of the protocol-controlled ether.

function getSharesByPooledEth(uint256 _ethAmount) view returns (uint256)

getPooledEthByShares()

Returns the amount of ether that corresponds to _sharesAmount token shares.

function getPooledEthByShares(uint256 _sharesAmount) view returns (uint256)

transferShares()

Moves token shares from the caller's account to the provided recipient account.

function transferShares(address _recipient, uint256 _sharesAmount) returns (uint256)
ParameterTypeDescription
_recipientaddressAddress of shares recipient
_sharesAmountuint256Amount of shares to transfer

Returns the number Amount of transferred tokens.

note

Requirements:

  • _recipient cannot be the zero address or stETH contract itself.
  • the caller must have at least _sharesAmount shares.
  • the contract must not be paused.

transferSharesFrom()

Moves _sharesAmount token shares from the _sender account to the _recipient using the allowance mechanism. The amount of tokens equivalent to _sharesAmount is then deducted from the caller's allowance.

function transferSharesFrom(
address _sender,
address _recipient,
uint256 _sharesAmount
) returns (uint256)
ParameterTypeDescription
_senderaddressAddress of spender
_recipientaddressAddress of recipient
_sharesAmountuint256Amount of shares

Returns the number of transferred tokens.

note

Requirements:

  • _sender cannot be the zero address.
  • _recipient cannot be the zero address or stETH contract itself.
  • _sender must have at least _sharesAmount shares.
  • the caller must have an allowance for _sender's tokens of at least getPooledEthByShares(_sharesAmount).
  • the contract must not be paused.

nonces()

Returns the current nonce for the owner. This value must be included whenever a signature is generated for an ERC-2612 permit.

function nonces(address owner) view returns (uint256)

DOMAIN_SEPARATOR()

Returns the domain separator used in the encoding of the signature for the ERC-2612 permit, as defined by EIP-712.

function DOMAIN_SEPARATOR() view returns (bytes32)

permit()

Sets value as the allowance of spender over owner's tokens, given owner's signed approval.

Emits an Approval event.

function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)
ParameterTypeDescription
owneraddressAddress of spender
spenderaddressAddress of recipient
valueuint256Allowance to set
deadlineuint256Permit expiry time
vuint8extended secp256k1 signature
rbytes32extended secp256k1 signature
sbytes32extended secp256k1 signature
note

Requirements:

  • spender cannot be the zero address.
  • deadline must be a timestamp in the future.
  • v, r and s must be a valid secp256k1 signature from owner over the EIP712-formatted function arguments.
  • the signature must use owner's current nonce (see nonces).

eip712Domain()

Returns the fields and values that describe the domain separator used by this contract for EIP-712

function eip712Domain() view returns (
string name,
string version,
uint256 chainId,
address verifyingContract
)

General Methods

getLidoLocator()

Returns the address of LidoLocator.

function getLidoLocator() view returns (address)

getContractVersion()

Returns the current contract version.

function getContractVersion() view returns (uint256)
note

Always returns 2.

transferToVault()

Overrides default AragonApp behavior to disallow recovery.

function transferToVault(address _token)
ParameterTypeDescription
_tokenaddressToken to be sent to recovery vault
note

Always reverts with NOT_SUPPORTED reason