Try   HackMD

Lido protocol with enabled Ethereum withdrawals. Audit scope

  • Created: 2022-11-28
  • Updated: 2023-03-20

Simple summary

A protocol upgrade for Lido on Ethereum to allow stETH token to be redeemed to Ξ natively once Ethereum undergone Shanghai/Capella hardfork implementing withdrawals (EIP-4895).

The major features of the protocol upgrade beyond withdrawals:

  • A smart contracts architecture which eases diversification of the Lido's validators by facilitating onboarding process of the heterogenous validators subsets (e.g., community-driven validators or committees of DVT-enabled validators) through a staking router adapter contract.
  • An updated oracle contract consensus mechanics that allows delivering huge data chunks (virtually unbounded) via two-step procedure: reach a consensus about the data hash, and deliver the data itself in a batched manner.

Context

Withdrawals

Lido on Ethereum is a liquid staking protocol on Ethereum mainnet. At the moment, Lido staked ether token stETH could be redeemed to Ξ only via market exchange mechanics (incentivized liquidity pools as the main source) because withdrawals (i.e., Beacon Chain 'unstaking') are not implemented yet on Ethereum natively. The latter is a subject of change when Shanghai/Capella ('Shapella') hardfork got activated. It will enable stand-alone unstaking flow transferring funds from validators to their withdrawal credentials addresses (requires 0x01-type withdrawals credentials) via new system-level operation type when:

  • a validator has an excessive balance above 32 Ξ
  • a validator has become inactive (exited either voluntary or ejected by the network consensus if penalized/slashed)

Lido validators share the same 0x01-type withdrawal credentials by design which correspond to the WithdrawalsVault contract.

The Lido protocol upgrade allows collecting withdrawal requests from stETH holders via the WithdrawalQueue contract and fulfilling them using an off-chain oracle daemon. The off-chain committee-driven Oracle daemon executes a significant part of withdrawals due to the following major considerations:

  • Consensus Layer data (validators' states and balances) is unavailable (not exposed) on Execution Layer
  • There is no way to exit a validator by the command from Execution Layer

More details about withdrawals design landscape for Lido on Ethereum are desribed here:
https://research.lido.fi/t/withdrawals-for-lido-on-ethereum/3690

Staking router

Lido-participating Ethereum validators are currently represented by the curated set of node operators. In order to become a node operator in Lido, it is necessary to pass the assessment of the Lido Node Operator Sub Governance Group (LNOSG). This group accepts applications, evaluates and offers a shortlist, which is sent to the DAO for an on-chain approval via voting process. On approval each node operator is added to the NodeOperatorRegistry contract, which contains all node operators with their validators keys pre-approved by the Lido DAO.

To diversify Lido validators set and make it heterogeneous, there is the intermediate StakingRouter contract that incorporates previously known NodeOperatorRegistry as a kind of module with ability to add another modules (e.g., community-driven validators or committees of DVT-enabled validators).

Two-phase oracle

To support withdrawals for Lido, the part of the protocol that currently passes data from consensus layer to execution layer (the Oracle) has to be extended and reworked. More kinds of data are required, not only CL-derived state data, buf also diff data potentially unlimited amount (contains keys to be exited and exited keys since previous report for each protocol-participating node operator). Thus, Oracle has two different reporting intervals: exited keys are reported more frequently (a few hours) than other data (once a day).

Off-chain oracle consists of two functional modules.

  • The accounting module is responsible for protocol accounting and follows the current lifecycle (24h amortized period), though reporting more values than now (withdrawals requests to finalize, withdrawals vault balance corresponding to the report’s epoch, and exited validators for each node operator). This module interacts with the new AccountingOracle contract.
  • The ejection module performs validator exit requests signalling, estimating the needed validator exits amount and interacting with the new ValidatorExitBusOracle contract, using more agile report interval.

Both modules are running using the same oracle committee, but their lifecycle is uncoupled due to different asynchronous environment and report asumptions.

Documentation and sources

The picture below outlines the smart contracts architecture.

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 →

The architecture was presented on the Lido community call #3, see also Presentation slides.

General documentation

The protocol has been running on top of the Aragon framework.

Staking router docs
Withdrawals docs

Usage and purpose

Withdrawals

stETH holder interacts with Lido by locking tokens on WithdrawalQueue and receiving an enqueued position index in return. The position is monotonically increasing counter representing the place in the internal withdrawal requests queue. Locked stETH continues accumulating rewards on behalf of other protocol users, the original holder stops receiving a positive rebase since the block number of the successfully placed withdrawal request.

Upon the next oracle report, previously accumulated withdrawal requests are processed and finalized if eligible (timelock passed and enough funds to fulfill). Other requests are carried over to the next oracle report round. stETH shares are burned on behalf of the Oracle report together with decreasing the total pooled ether amount.

The logic of withdrawal requests unwinding and processing is implemented within the off-chain Oracle daemon (accounting module that delivers data to the AccountingOracle contract). The Oracle daemon provides the following data during the reports: the index of the last finalized withdrawal request, Lido validators balances, active validators number, exited validators number per each node operator, balance of the Lido withdrawal credentials address (all info is valid for the referece slot of the expected report).

The oracle must track all of the Lido-participating validators' exits (node operators could also initiate a voluntary exit without request from Lido) that occurred over the previous oracle round and modify their on-chain state stored within Lido protocol to process rewards and balance the newly submitted ether.

After the withdrawals requests have been processed and finalized, anyone can claim the ether corresponding to the provided finalized position index on behalf of the original requestor (recipient).

Another signaling off-chain process which has its own lifecycle, is needed to report the validator keys needs to be exited to fulfill withdrawal requests (ejection module that delivers data to the ValidatorExitBusOracle contract). This process could be executed on behalf of the main Oracle committee to request the exit of specific validators based on the amount of withdrawal requests, slashing conditions, status/performance of validators controlled by Lido, staked ether buffer size, execution layer rewards, and previously requested withdrawals. The order of the proposed exits is deterministic and verifiable externally (i.e., sorted by validator's index resembling validator age counted from its activation).

Staking router

The StakingRouter contract is responsible for:

  • plugging validator subsets modules together
  • distribute new ether for staking to modules
  • distribute accrued rewards between modules
  • push ether together with signed keys to the DepositContract (Beacon Deposit Contract)

NodeOperatorsRegistry has the updated interface to be a pluggable module for StakingRouter.

There is also a separate contract that mitigates deposit front-running vulnerability DepositSecurityModule that involved into the staking flow.

Permissions and upgradability

The permissions model is managed by the Aragon ACL and OpenZeppelin access control.

The protocol contains both upgradable and non-upgradable contracts.

Scope of the audit

The audit scope consists of two parts: on-chain and off-chain code.

On-chain scope

The following contracts list is included in the on-chain scope:

On-chain code size:
~5000 nSLOC (using the [1] method)

NOTE: The following contracts were excluded from the scope:
**/template/**
**/mock*
**/text*

The contracts are written in Solidity 0.4.24, 0.6.12, and 0.8.9.

The project uses the hardhat framework.

Off-chain scope

The following source files are included in the off-chain scope:

./services/withdrawal.py
./services/exit_order.py
./services/validator_state.py
./services/bunker.py
./services/prediction.py
./services/safe_border.py
./variables.py
./providers/consensus/client.py
./providers/consensus/typings.py
./providers/keys/client.py
./providers/keys/typings.py
./providers/http_provider.py
./modules/ejector/ejector.py
./modules/ejector/typings.py
./modules/ejector/data_encode.py
./modules/submodules/oracle_module.py
./modules/submodules/typings.py
./modules/submodules/consensus.py
./modules/submodules/exceptions.py
./modules/accounting/extra_data.py
./modules/accounting/typings.py
./modules/accounting/accounting.py
./metrics/prometheus/basic.py
./metrics/logging.py
./metrics/healthcheck_server.py
./utils/validator_state.py
./utils/events.py
./utils/abi.py
./utils/types.py
./utils/slot.py
./utils/dataclass.py
./utils/blockstamp.py
./typings.py
./web3py/typings.py
./web3py/extensions/keys_api.py
./web3py/extensions/contracts.py
./web3py/extensions/lido_validators.py
./web3py/extensions/consensus.py
./web3py/extensions/tx_utils.py
./web3py/contract_tweak.py
./web3py/middleware.py
./constants.py
./main.py

Expected off-chain code size:
~3300 lines of code (using the loc/cloc utils method)

The off-chain code is written in Python 3.11.

Final commit to audit

The final commits to audit is:

Edits log

  • 2023-01-10: The audit scope is updated with respect to the shapella upgrade branch, more up-to-date context regarding Staking Router.
  • 2023-02-02: Latest changes described at the moment of writing: two-phase oracle details, new docs, updated contracts list.
  • 2023-02-06: Updated contracts list (feature-freeze), readiness tiers, staking router LIP-20
  • 2023-02-08: SelfOwnedStETHBurnerBurner (renamed), community call #3 and presentation links added
  • 2023-02-10: Tiers updated, scope refinements (Packed64.sol → Packed64x4.sol, LidoOracleLegacyOracle, Pausable.sol moved from libs to utils, ResizableArray.sol completely removed)
  • 2023-02-13: Tiers removed, soft code freeze is achieved.
  • 2023-02-21: New article about bunker mode. Code freeze for the off-chain part is on the 1st of March, follow-up fixes for the on-chain code ("hard freeze", commit id: e575177).
  • 2023-02-22: Notes on renamed/removed contracts (-Math64.sol, IEIP712.solIEIP712StETH.sol, WithdrawalRequestNFT.solWithdrawalQueueERC721.sol).
  • 2023-03-01: Off-chain codefreeze commit, new draft docs about oracle (Accounting oracle, Associated slashings research, Ejector oracle), new draft about finalization share rate.
  • 2023-03-14: On-chain code updated to resolve the already reported findings (2bce10d), new library contracts/common/SignatureUtils.sol for ERC-1271, see the release notes.
  • 2023-03-20: Off-chain code update to sync up with the on-chain beta.3 version. New commit: f3b314c31d9823f8c68b8ab3458f4c24a0eef004.

External references