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:
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:
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:
More details about withdrawals design landscape for Lido on Ethereum are desribed here:
https://research.lido.fi/t/withdrawals-for-lido-on-ethereum/3690
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).
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.
AccountingOracle
contract.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.
The picture below outlines the smart contracts architecture.
The architecture was presented on the Lido community call #3, see also Presentation slides.
shapella-upgrade
branch for audit assessments)The protocol has been running on top of the Aragon framework.
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).
The StakingRouter
contract is responsible for:
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.
The permissions model is managed by the Aragon ACL and OpenZeppelin access control.
The protocol contains both upgradable and non-upgradable contracts.
The audit scope consists of two parts: on-chain and off-chain code.
The following contracts list is included in the on-chain scope:
WithdrawalRequestNFT.sol
)IEIP712.sol
)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.
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.
The final commits to audit is:
e57517730c3e11a41e9cbc32ce018726722335b7
] initial commit (Lido 2.0 beta2)2bce10d4f0cb10cde11bead4719a5bcde76b93f9
] updated Lido 2.0 beta320bc575f4fae7b3139b210b92f6d05a28215a9fb
]f3b314c31d9823f8c68b8ab3458f4c24a0eef004
] updated Oracle 3.0.0-beta.1
SelfOwnedStETHBurner
→ Burner
(renamed), community call #3 and presentation links addedPacked64.sol → Packed64x4.sol
, LidoOracle
→ LegacyOracle
, Pausable.sol
moved from libs to utils, ResizableArray.sol
completely removed)e575177
).Math64.sol
, IEIP712.sol
→IEIP712StETH.sol
, WithdrawalRequestNFT.sol
→WithdrawalQueueERC721.sol
).beta.3
version. New commit: f3b314c31d9823f8c68b8ab3458f4c24a0eef004
.